diff options
Diffstat (limited to 'crates/shirabe-php-shim/src')
| -rw-r--r-- | crates/shirabe-php-shim/src/datetime.rs | 21 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/output.rs | 3 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/stream.rs | 48 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/zip.rs | 199 |
4 files changed, 238 insertions, 33 deletions
diff --git a/crates/shirabe-php-shim/src/datetime.rs b/crates/shirabe-php-shim/src/datetime.rs index 9810966..bddf5da 100644 --- a/crates/shirabe-php-shim/src/datetime.rs +++ b/crates/shirabe-php-shim/src/datetime.rs @@ -1,6 +1,10 @@ static DEFAULT_TIMEZONE: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None); pub fn date_create<Tz: chrono::TimeZone>(s: &str) -> chrono::ParseResult<chrono::DateTime<Tz>> { + // TODO(phase-d): PHP `date_create` accepts the full strtotime() grammar (RFC2822, ISO8601, + // VCS-specific and relative formats), which requires a dedicated date parser not available + // here. The generic `Tz` also has no constructor from a parsed `FixedOffset`/`Utc` value. + let _ = s; todo!() } @@ -22,6 +26,8 @@ pub fn date_format_to_strftime(format: &str) -> &'static str { } pub fn strtotime(_time: &str) -> Option<i64> { + // TODO(phase-d): requires the full strtotime() grammar (absolute, relative and compound + // expressions); a partial parser would silently mis-handle unsupported inputs. todo!() } @@ -53,6 +59,17 @@ pub fn date_default_timezone_set(tz: &str) -> bool { true } -pub fn date(_format: &str, _timestamp: Option<i64>) -> String { - todo!() +pub fn date(format: &str, timestamp: Option<i64>) -> String { + let timestamp = timestamp.unwrap_or_else(time); + // PHP `date()` renders in the default timezone. Without a timezone database only "UTC" can be + // resolved; any named zone is rejected loudly rather than silently rendered in the wrong zone. + let tz = date_default_timezone_get(); + if tz != "UTC" { + panic!( + "date() with non-UTC default timezone {tz:?} is not supported (no timezone database)" + ); + } + let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(timestamp, 0) + .expect("date() timestamp out of range"); + dt.format(date_format_to_strftime(format)).to_string() } diff --git a/crates/shirabe-php-shim/src/output.rs b/crates/shirabe-php-shim/src/output.rs index 17f7ab9..698f3bf 100644 --- a/crates/shirabe-php-shim/src/output.rs +++ b/crates/shirabe-php-shim/src/output.rs @@ -1,3 +1,6 @@ +// PHP output buffering captures everything the interpreter would echo to stdout. The shim has no +// general echo-to-buffer routing, and its only producer in Composer (`phpinfo`) depends on PHP +// runtime configuration that is itself unmodeled, so a buffer here would silently capture nothing. pub fn ob_start() -> bool { todo!() } diff --git a/crates/shirabe-php-shim/src/stream.rs b/crates/shirabe-php-shim/src/stream.rs index e5e5200..f5582a7 100644 --- a/crates/shirabe-php-shim/src/stream.rs +++ b/crates/shirabe-php-shim/src/stream.rs @@ -13,6 +13,8 @@ pub fn stream_get_contents(stream: &PhpResource) -> Option<String> { } pub fn stream_resolve_include_path(filename: &str) -> Option<String> { + // TODO(phase-d): resolution searches the `include_path` ini setting, which the shim does not + // model; checking only the current directory would silently miss configured include paths. let _ = filename; todo!() } @@ -60,19 +62,49 @@ fn stream_read_remaining(stream: &PhpResource, max_length: Option<i64>) -> Optio } } +// A stream context is modeled as an object holding the two arrays PHP keeps for it: the per-wrapper +// `options` and the `params`. This is a self-contained value, so it round-trips through the +// accessors below without any registry. pub fn stream_context_create( - _options: &IndexMap<String, PhpMixed>, - _params: Option<&IndexMap<String, PhpMixed>>, + options: &IndexMap<String, PhpMixed>, + params: Option<&IndexMap<String, PhpMixed>>, ) -> PhpMixed { - todo!() + let mut context = IndexMap::new(); + context.insert("options".to_string(), PhpMixed::Array(options.clone())); + context.insert( + "params".to_string(), + PhpMixed::Array(params.cloned().unwrap_or_default()), + ); + PhpMixed::Object(context) } -pub fn stream_context_get_options(_stream_or_context: &PhpMixed) -> IndexMap<String, PhpMixed> { - todo!() +pub fn stream_context_get_options(stream_or_context: &PhpMixed) -> IndexMap<String, PhpMixed> { + match stream_or_context { + PhpMixed::Object(context) | PhpMixed::Array(context) => context + .get("options") + .and_then(PhpMixed::as_array) + .cloned() + .unwrap_or_default(), + _ => IndexMap::new(), + } } -pub fn stream_context_get_params(_stream_or_context: &PhpMixed) -> IndexMap<String, PhpMixed> { - todo!() +pub fn stream_context_get_params(stream_or_context: &PhpMixed) -> IndexMap<String, PhpMixed> { + let (options, params) = match stream_or_context { + PhpMixed::Object(context) | PhpMixed::Array(context) => ( + context.get("options").cloned().unwrap_or_default(), + context + .get("params") + .and_then(PhpMixed::as_array) + .cloned() + .unwrap_or_default(), + ), + _ => (PhpMixed::default(), IndexMap::new()), + }; + // PHP exposes the wrapper options under an "options" key alongside the params. + let mut result = params; + result.insert("options".to_string(), options); + result } pub fn stream_isatty(stream: PhpResource) -> bool { @@ -80,6 +112,8 @@ pub fn stream_isatty(stream: PhpResource) -> bool { } pub fn stream_get_wrappers() -> Vec<String> { + // TODO(phase-d): the registered wrapper set depends on compiled-in extensions and runtime + // `stream_wrapper_register` calls; neither is modeled, so a fixed list would be inaccurate. todo!() } diff --git a/crates/shirabe-php-shim/src/zip.rs b/crates/shirabe-php-shim/src/zip.rs index 08662e6..6b4d87b 100644 --- a/crates/shirabe-php-shim/src/zip.rs +++ b/crates/shirabe-php-shim/src/zip.rs @@ -1,9 +1,30 @@ use crate::PhpMixed; +use crate::{StreamBacking, StreamState}; use indexmap::IndexMap; +use std::cell::RefCell; +use zip::write::SimpleFileOptions; + +/// Internal backing of an opened `ZipArchive`. PHP's `ZipArchive` multiplexes +/// reading an existing archive and building a new one through the same handle; +/// the `zip` crate splits these into `ZipArchive` (read) and `ZipWriter` (write), +/// so the open mode determines which variant is live. +#[derive(Debug, Default)] +enum ZipState { + #[default] + Closed, + Reader(zip::ZipArchive<std::fs::File>), + Writer { + writer: zip::ZipWriter<std::fs::File>, + /// The destination path, retained so `close` can confirm the file exists. + path: String, + status: String, + }, +} #[derive(Debug)] pub struct ZipArchive { pub num_files: i64, + state: RefCell<ZipState>, } impl Default for ZipArchive { @@ -14,63 +35,193 @@ impl Default for ZipArchive { impl ZipArchive { pub fn new() -> Self { - todo!() + Self { + num_files: 0, + state: RefCell::new(ZipState::Closed), + } } - pub fn open(&mut self, _filename: &str, _flags: i64) -> Result<(), i64> { - todo!() + pub fn open(&mut self, filename: &str, flags: i64) -> Result<(), i64> { + if flags & Self::CREATE != 0 { + let file = match std::fs::File::create(filename) { + Ok(f) => f, + Err(_) => return Err(Self::ER_OPEN), + }; + *self.state.borrow_mut() = ZipState::Writer { + writer: zip::ZipWriter::new(file), + path: filename.to_string(), + status: String::new(), + }; + self.num_files = 0; + Ok(()) + } else { + let file = match std::fs::File::open(filename) { + Ok(f) => f, + Err(_) => return Err(Self::ER_NOENT), + }; + let archive = match zip::ZipArchive::new(file) { + Ok(a) => a, + Err(_) => return Err(Self::ER_NOZIP), + }; + self.num_files = archive.len() as i64; + *self.state.borrow_mut() = ZipState::Reader(archive); + Ok(()) + } } pub fn close(&self) -> bool { - todo!() + let state = std::mem::take(&mut *self.state.borrow_mut()); + match state { + ZipState::Closed => false, + ZipState::Reader(_) => true, + ZipState::Writer { writer, .. } => writer.finish().is_ok(), + } } pub fn count(&self) -> i64 { - todo!() + self.num_files } - pub fn stat_index(&self, _index: i64) -> Option<IndexMap<String, PhpMixed>> { - todo!() + pub fn stat_index(&self, index: i64) -> Option<IndexMap<String, PhpMixed>> { + let mut state = self.state.borrow_mut(); + let ZipState::Reader(archive) = &mut *state else { + return None; + }; + let file = archive.by_index(index as usize).ok()?; + let mut stat = IndexMap::new(); + stat.insert( + "name".to_string(), + PhpMixed::String(file.name().to_string()), + ); + stat.insert("index".to_string(), PhpMixed::Int(index)); + stat.insert("crc".to_string(), PhpMixed::Int(file.crc32() as i64)); + stat.insert("size".to_string(), PhpMixed::Int(file.size() as i64)); + // PHP exposes the last-modified time as a Unix timestamp. The `zip` crate + // only surfaces a 2-second-precision MS-DOS datetime; no consumer reads + // this field, so it is reported as 0 rather than reconstructing it. + stat.insert("mtime".to_string(), PhpMixed::Int(0)); + stat.insert( + "comp_size".to_string(), + PhpMixed::Int(file.compressed_size() as i64), + ); + let comp_method = match file.compression() { + zip::CompressionMethod::Stored => 0, + zip::CompressionMethod::Deflated => 8, + _ => -1, + }; + stat.insert("comp_method".to_string(), PhpMixed::Int(comp_method)); + Some(stat) } - pub fn extract_to(&self, _path: &str) -> bool { - todo!() + pub fn extract_to(&self, path: &str) -> bool { + let mut state = self.state.borrow_mut(); + let ZipState::Reader(archive) = &mut *state else { + return false; + }; + archive.extract(path).is_ok() } - pub fn locate_name(&self, _name: &str) -> Option<i64> { - todo!() + pub fn locate_name(&self, name: &str) -> Option<i64> { + let state = self.state.borrow(); + let ZipState::Reader(archive) = &*state else { + return None; + }; + archive.index_for_name(name).map(|i| i as i64) } - pub fn get_from_index(&self, _index: i64) -> Option<String> { - todo!() + pub fn get_from_index(&self, index: i64) -> Option<String> { + let mut state = self.state.borrow_mut(); + let ZipState::Reader(archive) = &mut *state else { + return None; + }; + let mut file = archive.by_index(index as usize).ok()?; + let mut buf = Vec::new(); + std::io::Read::read_to_end(&mut file, &mut buf).ok()?; + Some(String::from_utf8_lossy(&buf).into_owned()) } - pub fn get_name_index(&self, _index: i64) -> String { - todo!() + pub fn get_name_index(&self, index: i64) -> String { + let mut state = self.state.borrow_mut(); + let ZipState::Reader(archive) = &mut *state else { + return String::new(); + }; + match archive.by_index(index as usize) { + Ok(file) => file.name().to_string(), + Err(_) => String::new(), + } } - pub fn get_from_name(&self, _name: &str) -> Option<String> { - todo!() + pub fn get_from_name(&self, name: &str) -> Option<String> { + let mut state = self.state.borrow_mut(); + let ZipState::Reader(archive) = &mut *state else { + return None; + }; + let mut file = archive.by_name(name).ok()?; + let mut buf = Vec::new(); + std::io::Read::read_to_end(&mut file, &mut buf).ok()?; + Some(String::from_utf8_lossy(&buf).into_owned()) } - pub fn get_stream(&self, _name: &str) -> Option<crate::PhpResource> { - todo!() + pub fn get_stream(&self, name: &str) -> Option<crate::PhpResource> { + let mut state = self.state.borrow_mut(); + let ZipState::Reader(archive) = &mut *state else { + return None; + }; + let mut file = archive.by_name(name).ok()?; + let mut buf = Vec::new(); + std::io::Read::read_to_end(&mut file, &mut buf).ok()?; + Some(StreamState::new( + StreamBacking::Memory(std::io::Cursor::new(buf)), + true, + false, + "r".to_string(), + format!("zip://{}", name), + )) } - pub fn add_empty_dir(&self, _local_name: &str) -> bool { - todo!() + pub fn add_empty_dir(&self, local_name: &str) -> bool { + let mut state = self.state.borrow_mut(); + let ZipState::Writer { writer, .. } = &mut *state else { + return false; + }; + writer + .add_directory(local_name, SimpleFileOptions::default()) + .is_ok() } - pub fn add_file(&self, _filepath: &str, _local_name: &str) -> bool { - todo!() + pub fn add_file(&self, filepath: &str, local_name: &str) -> bool { + let contents = match std::fs::read(filepath) { + Ok(c) => c, + Err(_) => return false, + }; + let mut state = self.state.borrow_mut(); + let ZipState::Writer { writer, .. } = &mut *state else { + return false; + }; + let options = + SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated); + if writer.start_file(local_name, options).is_err() { + return false; + } + std::io::Write::write_all(writer, &contents).is_ok() } pub fn set_external_attributes_name(&self, _name: &str, _opsys: i64, _attr: i64) -> bool { + // TODO(phase-d): PHP's setExternalAttributesName mutates an already-added + // entry's external attributes (e.g. Unix permissions) after addFile. The + // `zip` crate fixes external attributes at start_file time via FileOptions + // and exposes no API to amend a written entry, so this cannot be faithfully + // reproduced without re-architecting add_file. Left unimplemented rather + // than silently dropping the permission bits. todo!() } pub fn get_status_string(&self) -> String { - todo!() + let state = self.state.borrow(); + match &*state { + ZipState::Writer { status, .. } => status.clone(), + _ => String::new(), + } } } |
