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/versioning.rs | 116 ++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 crates/shirabe-php-src/src/standard/versioning.rs (limited to 'crates/shirabe-php-src/src/standard/versioning.rs') diff --git a/crates/shirabe-php-src/src/standard/versioning.rs b/crates/shirabe-php-src/src/standard/versioning.rs new file mode 100644 index 00000000..27f06309 --- /dev/null +++ b/crates/shirabe-php-src/src/standard/versioning.rs @@ -0,0 +1,116 @@ +/// php-src: ext/standard/versioning.c `php_version_compare` (PHP 8.5.2) +/// +/// Returns -1, 0 or 1. The original walks the canonicalized strings with destructive `.` splits +/// and moving pointers; this splits into a `Vec<&str>` and indexes instead. +pub fn php_version_compare(v1: &str, v2: &str) -> i32 { + if v1.is_empty() || v2.is_empty() { + return match (v1.is_empty(), v2.is_empty()) { + (true, true) => 0, + (false, _) => 1, + (_, false) => -1, + }; + } + let c1 = canonicalize_version(v1); + let c2 = canonicalize_version(v2); + let t1: Vec<&str> = c1.split('.').filter(|s| !s.is_empty()).collect(); + let t2: Vec<&str> = c2.split('.').filter(|s| !s.is_empty()).collect(); + + let mut compare = 0; + let mut i = 0; + while i < t1.len() && i < t2.len() && compare == 0 { + compare = version_token_compare(t1[i], t2[i]); + i += 1; + } + if compare == 0 { + // A leftover numeric token wins; a leftover special form is compared against the implicit + // release baseline ("#", order 4). + if i < t1.len() { + let p = t1[i]; + compare = if p.as_bytes()[0].is_ascii_digit() { + 1 + } else { + special_form_order(p).cmp(&4) as i32 + }; + } else if i < t2.len() { + let p = t2[i]; + compare = if p.as_bytes()[0].is_ascii_digit() { + -1 + } else { + 4.cmp(&special_form_order(p)) as i32 + }; + } + } + compare +} + +/// php-src: ext/standard/versioning.c `php_canonicalize_version` (PHP 8.5.2) +/// +/// Separators (-, _, +, .) collapse to a single '.', and a '.' is inserted at every digit <-> +/// non-digit boundary. The original's `!isalnum(*p)` branch is not ported. +fn canonicalize_version(version: &str) -> String { + let bytes = version.as_bytes(); + if bytes.is_empty() { + return String::new(); + } + let mut q: Vec = Vec::with_capacity(bytes.len() * 2); + q.push(bytes[0]); + for &raw in &bytes[1..] { + let ch = if matches!(raw, b'-' | b'_' | b'+') { + b'.' + } else { + raw + }; + let last = *q.last().unwrap(); + if ch == b'.' { + if last != b'.' { + q.push(b'.'); + } + } else if last.is_ascii_digit() != ch.is_ascii_digit() { + q.push(b'.'); + q.push(ch); + } else { + q.push(ch); + } + } + String::from_utf8_lossy(&q).into_owned() +} + +/// php-src: ext/standard/versioning.c `php_version_compare` loop body (PHP 8.5.2) +fn version_token_compare(t1: &str, t2: &str) -> i32 { + let d1 = t1.as_bytes()[0].is_ascii_digit(); + let d2 = t2.as_bytes()[0].is_ascii_digit(); + if d1 && d2 { + let l1 = t1.parse::().unwrap_or(0); + let l2 = t2.parse::().unwrap_or(0); + l1.cmp(&l2) as i32 + } else if !d1 && !d2 { + special_form_order(t1).cmp(&special_form_order(t2)) as i32 + } else if d1 { + // A numeric token is treated as the "#" form (order 4). + 4.cmp(&special_form_order(t2)) as i32 + } else { + special_form_order(t1).cmp(&4) as i32 + } +} + +/// php-src: ext/standard/versioning.c `compare_special_version_forms` (PHP 8.5.2) +fn special_form_order(form: &str) -> i32 { + const FORMS: &[(&str, i32)] = &[ + ("dev", 0), + ("alpha", 1), + ("a", 1), + ("beta", 2), + ("b", 2), + ("RC", 3), + ("rc", 3), + ("#", 4), + ("pl", 5), + ("p", 5), + ]; + for (name, order) in FORMS { + if form.starts_with(name) { + return *order; + } + } + -1 +} -- cgit v1.3.1