aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-external-packages/src')
-rw-r--r--crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs70
1 files changed, 62 insertions, 8 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs b/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs
index 9b449b76..d773e60c 100644
--- a/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs
+++ b/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs
@@ -1,5 +1,10 @@
//! ref: composer/vendor/symfony/filesystem/Filesystem.php
+// TODO(phase-c): PHP's box()/self::$lastError mechanism (captures the underlying OS error message
+// from a failed native call via set_error_handler) is not modeled anywhere in this file. Every
+// IOException message constructed below therefore omits the trailing low-level error string that
+// PHP would append (e.g. "Failed to touch \"%s\": ".self::$lastError).
+
use crate::symfony::filesystem::exception::io_exception::IOException;
use shirabe_php_shim::PhpMixed;
@@ -77,6 +82,10 @@ impl Filesystem {
let bytes_copied = match shirabe_php_shim::copy(origin_file, target_file) {
true => shirabe_php_shim::filesize(target_file).unwrap_or(0),
false => {
+ // TODO(phase-c): PHP distinguishes fopen($originFile) failure ("source file
+ // could not be opened for reading") from fopen($targetFile) failure ("target
+ // file could not be opened for writing"); the shim's copy() collapses both
+ // (and the actual copy failure) into this single generic message.
return Err(IOException::new(
format!("Failed to copy \"{}\" to \"{}\".", origin_file, target_file),
0,
@@ -106,8 +115,9 @@ impl Filesystem {
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.
+ // TODO(phase-c): PHP preserves the origin file's mtime via
+ // touch($targetFile, filemtime($originFile)). shirabe_php_shim::touch2 now exists
+ // and could implement this, but it is not wired up here yet.
let bytes_origin = shirabe_php_shim::filesize(origin_file).unwrap_or(0);
if bytes_copied != bytes_origin {
@@ -148,6 +158,9 @@ impl Filesystem {
}
fn exists(&self, files: PhpMixed) -> bool {
+ // TODO(phase-c): PHP throws IOException when a path exceeds PHP_MAXPATHLEN - 2 characters;
+ // this port has no such guard, and the plain `bool` return type here cannot express that
+ // throw path without changing the signature.
for file in Self::to_iterable(&files) {
if !shirabe_php_shim::file_exists(&file) {
return false;
@@ -161,6 +174,11 @@ impl Filesystem {
Self::do_remove(files, false)
}
+ // TODO(phase-c): `is_recursive` is unused. In PHP, doRemove() uses it as a top-level-call guard:
+ // on the first (non-recursive) call for a directory, it renames the directory to a random
+ // hidden name before recursing into it (and renames it back if rmdir subsequently fails), to
+ // avoid a race where another process recreates the path mid-removal. That rename/rollback
+ // trick is entirely unported here; this version always operates on the original path.
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;
@@ -168,6 +186,12 @@ impl Filesystem {
for file in files {
if shirabe_php_shim::is_link(&file) {
// See https://bugs.php.net/52176
+ // TODO(phase-c): PHP's condition is
+ // `!(unlink() || '\\' !== DIRECTORY_SEPARATOR || rmdir()) && file_exists()`. On
+ // Unix, `'\\' !== DIRECTORY_SEPARATOR` is always true, so the `!(...)` is always
+ // false and this branch never throws there regardless of unlink's result (the
+ // rmdir fallback and this exception only matter on Windows). This port omits that
+ // always-true disjunct, so it CAN throw here on Unix where upstream never would.
if !shirabe_php_shim::unlink(&file) && shirabe_php_shim::file_exists(&file) {
return Err(IOException::new(
format!("Failed to remove symlink \"{}\": ", file),
@@ -211,6 +235,10 @@ impl Filesystem {
.into());
}
} else if !shirabe_php_shim::unlink(&file) && shirabe_php_shim::file_exists(&file) {
+ // TODO(phase-c): PHP also throws when self::$lastError contains "Permission
+ // denied", even if file_exists() is now false (e.g. the file vanished between the
+ // failed unlink and this check). That OR-branch is dropped along with the general
+ // $lastError omission noted at the top of this file.
return Err(IOException::new(
format!("Failed to remove file \"{}\": ", file),
0,
@@ -223,14 +251,16 @@ impl Filesystem {
Ok(())
}
+ // TODO(phase-c): PHP's symlink() has a `'\\' === DIRECTORY_SEPARATOR` branch that (a)
+ // normalizes '/' to '\\' in both paths, and (b) when $copyOnWindows is true, mirrors the
+ // directory instead of symlinking it and returns early. Neither is ported: `_copy_on_windows`
+ // is accepted but unused, so this always symlinks even where PHP would have copied.
pub fn symlink(
&self,
origin_dir: &str,
target_dir: &str,
_copy_on_windows: bool,
) -> anyhow::Result<()> {
- // 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,
@@ -249,8 +279,11 @@ impl Filesystem {
Ok(())
}
+ // TODO(phase-c): PHP special-cases a Windows error containing "error code(1314)" with a
+ // distinct "Do you have the required Administrator-rights?" message; that check (and the
+ // self::$lastError inspection it depends on) is not ported, so this always throws the generic
+ // message below.
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 \"{}\": ",
@@ -263,10 +296,20 @@ impl Filesystem {
.into())
}
+ // TODO(phase-c): this only ports Symfony's readlink($path, $canonicalize = false) overload;
+ // the $canonicalize = true branch (realpath()-based resolution, returning null if the path
+ // does not exist at all) is entirely unported. Per the phase B default-argument convention
+ // this should become a `read_link2` overload if that branch is ever needed.
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.
+ // TODO(phase-c): the Rust signature is non-Option, so the non-link case yields the path's
+ // readlink result (empty string on failure) instead of PHP's null, to keep the symlink()
+ // caller working. Every current caller checks is_link() first, so this never triggers on
+ // the live code paths, but the collapsed Option<String> -> String signature is a real
+ // divergence from upstream.
+ // TODO(phase-c): PHP also has `if ('\\' === DIRECTORY_SEPARATOR && PHP_VERSION_ID < 70400)
+ // return realpath($path);` ahead of the plain readlink() call below, working around a
+ // pre-7.4 Windows bug. Not ported here; on old Windows PHP this would resolve differently.
std::fs::read_link(path)
.ok()
.map(|p| p.to_string_lossy().into_owned())
@@ -274,7 +317,11 @@ impl Filesystem {
}
// 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.
+ // wrappers (http://, ftp://, ...).
+ // TODO(phase-c): this is PHP's built-in stream_is_local(), which queries the registered stream
+ // wrapper for STREAM_IS_URL rather than just parsing the scheme. It is approximated here via
+ // parse_url()'s scheme instead of being added to shirabe-php-shim, so a registered custom
+ // stream wrapper claiming to be local (or vice versa) would be classified differently than PHP.
fn stream_is_local(path: &str) -> bool {
let scheme = shirabe_php_shim::parse_url(path, shirabe_php_shim::PHP_URL_SCHEME);
match scheme {
@@ -366,6 +413,13 @@ impl Filesystem {
.and_then(|v| v.as_bool())
.unwrap_or(false);
+ // TODO(phase-c): PHP's skip condition is
+ // `$file->getPathname() === $targetDir || $file->getRealPath() === $targetDir ||
+ // isset($filesCreatedWhileMirroring[$file->getRealPath()])`, and it records every
+ // `$target` it creates into `$filesCreatedWhileMirroring` to avoid revisiting a path
+ // already produced earlier in this same mirror() call (e.g. via a symlink loop back into
+ // the tree). Only the plain pathname comparison is ported; the getRealPath() comparison
+ // and the created-files dedup set are both omitted.
for file in &iterator {
let pathname = file.get_pathname();
if pathname == target_dir {