aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-php-src')
-rw-r--r--crates/shirabe-php-src/Cargo.toml3
-rw-r--r--crates/shirabe-php-src/src/lib.rs6
-rw-r--r--crates/shirabe-php-src/src/main.rs1
-rw-r--r--crates/shirabe-php-src/src/main/snprintf.rs83
-rw-r--r--crates/shirabe-php-src/src/zend.rs1
-rw-r--r--crates/shirabe-php-src/src/zend/zend_smart_str.rs73
6 files changed, 167 insertions, 0 deletions
diff --git a/crates/shirabe-php-src/Cargo.toml b/crates/shirabe-php-src/Cargo.toml
index e2028c40..ae73c313 100644
--- a/crates/shirabe-php-src/Cargo.toml
+++ b/crates/shirabe-php-src/Cargo.toml
@@ -3,6 +3,9 @@ name = "shirabe-php-src"
version.workspace = true
edition.workspace = true
license = "BSD-3-Clause AND Zlib"
+# The module tree mirrors php-src's own tree, so `src/main.rs` is the `main/` directory of
+# php-src, not a binary entry point.
+autobins = false
[lints]
workspace = true
diff --git a/crates/shirabe-php-src/src/lib.rs b/crates/shirabe-php-src/src/lib.rs
index 1e2c2a0f..a5682e24 100644
--- a/crates/shirabe-php-src/src/lib.rs
+++ b/crates/shirabe-php-src/src/lib.rs
@@ -1,5 +1,6 @@
//! Rust port from the original C implementation in php-src.
//! See `LICENSE.md` at the repository root.
+#![allow(special_module_name)]
//!
//! Rules for this crate:
//!
@@ -16,4 +17,9 @@
//! /// php-src: ext/standard/strnatcmp.c `strnatcmp_ex` (PHP 8.5.2)
//! ```
+// The module mirrors php-src's `main/` directory; it is not a binary entry point (autobins is
+// disabled in Cargo.toml). `special_module_name` is allowed crate-wide because the item-level
+// attribute does not reach this early-pass lint.
+pub mod main;
pub mod standard;
+pub mod zend;
diff --git a/crates/shirabe-php-src/src/main.rs b/crates/shirabe-php-src/src/main.rs
new file mode 100644
index 00000000..d2bbafb0
--- /dev/null
+++ b/crates/shirabe-php-src/src/main.rs
@@ -0,0 +1 @@
+pub mod snprintf;
diff --git a/crates/shirabe-php-src/src/main/snprintf.rs b/crates/shirabe-php-src/src/main/snprintf.rs
new file mode 100644
index 00000000..f1411d6e
--- /dev/null
+++ b/crates/shirabe-php-src/src/main/snprintf.rs
@@ -0,0 +1,83 @@
+/// php-src: main/snprintf.c `php_gcvt` (PHP 8.5.8)
+///
+/// Only the `ndigit < 0` path (dtoa mode 0, the shortest round-trip representation used when
+/// `serialize_precision=-1`) is ported; the fixed-precision mode 2 path is not needed yet.
+/// The digit extraction delegates to Rust's own shortest round-trip float formatting, which
+/// produces the same digit string as `zend_dtoa` in mode 0 (both compute the unique shortest
+/// decimal that round-trips), so only the digit placement logic is ported here.
+pub fn php_gcvt(value: f64, ndigit: i32, dec_point: char, exponent: char) -> String {
+ let mode = if ndigit >= 0 { 2 } else { 0 };
+ if mode != 0 {
+ todo!("php_gcvt is only ported for ndigit < 0 (serialize_precision=-1)");
+ }
+ let ndigit = 17i32;
+
+ let (sign, digits, decpt) = dtoa_shortest(value);
+
+ let mut buf = String::new();
+ if sign {
+ buf.push('-');
+ }
+
+ if if decpt < 0 {
+ decpt < -3
+ } else {
+ decpt > ndigit
+ } {
+ // exponential format (e.g. 1.0E+17)
+ let exp = decpt - 1;
+ let mut chars = digits.chars();
+ buf.push(chars.next().expect("dtoa always yields at least one digit"));
+ buf.push(dec_point);
+ let rest = chars.as_str();
+ if rest.is_empty() {
+ buf.push('0');
+ } else {
+ buf.push_str(rest);
+ }
+ buf.push(exponent);
+ if exp < 0 {
+ buf.push('-');
+ } else {
+ buf.push('+');
+ }
+ buf.push_str(&exp.abs().to_string());
+ } else if decpt > 0 {
+ // standard format, integer part present
+ let decpt = decpt as usize;
+ if digits.len() <= decpt {
+ buf.push_str(&digits);
+ for _ in digits.len()..decpt {
+ buf.push('0');
+ }
+ } else {
+ buf.push_str(&digits[..decpt]);
+ buf.push(dec_point);
+ buf.push_str(&digits[decpt..]);
+ }
+ } else {
+ // standard format, 0.000ddd
+ buf.push('0');
+ buf.push(dec_point);
+ for _ in decpt..0 {
+ buf.push('0');
+ }
+ buf.push_str(&digits);
+ }
+
+ buf
+}
+
+/// php-src: Zend/zend_strtod.c `zend_dtoa` mode 0 equivalent: the shortest round-trip digit
+/// string of `|value|`, its sign, and the decimal point position (`value = 0.digits * 10^decpt`).
+/// Implemented on top of Rust's `{:e}` formatting, which is also shortest-round-trip.
+fn dtoa_shortest(value: f64) -> (bool, String, i32) {
+ let sign = value.is_sign_negative();
+ let formatted = format!("{:e}", value.abs());
+ let (mantissa, exp) = formatted
+ .split_once('e')
+ .expect("`{:e}` always contains an exponent");
+ let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
+ let exp: i32 = exp.parse().expect("`{:e}` exponent is a decimal integer");
+ (sign, digits, exp + 1)
+}
diff --git a/crates/shirabe-php-src/src/zend.rs b/crates/shirabe-php-src/src/zend.rs
new file mode 100644
index 00000000..a11d8e55
--- /dev/null
+++ b/crates/shirabe-php-src/src/zend.rs
@@ -0,0 +1 @@
+pub mod zend_smart_str;
diff --git a/crates/shirabe-php-src/src/zend/zend_smart_str.rs b/crates/shirabe-php-src/src/zend/zend_smart_str.rs
new file mode 100644
index 00000000..b4a9f2ec
--- /dev/null
+++ b/crates/shirabe-php-src/src/zend/zend_smart_str.rs
@@ -0,0 +1,73 @@
+use crate::main::snprintf::php_gcvt;
+
+/// php-src: Zend/zend_smart_str.c `smart_str_append_double` (PHP 8.5.8), folding in the `%H`
+/// NAN/INF handling from main/snprintf.c `format_converter` that the original reaches through
+/// `snprintf(buf, sizeof(buf), "%.*H", precision, num)`.
+pub fn smart_str_append_double(dest: &mut String, num: f64, precision: i32, zero_fraction: bool) {
+ if num.is_nan() {
+ dest.push_str("NAN");
+ return;
+ }
+ if num.is_infinite() {
+ dest.push_str(if num > 0.0 { "INF" } else { "-INF" });
+ return;
+ }
+ let buf = php_gcvt(num, precision, '.', 'E');
+ let had_period = buf.contains('.');
+ dest.push_str(&buf);
+ if zero_fraction && !had_period {
+ dest.push_str(".0");
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn serialize_repr(num: f64) -> String {
+ let mut out = String::new();
+ smart_str_append_double(&mut out, num, -1, false);
+ out
+ }
+
+ // Expected strings are the output of `serialize()` under PHP 8.5.8 with
+ // serialize_precision=-1 (without the `d:`/`;` wrapper).
+ #[test]
+ fn matches_php_serialize_output() {
+ assert_eq!(serialize_repr(0.0), "0");
+ assert_eq!(serialize_repr(-0.0), "-0");
+ assert_eq!(serialize_repr(1.5), "1.5");
+ assert_eq!(serialize_repr(0.1), "0.1");
+ assert_eq!(serialize_repr(2.0), "2");
+ assert_eq!(serialize_repr(-2.0), "-2");
+ assert_eq!(serialize_repr(100.0), "100");
+ assert_eq!(serialize_repr(1e15), "1000000000000000");
+ assert_eq!(serialize_repr(1e16), "10000000000000000");
+ assert_eq!(serialize_repr(1e17), "1.0E+17");
+ assert_eq!(serialize_repr(1e18), "1.0E+18");
+ assert_eq!(serialize_repr(1e20), "1.0E+20");
+ assert_eq!(serialize_repr(1.5e20), "1.5E+20");
+ assert_eq!(serialize_repr(1e-4), "0.0001");
+ assert_eq!(serialize_repr(1e-5), "1.0E-5");
+ assert_eq!(serialize_repr(12345.6789e-9), "1.23456789E-5");
+ assert_eq!(serialize_repr(1e-300), "1.0E-300");
+ assert_eq!(serialize_repr(f64::MAX), "1.7976931348623157E+308");
+ assert_eq!(serialize_repr(5e-324), "5.0E-324");
+ assert_eq!(serialize_repr(1.0 / 3.0), "0.3333333333333333");
+ assert_eq!(serialize_repr(0.30000000000000004), "0.30000000000000004");
+ assert_eq!(serialize_repr(f64::NAN), "NAN");
+ assert_eq!(serialize_repr(f64::INFINITY), "INF");
+ assert_eq!(serialize_repr(f64::NEG_INFINITY), "-INF");
+ }
+
+ #[test]
+ fn zero_fraction_appends_dot_zero_to_integral_values() {
+ let mut out = String::new();
+ smart_str_append_double(&mut out, 2.0, -1, true);
+ assert_eq!(out, "2.0");
+
+ let mut out = String::new();
+ smart_str_append_double(&mut out, 1e17, -1, true);
+ assert_eq!(out, "1.0E+17");
+ }
+}