aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-src/src/standard/strnatcmp.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-01 04:52:50 +0900
committernsfisis <nsfisis@gmail.com>2026-08-01 04:52:50 +0900
commitb8d46b0495d00815a699932ced0b43955c949ab9 (patch)
tree17c340f51cbf259ea631ca7da7549714c91b916f /crates/shirabe-php-src/src/standard/strnatcmp.rs
parent8e8a3c147aa388c4b0fc021a08b30113eb590727 (diff)
downloadphp-shirabe-b8d46b0495d00815a699932ced0b43955c949ab9.tar.gz
php-shirabe-b8d46b0495d00815a699932ced0b43955c949ab9.tar.zst
php-shirabe-b8d46b0495d00815a699932ced0b43955c949ab9.zip
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) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-src/src/standard/strnatcmp.rs')
-rw-r--r--crates/shirabe-php-src/src/standard/strnatcmp.rs146
1 files changed, 146 insertions, 0 deletions
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;
+ }
+}