aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-external-packages')
-rw-r--r--crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs733
1 files changed, 688 insertions, 45 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs b/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs
index 349f7ca..ba5b764 100644
--- a/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs
+++ b/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs
@@ -1,5 +1,6 @@
//! ref: composer/vendor/symfony/filesystem/Filesystem.php
+use crate::symfony::filesystem::exception::io_exception::IOException;
use shirabe_php_shim::PhpMixed;
#[derive(Debug, Clone)]
@@ -13,50 +14,283 @@ impl Default for Filesystem {
impl Filesystem {
pub fn new() -> Self {
- todo!()
+ Filesystem
+ }
+
+ // Symfony's toIterable(): a single string yields one element, an array/iterable yields each
+ // element as a string.
+ fn to_iterable(files: &PhpMixed) -> Vec<String> {
+ match files {
+ PhpMixed::String(s) => vec![s.clone()],
+ PhpMixed::List(items) => items
+ .iter()
+ .map(|item| item.as_string().unwrap_or("").to_string())
+ .collect(),
+ PhpMixed::Array(entries) => entries
+ .values()
+ .map(|item| item.as_string().unwrap_or("").to_string())
+ .collect(),
+ _ => vec![files.as_string().unwrap_or("").to_string()],
+ }
}
pub fn copy(
&self,
- _origin_file: &str,
- _target_file: &str,
- _override_file: bool,
+ origin_file: &str,
+ target_file: &str,
+ override_file: bool,
) -> anyhow::Result<()> {
- todo!()
+ // PHP: stream_is_local($originFile) || 0 === stripos($originFile, 'file://')
+ let origin_is_local = Self::stream_is_local(origin_file)
+ || shirabe_php_shim::stripos(origin_file, "file://") == Some(0);
+
+ if origin_is_local && !shirabe_php_shim::is_file(origin_file) {
+ return Err(IOException::new(
+ format!(
+ "Failed to copy \"{}\" because file does not exist.",
+ origin_file
+ ),
+ 0,
+ None,
+ Some(origin_file.to_string()),
+ )
+ .into());
+ }
+
+ self.mkdir(
+ PhpMixed::String(shirabe_php_shim::dirname(target_file)),
+ 0o777,
+ )?;
+
+ let mut do_copy = true;
+ // PHP: !$overwriteNewerFiles && !parse_url($originFile, PHP_URL_HOST) && is_file($targetFile)
+ let origin_host = shirabe_php_shim::parse_url(origin_file, shirabe_php_shim::PHP_URL_HOST);
+ if !override_file
+ && matches!(origin_host, PhpMixed::Null | PhpMixed::Bool(false))
+ && shirabe_php_shim::is_file(target_file)
+ {
+ do_copy = shirabe_php_shim::filemtime(origin_file).unwrap_or(0)
+ > shirabe_php_shim::filemtime(target_file).unwrap_or(0);
+ }
+
+ if do_copy {
+ let bytes_copied = match shirabe_php_shim::copy(origin_file, target_file) {
+ true => shirabe_php_shim::filesize(target_file).unwrap_or(0),
+ false => {
+ return Err(IOException::new(
+ format!("Failed to copy \"{}\" to \"{}\".", origin_file, target_file),
+ 0,
+ None,
+ Some(origin_file.to_string()),
+ )
+ .into());
+ }
+ };
+
+ if !shirabe_php_shim::is_file(target_file) {
+ return Err(IOException::new(
+ format!("Failed to copy \"{}\" to \"{}\".", origin_file, target_file),
+ 0,
+ None,
+ Some(origin_file.to_string()),
+ )
+ .into());
+ }
+
+ if origin_is_local {
+ // Like `cp`, preserve executable permission bits.
+ shirabe_php_shim::chmod(
+ target_file,
+ (shirabe_php_shim::fileperms(target_file)
+ | (shirabe_php_shim::fileperms(origin_file) & 0o111))
+ as u32,
+ );
+
+ // Like `cp`, preserve the file modification time. The shim's touch2/touch3 (explicit
+ // mtime) are unimplemented (no utimensat), so mtime preservation is omitted here.
+
+ let bytes_origin = shirabe_php_shim::filesize(origin_file).unwrap_or(0);
+ if bytes_copied != bytes_origin {
+ return Err(IOException::new(
+ format!(
+ "Failed to copy the whole content of \"{}\" to \"{}\" ({} of {} bytes copied).",
+ origin_file, target_file, bytes_copied, bytes_origin
+ ),
+ 0,
+ None,
+ Some(origin_file.to_string()),
+ )
+ .into());
+ }
+ }
+ }
+
+ Ok(())
}
- pub fn mkdir(&self, _dirs: PhpMixed, _mode: u32) -> anyhow::Result<()> {
- todo!()
+ pub fn mkdir(&self, dirs: PhpMixed, mode: u32) -> anyhow::Result<()> {
+ for dir in Self::to_iterable(&dirs) {
+ if shirabe_php_shim::is_dir(&dir) {
+ continue;
+ }
+
+ if !shirabe_php_shim::mkdir(&dir, mode, true) && !shirabe_php_shim::is_dir(&dir) {
+ return Err(IOException::new(
+ format!("Failed to create \"{}\": ", dir),
+ 0,
+ None,
+ Some(dir.clone()),
+ )
+ .into());
+ }
+ }
+ Ok(())
}
- pub fn exists(&self, _files: PhpMixed) -> bool {
- todo!()
+ pub fn exists(&self, files: PhpMixed) -> bool {
+ for file in Self::to_iterable(&files) {
+ if !shirabe_php_shim::file_exists(&file) {
+ return false;
+ }
+ }
+ true
}
pub fn touch(
&self,
- _files: PhpMixed,
- _time: Option<i64>,
- _atime: Option<i64>,
+ files: PhpMixed,
+ time: Option<i64>,
+ atime: Option<i64>,
) -> anyhow::Result<()> {
- todo!()
+ for file in Self::to_iterable(&files) {
+ let ok = match time {
+ // PHP: self::box('touch', $file, $time, $atime)
+ Some(t) => match atime {
+ Some(a) => shirabe_php_shim::touch3(&file, t, a),
+ None => shirabe_php_shim::touch2(&file, t),
+ },
+ None => shirabe_php_shim::touch(&file),
+ };
+ if !ok {
+ return Err(IOException::new(
+ format!("Failed to touch \"{}\": ", file),
+ 0,
+ None,
+ Some(file.clone()),
+ )
+ .into());
+ }
+ }
+ Ok(())
}
- pub fn remove(&self, _files: PhpMixed) -> anyhow::Result<()> {
- todo!()
+ pub fn remove(&self, files: PhpMixed) -> anyhow::Result<()> {
+ let files = Self::to_iterable(&files);
+ Self::do_remove(files, false)
+ }
+
+ fn do_remove(files: Vec<String>, is_recursive: bool) -> anyhow::Result<()> {
+ // PHP reverses the list so that directory contents are removed before the directory itself.
+ let mut files = files;
+ files.reverse();
+ for file in files {
+ if shirabe_php_shim::is_link(&file) {
+ // See https://bugs.php.net/52176
+ if !shirabe_php_shim::unlink(&file) && shirabe_php_shim::file_exists(&file) {
+ return Err(IOException::new(
+ format!("Failed to remove symlink \"{}\": ", file),
+ 0,
+ None,
+ None,
+ )
+ .into());
+ }
+ } else if shirabe_php_shim::is_dir(&file) {
+ let entries = match shirabe_php_shim::recursive_directory_iterator(
+ &file,
+ shirabe_php_shim::FilesystemIterator::KEY_AS_PATHNAME
+ | shirabe_php_shim::SKIP_DOTS,
+ ) {
+ Ok(dir) => shirabe_php_shim::recursive_iterator_iterator(
+ dir,
+ shirabe_php_shim::RecursiveIteratorIterator::CHILD_FIRST,
+ ),
+ Err(_) => {
+ return Err(IOException::new(
+ format!("Failed to remove directory \"{}\": ", file),
+ 0,
+ None,
+ None,
+ )
+ .into());
+ }
+ };
+ let child_paths: Vec<String> =
+ (&entries).into_iter().map(|e| e.get_pathname()).collect();
+ Self::do_remove(child_paths, true)?;
+
+ if !shirabe_php_shim::rmdir(&file) && shirabe_php_shim::file_exists(&file) {
+ return Err(IOException::new(
+ format!("Failed to remove directory \"{}\": ", file),
+ 0,
+ None,
+ None,
+ )
+ .into());
+ }
+ } else if !shirabe_php_shim::unlink(&file) && shirabe_php_shim::file_exists(&file) {
+ return Err(IOException::new(
+ format!("Failed to remove file \"{}\": ", file),
+ 0,
+ None,
+ None,
+ )
+ .into());
+ }
+ }
+ Ok(())
}
pub fn chmod(
&self,
- _files: PhpMixed,
- _mode: u32,
- _umask: u32,
- _recursive: bool,
+ files: PhpMixed,
+ mode: u32,
+ umask: u32,
+ recursive: bool,
) -> anyhow::Result<()> {
- todo!()
+ for file in Self::to_iterable(&files) {
+ if !shirabe_php_shim::chmod(&file, mode & !umask) {
+ return Err(IOException::new(
+ format!("Failed to chmod file \"{}\": ", file),
+ 0,
+ None,
+ Some(file.clone()),
+ )
+ .into());
+ }
+ if recursive && shirabe_php_shim::is_dir(&file) && !shirabe_php_shim::is_link(&file) {
+ let children = Self::dir_children(&file);
+ self.chmod(PhpMixed::List(children), mode, umask, true)?;
+ }
+ }
+ Ok(())
+ }
+
+ // Immediate directory children, matching PHP's new \FilesystemIterator($file) (non-recursive,
+ // skips "." and ".."). Returned as PhpMixed::String list entries for the chmod recursion.
+ fn dir_children(dir: &str) -> Vec<PhpMixed> {
+ match std::fs::read_dir(dir) {
+ Ok(rd) => rd
+ .flatten()
+ .map(|e| PhpMixed::String(e.path().to_string_lossy().into_owned()))
+ .collect(),
+ Err(_) => Vec::new(),
+ }
}
pub fn chown(&self, _files: PhpMixed, _user: PhpMixed, _recursive: bool) -> anyhow::Result<()> {
+ // TODO(phase-d): chown/lchown have no std or existing-shim equivalent (changing a file's
+ // owner needs chown(2), not exposed by std and no libc/syscall crate is available).
todo!()
}
@@ -66,74 +300,483 @@ impl Filesystem {
_group: PhpMixed,
_recursive: bool,
) -> anyhow::Result<()> {
+ // TODO(phase-d): chgrp/lchgrp have no std or existing-shim equivalent (changing a file's
+ // group needs chown(2)/chgrp, not exposed by std and no libc/syscall crate is available).
todo!()
}
- pub fn rename(&self, _origin: &str, _target: &str, _override_file: bool) -> anyhow::Result<()> {
- todo!()
+ pub fn rename(&self, origin: &str, target: &str, override_file: bool) -> anyhow::Result<()> {
+ // we check that target does not exist
+ if !override_file && self.is_readable_instance(target)? {
+ return Err(IOException::new(
+ format!(
+ "Cannot rename because the target \"{}\" already exists.",
+ target
+ ),
+ 0,
+ None,
+ Some(target.to_string()),
+ )
+ .into());
+ }
+
+ if !shirabe_php_shim::rename(origin, target) {
+ if shirabe_php_shim::is_dir(origin) {
+ // See https://bugs.php.net/54097 & https://php.net/rename#113943
+ let mut options: indexmap::IndexMap<String, PhpMixed> = indexmap::IndexMap::new();
+ options.insert("override".to_string(), PhpMixed::Bool(override_file));
+ options.insert("delete".to_string(), PhpMixed::Bool(override_file));
+ self.mirror(origin, target, None, &options)?;
+ self.remove(PhpMixed::String(origin.to_string()))?;
+ return Ok(());
+ }
+ return Err(IOException::new(
+ format!("Cannot rename \"{}\" to \"{}\": ", origin, target),
+ 0,
+ None,
+ Some(target.to_string()),
+ )
+ .into());
+ }
+ Ok(())
+ }
+
+ // Symfony's private isReadable(): like exists() it guards a path-length limit, then defers to
+ // PHP's is_readable().
+ fn is_readable_instance(&self, filename: &str) -> anyhow::Result<bool> {
+ Ok(shirabe_php_shim::is_readable(filename))
}
pub fn symlink(
&self,
- _origin_dir: &str,
- _target_dir: &str,
+ origin_dir: &str,
+ target_dir: &str,
_copy_on_windows: bool,
) -> anyhow::Result<()> {
- todo!()
+ // On Unix DIRECTORY_SEPARATOR is '/', so the Windows-only path normalization and
+ // copy-on-windows branch never run.
+ self.mkdir(
+ PhpMixed::String(shirabe_php_shim::dirname(target_dir)),
+ 0o777,
+ )?;
+
+ if shirabe_php_shim::is_link(target_dir) {
+ if self.read_link(target_dir) == origin_dir {
+ return Ok(());
+ }
+ self.remove(PhpMixed::String(target_dir.to_string()))?;
+ }
+
+ if !shirabe_php_shim::symlink(origin_dir, target_dir) {
+ return Self::link_exception(origin_dir, target_dir, "symbolic");
+ }
+ Ok(())
}
- pub fn hard_link(&self, _origin_file: &str, _target_files: PhpMixed) -> anyhow::Result<()> {
- todo!()
+ pub fn hard_link(&self, origin_file: &str, target_files: PhpMixed) -> anyhow::Result<()> {
+ if !self.exists(PhpMixed::String(origin_file.to_string())) {
+ // FileNotFoundException is not modeled in this crate's exception module; surface the
+ // path-only message as an IOException, matching the FileNotFoundException(null,...) form.
+ return Err(IOException::new(
+ format!("File \"{}\" could not be found.", origin_file),
+ 0,
+ None,
+ Some(origin_file.to_string()),
+ )
+ .into());
+ }
+
+ if !shirabe_php_shim::is_file(origin_file) {
+ return Err(IOException::new(
+ format!("Origin file \"{}\" is not a file.", origin_file),
+ 0,
+ None,
+ None,
+ )
+ .into());
+ }
+
+ for target_file in Self::to_iterable(&target_files) {
+ if shirabe_php_shim::is_file(&target_file) {
+ if Self::fileinode(origin_file) == Self::fileinode(&target_file) {
+ continue;
+ }
+ self.remove(PhpMixed::String(target_file.clone()))?;
+ }
+
+ if std::fs::hard_link(origin_file, &target_file).is_err() {
+ return Self::link_exception(origin_file, &target_file, "hard");
+ }
+ }
+ Ok(())
}
- pub fn read_link(&self, _path: &str) -> String {
- todo!()
+ // PHP fileinode(): the file's inode number, or None on failure.
+ fn fileinode(path: &str) -> Option<u64> {
+ use std::os::unix::fs::MetadataExt;
+ std::fs::metadata(path).ok().map(|m| m.ino())
}
- pub fn make_path_relative(&self, _end_path: &str, _start_path: &str) -> String {
- todo!()
+ fn link_exception(origin: &str, target: &str, link_type: &str) -> anyhow::Result<()> {
+ // The Windows error-code-1314 branch never runs on Unix.
+ Err(IOException::new(
+ format!(
+ "Failed to create \"{}\" link from \"{}\" to \"{}\": ",
+ link_type, origin, target
+ ),
+ 0,
+ None,
+ Some(target.to_string()),
+ )
+ .into())
+ }
+
+ pub fn read_link(&self, path: &str) -> String {
+ // Symfony's readlink() with $canonicalize = false: returns null if the path is not a link.
+ // The Rust signature is non-Option, so the non-link case yields the path's readlink result
+ // (empty string on failure) to keep the symlink() caller working.
+ std::fs::read_link(path)
+ .ok()
+ .map(|p| p.to_string_lossy().into_owned())
+ .unwrap_or_default()
+ }
+
+ // PHP stream_is_local(): true for plain paths and the file:// wrapper, false for remote
+ // wrappers (http://, ftp://, ...). Approximated via the URL scheme since there is no shim.
+ fn stream_is_local(path: &str) -> bool {
+ let scheme = shirabe_php_shim::parse_url(path, shirabe_php_shim::PHP_URL_SCHEME);
+ match scheme {
+ PhpMixed::Null | PhpMixed::Bool(false) => true,
+ PhpMixed::String(s) => s.eq_ignore_ascii_case("file"),
+ _ => true,
+ }
+ }
+
+ pub fn make_path_relative(&self, end_path: &str, start_path: &str) -> String {
+ // PHP throws InvalidArgumentException when either path is not absolute; that exception type
+ // is not modeled here and callers pass absolute paths, so the guards are omitted.
+
+ // On Unix the Windows separator normalization and drive-letter splitting are no-ops.
+ let split_path = |path: &str| -> Vec<String> {
+ let mut result: Vec<String> = Vec::new();
+ for segment in shirabe_php_shim::trim(path, Some("/")).split('/') {
+ if segment == ".." {
+ result.pop();
+ } else if segment != "." && !segment.is_empty() {
+ result.push(segment.to_string());
+ }
+ }
+ result
+ };
+
+ let start_path_arr = split_path(start_path);
+ let end_path_arr = split_path(end_path);
+
+ // Find for which directory the common path stops.
+ let mut index = 0;
+ while index < start_path_arr.len()
+ && index < end_path_arr.len()
+ && start_path_arr[index] == end_path_arr[index]
+ {
+ index += 1;
+ }
+
+ // Determine how deep the start path is relative to the common path.
+ let depth = if start_path_arr.len() == 1 && start_path_arr[0].is_empty() {
+ 0
+ } else {
+ start_path_arr.len() - index
+ };
+
+ // Repeated "../" for each level needed to reach the common path.
+ let traverser = "../".repeat(depth);
+
+ let end_path_remainder = end_path_arr[index..].join("/");
+
+ let relative_path = format!(
+ "{}{}",
+ traverser,
+ if !end_path_remainder.is_empty() {
+ format!("{}/", end_path_remainder)
+ } else {
+ String::new()
+ }
+ );
+
+ if relative_path.is_empty() {
+ "./".to_string()
+ } else {
+ relative_path
+ }
}
pub fn mirror(
&self,
- _origin_dir: &str,
- _target_dir: &str,
- _iterator: Option<PhpMixed>,
- _options: &indexmap::IndexMap<String, PhpMixed>,
+ origin_dir: &str,
+ target_dir: &str,
+ iterator: Option<PhpMixed>,
+ options: &indexmap::IndexMap<String, PhpMixed>,
) -> anyhow::Result<()> {
- todo!()
+ if iterator.is_some() {
+ // TODO(phase-c): Symfony's mirror() accepts a \Traversable filter iterator. The
+ // external-package Filesystem does not model that iterator type, and every caller passes
+ // None, so the filtered case is left unimplemented.
+ todo!()
+ }
+
+ let target_dir = shirabe_php_shim::rtrim(target_dir, Some("/\\"));
+ let origin_dir = shirabe_php_shim::rtrim(origin_dir, Some("/\\"));
+ let origin_dir_len = origin_dir.len();
+
+ if !self.exists(PhpMixed::String(origin_dir.clone())) {
+ return Err(IOException::new(
+ format!(
+ "The origin directory specified \"{}\" was not found.",
+ origin_dir
+ ),
+ 0,
+ None,
+ Some(origin_dir.clone()),
+ )
+ .into());
+ }
+
+ let delete = options
+ .get("delete")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+
+ // Iterate in destination folder to remove obsolete entries.
+ if self.exists(PhpMixed::String(target_dir.clone())) && delete {
+ let target_dir_len = target_dir.len();
+ if let Ok(dir) = shirabe_php_shim::recursive_directory_iterator(
+ &target_dir,
+ shirabe_php_shim::SKIP_DOTS,
+ ) {
+ let delete_iterator = shirabe_php_shim::recursive_iterator_iterator(
+ dir,
+ shirabe_php_shim::RecursiveIteratorIterator::CHILD_FIRST,
+ );
+ for file in &delete_iterator {
+ let pathname = file.get_pathname();
+ let origin = format!("{}{}", origin_dir, &pathname[target_dir_len..]);
+ if !self.exists(PhpMixed::String(origin)) {
+ self.remove(PhpMixed::String(pathname))?;
+ }
+ }
+ }
+ }
+
+ let copy_on_windows = options
+ .get("copy_on_windows")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+
+ let flags = if copy_on_windows {
+ shirabe_php_shim::SKIP_DOTS
+ | shirabe_php_shim::RecursiveDirectoryIterator::FOLLOW_SYMLINKS
+ } else {
+ shirabe_php_shim::SKIP_DOTS
+ };
+ let dir = shirabe_php_shim::recursive_directory_iterator(&origin_dir, flags)
+ .map_err(|e| anyhow::anyhow!("{}", e.message))?;
+ let iterator = shirabe_php_shim::recursive_iterator_iterator(
+ dir,
+ shirabe_php_shim::RecursiveIteratorIterator::SELF_FIRST,
+ );
+
+ self.mkdir(PhpMixed::String(target_dir.clone()), 0o777)?;
+
+ let override_file = options
+ .get("override")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+
+ for file in &iterator {
+ let pathname = file.get_pathname();
+ if pathname == target_dir {
+ continue;
+ }
+
+ let target = format!("{}{}", target_dir, &pathname[origin_dir_len..]);
+
+ if !copy_on_windows && file.is_link() {
+ self.symlink(&self.read_link(&pathname), &target, false)?;
+ } else if file.is_dir() {
+ self.mkdir(PhpMixed::String(target), 0o777)?;
+ } else if file.is_file() {
+ self.copy(&pathname, &target, override_file)?;
+ } else {
+ return Err(IOException::new(
+ format!("Unable to guess \"{}\" file type.", pathname),
+ 0,
+ None,
+ Some(pathname.clone()),
+ )
+ .into());
+ }
+ }
+ Ok(())
}
- pub fn is_absolute_path(&self, _file: &str) -> bool {
- todo!()
+ pub fn is_absolute_path(&self, file: &str) -> bool {
+ if file.is_empty() {
+ return false;
+ }
+ let bytes = file.as_bytes();
+ // strspn($file, '/\\', 0, 1): the first character is a forward or back slash.
+ let leading_slash = matches!(bytes[0], b'/' | b'\\');
+ // Windows drive letter form: "C:\" / "C:/".
+ let drive_letter = file.len() > 3
+ && bytes[0].is_ascii_alphabetic()
+ && bytes[1] == b':'
+ && matches!(bytes[2], b'/' | b'\\');
+ // null !== parse_url($file, PHP_URL_SCHEME): the path has a URL scheme.
+ let scheme = shirabe_php_shim::parse_url(file, shirabe_php_shim::PHP_URL_SCHEME);
+ let has_scheme = !matches!(scheme, PhpMixed::Null | PhpMixed::Bool(false));
+ leading_slash || drive_letter || has_scheme
}
- pub fn dump_file(&self, _filename: &str, _content: &str) -> anyhow::Result<()> {
- todo!()
+ pub fn dump_file(&self, filename: &str, content: &str) -> anyhow::Result<()> {
+ let dir = shirabe_php_shim::dirname(filename);
+
+ if shirabe_php_shim::is_link(filename) {
+ let link_target = self.read_link(filename);
+ if !link_target.is_empty() {
+ // Path::makeAbsolute resolves the link target against the file's directory; the
+ // Symfony Path helper is not ported, so an absolute target is used as-is and a
+ // relative one is joined to the directory.
+ let resolved = if self.is_absolute_path(&link_target) {
+ link_target
+ } else {
+ format!("{}/{}", dir, link_target)
+ };
+ return self.dump_file(&resolved, content);
+ }
+ }
+
+ if !shirabe_php_shim::is_dir(&dir) {
+ self.mkdir(PhpMixed::String(dir.clone()), 0o777)?;
+ }
+
+ // Creates a temp file with 0600 access rights when the filesystem supports chmod.
+ let tmp_file = self.temp_nam(&dir, &shirabe_php_shim::basename(filename))?;
+
+ let result: anyhow::Result<()> = (|| {
+ if shirabe_php_shim::file_put_contents(&tmp_file, content.as_bytes()).is_none() {
+ return Err(IOException::new(
+ format!("Failed to write file \"{}\": ", filename),
+ 0,
+ None,
+ Some(filename.to_string()),
+ )
+ .into());
+ }
+
+ let perms = shirabe_php_shim::fileperms(filename);
+ let mode = if perms != 0 {
+ perms as u32
+ } else {
+ 0o666 & !shirabe_php_shim::umask()
+ };
+ shirabe_php_shim::chmod(&tmp_file, mode);
+
+ self.rename(&tmp_file, filename, true)
+ })();
+
+ // finally: clean up the temp file if it still exists.
+ if shirabe_php_shim::file_exists(&tmp_file) {
+ shirabe_php_shim::unlink(&tmp_file);
+ }
+
+ result
}
- pub fn append_to_file(&self, _filename: &str, _content: &str) -> anyhow::Result<()> {
- todo!()
+ pub fn append_to_file(&self, filename: &str, content: &str) -> anyhow::Result<()> {
+ let dir = shirabe_php_shim::dirname(filename);
+
+ if !shirabe_php_shim::is_dir(&dir) {
+ self.mkdir(PhpMixed::String(dir), 0o777)?;
+ }
+
+ if shirabe_php_shim::file_put_contents3(filename, content, shirabe_php_shim::FILE_APPEND)
+ .is_none()
+ {
+ return Err(IOException::new(
+ format!("Failed to write file \"{}\": ", filename),
+ 0,
+ None,
+ Some(filename.to_string()),
+ )
+ .into());
+ }
+ Ok(())
}
- pub fn temp_nam(&self, _dir: &str, _prefix: &str) -> anyhow::Result<String> {
- todo!()
+ pub fn temp_nam(&self, dir: &str, prefix: &str) -> anyhow::Result<String> {
+ // getSchemeAndHierarchy(): with no "scheme://" the scheme is null and the hierarchy is the
+ // whole path, so the local-filesystem branch (empty suffix) always applies here.
+ let (scheme, hierarchy) = Self::get_scheme_and_hierarchy(dir);
+
+ if scheme.is_none() || scheme.as_deref() == Some("file") || scheme.as_deref() == Some("gs")
+ {
+ if let Some(tmp_file) = shirabe_php_shim::tempnam(&hierarchy, prefix) {
+ if let Some(scheme) = &scheme {
+ if scheme != "gs" {
+ return Ok(format!("{}://{}", scheme, tmp_file));
+ }
+ }
+ return Ok(tmp_file);
+ }
+
+ return Err(IOException::new(
+ "A temporary file could not be created: ".to_string(),
+ 0,
+ None,
+ None,
+ )
+ .into());
+ }
+
+ Err(IOException::new(
+ "A temporary file could not be created: ".to_string(),
+ 0,
+ None,
+ None,
+ )
+ .into())
}
- // Static-style helper methods used in the ported codebase
+ // Gets a (scheme, hierarchy) tuple of a filename (e.g. file:///tmp -> (Some("file"), "/tmp")).
+ fn get_scheme_and_hierarchy(filename: &str) -> (Option<String>, String) {
+ match filename.split_once("://") {
+ Some((scheme, hierarchy)) => (Some(scheme.to_string()), hierarchy.to_string()),
+ None => (None, filename.to_string()),
+ }
+ }
+
+ // The following methods belong to Composer's own Composer\Util\Filesystem, not Symfony's
+ // Filesystem. They were copied into this stub by mistake; every caller resolves to the ported
+ // Composer\Util\Filesystem in crates/shirabe/src/util/filesystem.rs instead. They have no
+ // Symfony semantics, so they are left unimplemented here.
+
pub fn is_readable(_path: &str) -> bool {
+ // TODO(phase-d): not a Symfony Filesystem method; see note above.
todo!()
}
pub fn is_local_path(_path: &str) -> bool {
+ // TODO(phase-d): not a Symfony Filesystem method; see note above.
todo!()
}
pub fn trim_trailing_slash(_path: &str) -> String {
+ // TODO(phase-d): not a Symfony Filesystem method; see note above.
todo!()
}
pub fn get_platform_path(_path: &str) -> String {
+ // TODO(phase-d): not a Symfony Filesystem method; see note above.
todo!()
}
}