From b8d46b0495d00815a699932ced0b43955c949ab9 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 1 Aug 2026 04:52:50 +0900 Subject: feat(php-src): add a BSD-licensed crate for php-src derived code The audit in .ken/php-shim-copying.md judged 14 functions in shirabe-php-shim (plus php_wordwrap in shirabe-external-packages) to be line-by-line transcriptions or structural imitations of php-src. PHP's relicensing to 3-clause BSD makes keeping them legal, but the boundary between BSD-derived and MIT code was invisible in the source tree. Moving them into their own crate puts the license into the build metadata (so NOTICE generation follows the binary), makes a reverse dependency a compile error, and encodes the origin in the module path, which mirrors php-src's ext tree. Each function records its origin in a fixed-format doc comment, and a new php_src_derivation_boundary linter fails if `php-src` appears in any Rust source outside the crate. Public paths under shirabe_php_shim:: are unchanged: functions that are themselves derived are re-exported with `pub use`, and the wrappers that only validate arguments stay on the MIT side. This also resolves the duplicate wordwrap implementation. shirabe_php_shim::wordwrap was todo!(), so SymfonyStyle::block panicked, while shirabe-external-packages carried its own copy. Both now go through the single port, verified against real PHP on 13 cases covering multi-character breaks and cut. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe-php-src/src/standard/strnatcmp.rs | 146 +++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 crates/shirabe-php-src/src/standard/strnatcmp.rs (limited to 'crates/shirabe-php-src/src/standard/strnatcmp.rs') diff --git a/crates/shirabe-php-src/src/standard/strnatcmp.rs b/crates/shirabe-php-src/src/standard/strnatcmp.rs new file mode 100644 index 00000000..30f4176a --- /dev/null +++ b/crates/shirabe-php-src/src/standard/strnatcmp.rs @@ -0,0 +1,146 @@ +/// php-src: ext/standard/strnatcmp.c `strnatcmp_ex` (PHP 8.5.2) +/// +/// Operating on byte slices, an out-of-range index reads as 0, reproducing the NUL terminator +/// that the C implementation relies on. +pub fn strnatcmp_ex(a: &[u8], b: &[u8], fold_case: bool) -> i64 { + let a_len = a.len(); + let b_len = b.len(); + if a_len == 0 || b_len == 0 { + return match a_len.cmp(&b_len) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Greater => 1, + std::cmp::Ordering::Equal => 0, + }; + } + + let mut ap = 0usize; + let mut bp = 0usize; + let mut leading = true; + loop { + let mut ca = natcmp_at(a, ap); + let mut cb = natcmp_at(b, bp); + + // Skip over leading zeros. + while leading && ca == b'0' && natcmp_at(a, ap + 1).is_ascii_digit() { + ap += 1; + ca = natcmp_at(a, ap); + } + while leading && cb == b'0' && natcmp_at(b, bp + 1).is_ascii_digit() { + bp += 1; + cb = natcmp_at(b, bp); + } + leading = false; + + // Skip consecutive whitespace. + while natcmp_is_space(ca) { + ap += 1; + ca = natcmp_at(a, ap); + } + while natcmp_is_space(cb) { + bp += 1; + cb = natcmp_at(b, bp); + } + + // Process a run of digits. + if ca.is_ascii_digit() && cb.is_ascii_digit() { + let fractional = ca == b'0' || cb == b'0'; + let result = if fractional { + natcmp_compare_left(a, &mut ap, b, &mut bp) + } else { + natcmp_compare_right(a, &mut ap, b, &mut bp) + }; + if result != 0 { + return result; + } + } + + if ap == a_len && bp == b_len { + return 0; + } else if ap == a_len { + return -1; + } else if bp == b_len { + return 1; + } + + if fold_case { + ca = natcmp_at(a, ap).to_ascii_uppercase(); + cb = natcmp_at(b, bp).to_ascii_uppercase(); + } else { + ca = natcmp_at(a, ap); + cb = natcmp_at(b, bp); + } + + if ca < cb { + return -1; + } else if ca > cb { + return 1; + } + + ap += 1; + bp += 1; + } +} + +/// php-src: ext/standard/strnatcmp.c (no direct counterpart) (PHP 8.5.2) +fn natcmp_at(s: &[u8], i: usize) -> u8 { + if i < s.len() { s[i] } else { 0 } +} + +/// php-src: ext/standard/strnatcmp.c `isspace(3)` usage (PHP 8.5.2) +fn natcmp_is_space(c: u8) -> bool { + matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r') +} + +/// php-src: ext/standard/strnatcmp.c `compare_right` (PHP 8.5.2) +/// +/// Compare two right-aligned numbers: the longest run of digits wins; failing that, the first +/// differing digit decides, but only once magnitudes are known equal (tracked in `bias`). +fn natcmp_compare_right(a: &[u8], ap: &mut usize, b: &[u8], bp: &mut usize) -> i64 { + let mut bias = 0i64; + loop { + let ca = natcmp_at(a, *ap); + let cb = natcmp_at(b, *bp); + let a_digit = ca.is_ascii_digit(); + let b_digit = cb.is_ascii_digit(); + if !a_digit && !b_digit { + return bias; + } else if !a_digit { + return -1; + } else if !b_digit { + return 1; + } else if ca < cb { + if bias == 0 { + bias = -1; + } + } else if ca > cb && bias == 0 { + bias = 1; + } + *ap += 1; + *bp += 1; + } +} + +/// php-src: ext/standard/strnatcmp.c `compare_left` (PHP 8.5.2) +/// +/// Compare two left-aligned numbers: the first differing digit decides. +fn natcmp_compare_left(a: &[u8], ap: &mut usize, b: &[u8], bp: &mut usize) -> i64 { + loop { + let ca = natcmp_at(a, *ap); + let cb = natcmp_at(b, *bp); + let a_digit = ca.is_ascii_digit(); + let b_digit = cb.is_ascii_digit(); + if !a_digit && !b_digit { + return 0; + } else if !a_digit { + return -1; + } else if !b_digit { + return 1; + } else if ca < cb { + return -1; + } else if ca > cb { + return 1; + } + *ap += 1; + *bp += 1; + } +} -- cgit v1.3.1