aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/shirabe-php-shim/src/string.rs97
-rw-r--r--crates/shirabe/src/package/version/version_bumper.rs10
-rw-r--r--crates/shirabe/src/util/process_executor.rs2
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)),
);
}
}