diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-16 16:24:58 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-16 16:47:52 +0900 |
| commit | 5ed66b5485d20203970d594229d32458b3244ebb (patch) | |
| tree | 6a88bfa7c127b82ec06406ee7a5b8e29e33e4fcc | |
| parent | 5e4bd47bf92e2fe151e3905ee8ab9972c9f70f8d (diff) | |
| download | php-shirabe-5ed66b5485d20203970d594229d32458b3244ebb.tar.gz php-shirabe-5ed66b5485d20203970d594229d32458b3244ebb.tar.zst php-shirabe-5ed66b5485d20203970d594229d32458b3244ebb.zip | |
fix(php-shim): accept negative offsets in substr_replace
The signature took usize, so PHP's negative $start and $length, which
count from the end of the string, could not be expressed. Take i64 and
an optional length, and apply PHP's clamping rules.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| -rw-r--r-- | crates/shirabe-php-shim/src/string.rs | 97 | ||||
| -rw-r--r-- | crates/shirabe/src/package/version/version_bumper.rs | 10 | ||||
| -rw-r--r-- | crates/shirabe/src/util/process_executor.rs | 2 |
3 files changed, 98 insertions, 11 deletions
diff --git a/crates/shirabe-php-shim/src/string.rs b/crates/shirabe-php-shim/src/string.rs index de1a3b6e..024a3943 100644 --- a/crates/shirabe-php-shim/src/string.rs +++ b/crates/shirabe-php-shim/src/string.rs @@ -22,13 +22,31 @@ pub fn substr_count(haystack: &str, needle: &str) -> i64 { haystack.matches(needle).count() as i64 } -// Byte-based, matching PHP's substr_replace. -// TODO(php-semantics): PHP accepts negative $start/$length (counting from the end); this signature takes -// usize and therefore cannot express those cases. -pub fn substr_replace(string: &str, replace: &str, start: usize, length: usize) -> String { +// Byte-based, matching PHP's substr_replace. `length` of `None` is PHP's omitted `$length`, +// which replaces up to the end of the string. +// TODO(bytes): slicing at arbitrary byte offsets can split a multi-byte sequence, and the +// `String` return type forces the resulting invalid bytes through `from_utf8_lossy`. +pub fn substr_replace(string: &str, replace: &str, start: i64, length: Option<i64>) -> String { let bytes = string.as_bytes(); - let start = start.min(bytes.len()); - let end = start.saturating_add(length).min(bytes.len()); + let str_len = bytes.len() as i64; + + // A negative start counts from the end of the string, clamped to both ends of the string. + let start = if start < 0 { + (str_len + start).max(0) + } else { + start.min(str_len) + }; + + // A negative length means "stop that many bytes before the end of the string"; the result is + // clamped to an empty range rather than becoming negative. + let mut length = length.unwrap_or(str_len); + if length < 0 { + length = (str_len - start + length).max(0); + } + length = length.min(str_len - start); + + let start = start as usize; + let end = start + length as usize; let mut out: Vec<u8> = Vec::with_capacity(bytes.len() + replace.len()); out.extend_from_slice(&bytes[..start]); out.extend_from_slice(replace.as_bytes()); @@ -1084,3 +1102,70 @@ pub fn uniqid(prefix: &str, more_entropy: bool) -> String { base } } + +#[cfg(test)] +mod tests { + use super::substr_replace; + + #[test] + fn substr_replace_matches_php() { + // Expected values are the output of PHP 8.5.9's substr_replace(). + assert_eq!(substr_replace("Hello World", "XX", 0, None), "XX"); + assert_eq!(substr_replace("Hello World", "XX", 6, None), "Hello XX"); + assert_eq!(substr_replace("Hello World", "XX", 6, Some(5)), "Hello XX"); + assert_eq!( + substr_replace("Hello World", "XX", 6, Some(100)), + "Hello XX" + ); + assert_eq!( + substr_replace("Hello World", "XX", 20, Some(3)), + "Hello WorldXX" + ); + assert_eq!( + substr_replace("Hello World", "XX", 11, Some(3)), + "Hello WorldXX" + ); + assert_eq!(substr_replace("Hello World", "XX", -5, None), "Hello XX"); + assert_eq!( + substr_replace("Hello World", "XX", -5, Some(2)), + "Hello XXrld" + ); + assert_eq!( + substr_replace("Hello World", "XX", -100, Some(3)), + "XXlo World" + ); + assert_eq!(substr_replace("Hello World", "XX", -100, None), "XX"); + assert_eq!(substr_replace("Hello World", "XX", 0, Some(-5)), "XXWorld"); + assert_eq!( + substr_replace("Hello World", "XX", 0, Some(-100)), + "XXHello World" + ); + assert_eq!(substr_replace("Hello World", "XX", 3, Some(-3)), "HelXXrld"); + assert_eq!( + substr_replace("Hello World", "XX", -5, Some(-2)), + "Hello XXld" + ); + assert_eq!( + substr_replace("Hello World", "XX", -5, Some(-100)), + "Hello XXWorld" + ); + assert_eq!( + substr_replace("Hello World", "XX", 6, Some(0)), + "Hello XXWorld" + ); + assert_eq!( + substr_replace("Hello World", "XX", 0, Some(0)), + "XXHello World" + ); + assert_eq!(substr_replace("", "XX", 0, None), "XX"); + assert_eq!(substr_replace("", "XX", 5, Some(3)), "XX"); + assert_eq!(substr_replace("", "XX", -5, Some(-3)), "XX"); + assert_eq!(substr_replace("Hello World", "", 0, Some(5)), " World"); + assert_eq!(substr_replace("Hello World", "", -5, None), "Hello "); + assert_eq!(substr_replace("abc", "X", 1, Some(-1)), "aXc"); + assert_eq!(substr_replace("abc", "X", 2, Some(-2)), "abXc"); + assert_eq!(substr_replace("abc", "X", -1, Some(-1)), "abXc"); + assert_eq!(substr_replace("abc", "X", 3, Some(0)), "abcX"); + assert_eq!(substr_replace("abc", "X", 3, None), "abcX"); + } +} diff --git a/crates/shirabe/src/package/version/version_bumper.rs b/crates/shirabe/src/package/version/version_bumper.rs index c0e76520..fcdbf246 100644 --- a/crates/shirabe/src/package/version/version_bumper.rs +++ b/crates/shirabe/src/package/version/version_bumper.rs @@ -112,10 +112,12 @@ impl VersionBumper { } else { format!("{}{}", new_pretty_constraint, suffix) }; - let offset = match_offset as usize; - let length = Platform::strlen(match_str) as usize; - modified = - shirabe_php_shim::substr_replace(&modified, &replacement, offset, length); + modified = shirabe_php_shim::substr_replace( + &modified, + &replacement, + match_offset, + Some(Platform::strlen(match_str)), + ); } let new_constraint = parser.parse_constraints(&modified)?; diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs index 00091029..875c8810 100644 --- a/crates/shirabe/src/util/process_executor.rs +++ b/crates/shirabe/src/util/process_executor.rs @@ -224,7 +224,7 @@ impl ProcessExecutor { &command_str, &Self::escape(&Self::get_executable(&m1)), 0, - strlen(&m1) as usize, + Some(strlen(&m1)), ); } } |
