From 7484666757ce20426731b8ba4917aa0fc9e9d904 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 28 Mar 2025 03:27:37 +0100 Subject: [PATCH 01/60] Temporarily deprecate `alias this` of `DirEntry` --- std/file.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/std/file.d b/std/file.d index a5b53270f95..ecf18d263a1 100644 --- a/std/file.d +++ b/std/file.d @@ -3809,7 +3809,7 @@ else version (Windows) { @safe: public: - alias name this; + deprecated("0xEAB") alias name this; // TODO: undo temporary deprecation this(return scope string path) { @@ -3918,7 +3918,7 @@ else version (Posix) { @safe: public: - alias name this; + deprecated("0xEAB") alias name this; // TODO: undo temporary deprecation this(return scope string path) { From f7bd08520a46148fa082922e346379fbdf0677a3 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 28 Mar 2025 03:33:40 +0100 Subject: [PATCH 02/60] Add unittest --- std/file.d | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/std/file.d b/std/file.d index ecf18d263a1..37bf436833e 100644 --- a/std/file.d +++ b/std/file.d @@ -126,6 +126,73 @@ else version (Posix) else static assert(0); +@safe unittest +{ + import std.path : absolutePath, buildPath; + + string root = deleteme(); + mkdirRecurse(root); + scope (exit) rmdirRecurse(root); + + mkdirRecurse(root.buildPath("1", "2")); + mkdirRecurse(root.buildPath("3", "4")); + mkdirRecurse(root.buildPath("3", "5", "6")); + + const origWD = getcwd(); + + // Directory tree traversal test + { + chdir(root); + scope(exit) chdir(origWD); + + static void test(SpanMode spanMode, out int[7] found) @safe + { + found[] = 0; + foreach(DirEntry entry; ".".dirEntries(spanMode)) + { + enum switchCase(char c, char idx) = `case '` ~ c ~ `': ++found [` ~ idx ~ `]; break;`; + switch (entry.name[$-1]) + { + mixin(switchCase!('1', '0')); + mixin(switchCase!('2', '1')); + mixin(switchCase!('3', '2')); + mixin(switchCase!('4', '3')); + mixin(switchCase!('5', '4')); + mixin(switchCase!('6', '5')); + default: + assert(false, "Unexpected directory entry: " ~ entry.name); + } + } + } + + int[7] found; + + test(SpanMode.depth, found); + assert(found[0] == 1); + assert(found[1] == 1); + assert(found[2] == 1); + assert(found[3] == 1); + assert(found[4] == 1); + assert(found[5] == 1); + + test(SpanMode.breadth, found); + assert(found[0] == 1); + assert(found[1] == 1); + assert(found[2] == 1); + assert(found[3] == 1); + assert(found[4] == 1); + assert(found[5] == 1); + + test(SpanMode.shallow, found); + assert(found[0] == 1); + assert(found[1] == 0); + assert(found[2] == 1); + assert(found[3] == 0); + assert(found[4] == 0); + assert(found[5] == 0); + } +} + // Purposefully not documented. Use at your own risk @property string deleteme() @safe { From b2a9d95c1f784c2d168ca58ee0749330e54b185c Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 28 Mar 2025 05:13:57 +0100 Subject: [PATCH 03/60] Implement `DirEntry` name absolutization for Windows targets --- std/file.d | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 37bf436833e..cfcc5e130de 100644 --- a/std/file.d +++ b/std/file.d @@ -3886,6 +3886,7 @@ else version (Windows) throw new FileException(path, "File does not exist"); _name = path; + this.absolutizeName(); with (getFileAttributesWin(path)) { @@ -3915,11 +3916,35 @@ else version (Windows) _attributes = fd.dwFileAttributes; } - @property string name() const pure nothrow return scope + private void absolutizeName() + { + import std.path : absolutePath, isAbsolute; + + if (_name.isAbsolute) + return; + + const rel = _name; + const abs = rel.absolutePath; + const idx = abs.length - rel.length; + + if (idx == 0) + return; // Keep prefix `null`. + + _absolutePrefix = abs[0 .. idx]; + _name = abs; + } + + package @property string absoluteName() const pure nothrow return scope { return _name; } + @property string name() const pure nothrow return scope + { + import std.string : chompPrefix; + return _name.chompPrefix(_absolutePrefix); + } + @property bool isDir() const pure nothrow scope { return (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; @@ -3970,6 +3995,7 @@ else version (Windows) private: string _name; /// The file or directory represented by this DirEntry. + string _absolutePrefix; /// Optional absolute directory path that has been prepended to `_name`. SysTime _timeCreated; /// The time when the file was created. SysTime _timeLastAccessed; /// The time when the file was last accessed. From 16b587e337f297f1e6d32124ac7cee35d1eb16d3 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 28 Mar 2025 05:19:14 +0100 Subject: [PATCH 04/60] Add `DirEntry`-related traits --- std/file.d | 3 +++ 1 file changed, 3 insertions(+) diff --git a/std/file.d b/std/file.d index cfcc5e130de..6404ae13783 100644 --- a/std/file.d +++ b/std/file.d @@ -126,6 +126,9 @@ else version (Posix) else static assert(0); +private enum isConvertibleToStringButNoDirEntry(T) = !is(T == DirEntry) && isConvertibleToString!T; +private enum isDirEntry(T) = is(T == DirEntry); + @safe unittest { import std.path : absolutePath, buildPath; From a5b53935fe9c05e75713055a5367f2814cafebc3 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 28 Mar 2025 05:37:27 +0100 Subject: [PATCH 05/60] Convert tabs to spaces --- std/file.d | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/std/file.d b/std/file.d index 6404ae13783..c04af486320 100644 --- a/std/file.d +++ b/std/file.d @@ -131,15 +131,15 @@ private enum isDirEntry(T) = is(T == DirEntry); @safe unittest { - import std.path : absolutePath, buildPath; + import std.path : absolutePath, buildPath; - string root = deleteme(); - mkdirRecurse(root); - scope (exit) rmdirRecurse(root); + string root = deleteme(); + mkdirRecurse(root); + scope (exit) rmdirRecurse(parent); - mkdirRecurse(root.buildPath("1", "2")); - mkdirRecurse(root.buildPath("3", "4")); - mkdirRecurse(root.buildPath("3", "5", "6")); + mkdirRecurse(root.buildPath("1", "2")); + mkdirRecurse(root.buildPath("3", "4")); + mkdirRecurse(root.buildPath("3", "5", "6")); const origWD = getcwd(); From e0ba58e077604db45126ef96204f775fe8ba5889 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 28 Mar 2025 05:38:13 +0100 Subject: [PATCH 06/60] Add nirvana dir to test --- std/file.d | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index c04af486320..f84f61d7202 100644 --- a/std/file.d +++ b/std/file.d @@ -133,10 +133,14 @@ private enum isDirEntry(T) = is(T == DirEntry); { import std.path : absolutePath, buildPath; - string root = deleteme(); + string parent = deleteme().absolutePath; + string root = parent.buildPath("r"); mkdirRecurse(root); scope (exit) rmdirRecurse(parent); + string nirvana = parent.buildPath("nirvana"); + mkdir(nirvana); + mkdirRecurse(root.buildPath("1", "2")); mkdirRecurse(root.buildPath("3", "4")); mkdirRecurse(root.buildPath("3", "5", "6")); From 52a9e73623f0d6a9ae8082e33308e3b7c225e07b Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 28 Mar 2025 05:38:38 +0100 Subject: [PATCH 07/60] Port `exists()` --- std/file.d | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index f84f61d7202..717d7308831 100644 --- a/std/file.d +++ b/std/file.d @@ -147,6 +147,17 @@ private enum isDirEntry(T) = is(T == DirEntry); const origWD = getcwd(); + // DirEntry existance test + { + chdir(root); + scope(exit) chdir(origWD); + + auto entry = ".".dirEntries(SpanMode.shallow).front; + assert(entry.exists); + chdir(nirvana); + assert(entry.exists); + } + // Directory tree traversal test { chdir(root); @@ -157,6 +168,8 @@ private enum isDirEntry(T) = is(T == DirEntry); found[] = 0; foreach(DirEntry entry; ".".dirEntries(spanMode)) { + assert(entry.exists); + enum switchCase(char c, char idx) = `case '` ~ c ~ `': ++found [` ~ idx ~ `]; break;`; switch (entry.name[$-1]) { @@ -2013,11 +2026,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto bool exists(R)(auto ref R name) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return exists!(StringTypeOf!R)(name); } +/// ditto +bool exists(R)(auto ref const R name) +if (isDirEntry!R) +{ + version (Windows) + return exists(name.absoluteName); + version (Posix) + return exists(name.name); +} + /// @safe unittest { From cc14345a1785c7573e0d626f53a17629320a57b4 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 28 Mar 2025 05:42:41 +0100 Subject: [PATCH 08/60] Mark new unittest Windows-only --- std/file.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 717d7308831..0f7c925adcc 100644 --- a/std/file.d +++ b/std/file.d @@ -129,7 +129,7 @@ else private enum isConvertibleToStringButNoDirEntry(T) = !is(T == DirEntry) && isConvertibleToString!T; private enum isDirEntry(T) = is(T == DirEntry); -@safe unittest +version (Windows) @safe unittest { import std.path : absolutePath, buildPath; From 29380303737c68c24c8010a3f630444ba7b6b6f0 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 28 Mar 2025 06:25:29 +0100 Subject: [PATCH 09/60] Season attribute soup of `absolutizeName` --- std/file.d | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/std/file.d b/std/file.d index 0f7c925adcc..c2e21bf4e0a 100644 --- a/std/file.d +++ b/std/file.d @@ -3946,15 +3946,13 @@ else version (Windows) _attributes = fd.dwFileAttributes; } - private void absolutizeName() + private void absolutizeName() pure return scope { - import std.path : absolutePath, isAbsolute; - - if (_name.isAbsolute) - return; + import std.path : absolutePath; const rel = _name; - const abs = rel.absolutePath; + alias abs = _name; + _name = _name.absolutePath; const idx = abs.length - rel.length; if (idx == 0) From 78a4b708def08029822e4d6bace98b19ac464213 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 28 Mar 2025 06:34:18 +0100 Subject: [PATCH 10/60] Fix internal `DirEntry` ctor --- std/file.d | 2 ++ 1 file changed, 2 insertions(+) diff --git a/std/file.d b/std/file.d index c2e21bf4e0a..b1a77660f07 100644 --- a/std/file.d +++ b/std/file.d @@ -3944,6 +3944,8 @@ else version (Windows) _timeLastAccessed = FILETIMEToSysTime(&fd.ftLastAccessTime); _timeLastModified = FILETIMEToSysTime(&fd.ftLastWriteTime); _attributes = fd.dwFileAttributes; + + this.absolutizeName(); } private void absolutizeName() pure return scope From 1c9af995e493d0cd3572ea62c5196c89ed3e2a7b Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 03:08:53 +0200 Subject: [PATCH 11/60] Port `rename()` --- std/file.d | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index b1a77660f07..062d210d9ae 100644 --- a/std/file.d +++ b/std/file.d @@ -211,6 +211,50 @@ version (Windows) @safe unittest assert(found[4] == 0); assert(found[5] == 0); } + + // Renaming test + { + chdir(root); + scope(exit) chdir(origWD); + + { + rename("1", "x"); + assert(!"1".exists); + assert( "x".exists); + + rename("x", "1"); + assert( "1".exists); + assert(!"x".exists); + } + { + auto de1 = DirEntry("1"); + rename(de1, "x"); + assert(!"1".exists); + assert( "x".exists); + + auto deX = DirEntry("x"); + rename(deX, "1"); + assert( "1".exists); + assert(!"x".exists); + } + { + auto de1 = DirEntry("1"); + chdir(nirvana); + rename(de1, root.buildPath("x")); + + chdir(root); + assert(!"1".exists); + assert( "x".exists); + + auto deX = DirEntry("x"); + chdir(nirvana); + rename(deX, root.buildPath("1")); + + chdir(root); + assert( "1".exists); + assert(!"x".exists); + } + } } // Purposefully not documented. Use at your own risk @@ -1025,13 +1069,23 @@ if ((isSomeFiniteCharInputRange!RF || isSomeString!RF) && !isConvertibleToString /// ditto void rename(RF, RT)(auto ref RF from, auto ref RT to) -if (isConvertibleToString!RF || isConvertibleToString!RT) +if ((isConvertibleToString!RF || isConvertibleToString!RT) && !isDirEntry!RF) { import std.meta : staticMap; alias Types = staticMap!(convertToString, RF, RT); rename!Types(from, to); } +/// ditto +void rename(RF, RT)(auto ref RF from, auto ref RT to) +if ((isConvertibleToString!RF || isConvertibleToString!RT) && isDirEntry!RF) +{ + version (Windows) + return rename(from.absoluteName, to); + else + return rename(from.name, to); +} + @safe unittest { static assert(__traits(compiles, rename(TestAliasedString(null), TestAliasedString(null)))); From a1621958e28504c93bb433049b24085f714ae5dc Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 03:45:06 +0200 Subject: [PATCH 12/60] Port `remove()` --- std/file.d | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 062d210d9ae..42d59b8381d 100644 --- a/std/file.d +++ b/std/file.d @@ -255,6 +255,23 @@ version (Windows) @safe unittest assert(!"x".exists); } } + + // Removal test + { + chdir(root); + scope(exit) chdir(origWD); + + const string file = "1/2/test.txt"; + write(file, "…"); + auto entry = DirEntry(file); + assert(file.exists); + + chdir(nirvana); + remove(entry); + + chdir(root); + assert(!file.exists); + } } // Purposefully not documented. Use at your own risk @@ -1177,11 +1194,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto void remove(R)(auto ref R name) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { remove!(StringTypeOf!R)(name); } +/// ditto +void remove(R)(auto ref R name) +if (isDirEntry!R) +{ + version (Windows) + return remove(name.absoluteName); + else + return remove(name.name); +} + /// @safe unittest { From f6594185381883f3f51f31541f53a8928631ad1e Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 03:59:03 +0200 Subject: [PATCH 13/60] Refactor unittest to be more comprehensible --- std/file.d | 70 +++++++++++++++++++++++++----------------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/std/file.d b/std/file.d index 42d59b8381d..de876bad598 100644 --- a/std/file.d +++ b/std/file.d @@ -133,6 +133,15 @@ version (Windows) @safe unittest { import std.path : absolutePath, buildPath; + static void runIn(string dir, void delegate() @safe callback) + { + const origWD = getcwd(); + chdir(dir); + scope(exit) chdir(origWD); + + callback(); + } + string parent = deleteme().absolutePath; string root = parent.buildPath("r"); mkdirRecurse(root); @@ -145,24 +154,17 @@ version (Windows) @safe unittest mkdirRecurse(root.buildPath("3", "4")); mkdirRecurse(root.buildPath("3", "5", "6")); - const origWD = getcwd(); - // DirEntry existance test - { - chdir(root); - scope(exit) chdir(origWD); - + runIn(root, { auto entry = ".".dirEntries(SpanMode.shallow).front; assert(entry.exists); - chdir(nirvana); - assert(entry.exists); - } + runIn(nirvana, () { + assert(entry.exists); + }); + }); // Directory tree traversal test - { - chdir(root); - scope(exit) chdir(origWD); - + runIn(root, { static void test(SpanMode spanMode, out int[7] found) @safe { found[] = 0; @@ -210,13 +212,11 @@ version (Windows) @safe unittest assert(found[3] == 0); assert(found[4] == 0); assert(found[5] == 0); - } - - // Renaming test - { - chdir(root); - scope(exit) chdir(origWD); + }); + // Renaming tests + runIn(root, { + // string-based renaming { rename("1", "x"); assert(!"1".exists); @@ -226,6 +226,7 @@ version (Windows) @safe unittest assert( "1".exists); assert(!"x".exists); } + // regular DirEntry renaming { auto de1 = DirEntry("1"); rename(de1, "x"); @@ -237,41 +238,36 @@ version (Windows) @safe unittest assert( "1".exists); assert(!"x".exists); } + // DirEntry renaming with `chdir()` { auto de1 = DirEntry("1"); - chdir(nirvana); - rename(de1, root.buildPath("x")); - - chdir(root); + runIn(nirvana, { + rename(de1, root.buildPath("x")); + }); assert(!"1".exists); assert( "x".exists); auto deX = DirEntry("x"); - chdir(nirvana); - rename(deX, root.buildPath("1")); - - chdir(root); + runIn(nirvana, { + rename(deX, root.buildPath("1")); + }); assert( "1".exists); assert(!"x".exists); } - } + }); // Removal test - { - chdir(root); - scope(exit) chdir(origWD); - + runIn(root, { const string file = "1/2/test.txt"; write(file, "…"); auto entry = DirEntry(file); assert(file.exists); - chdir(nirvana); - remove(entry); - - chdir(root); + runIn(nirvana, { + remove(entry); + }); assert(!file.exists); - } + }); } // Purposefully not documented. Use at your own risk From 7405027f1c43004ec4abfa8e012fa1ecc37f9ddf Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 04:07:46 +0200 Subject: [PATCH 14/60] Port `getSize()` --- std/file.d | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index de876bad598..1c510687c8a 100644 --- a/std/file.d +++ b/std/file.d @@ -268,6 +268,21 @@ version (Windows) @safe unittest }); assert(!file.exists); }); + + // File-size querying test + runIn(root, { + const string file = "1/2/test.txt"; + static immutable data = cast(immutable(ubyte)[]) "foobar"; + write(file, data); + + auto entry = DirEntry(file); + runIn(nirvana, { + assert(getSize(entry) == data.length); + }); + + remove(file); + assert(!file.exists); + }); } // Purposefully not documented. Use at your own risk @@ -1348,11 +1363,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto ulong getSize(R)(auto ref R name) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return getSize!(StringTypeOf!R)(name); } +/// ditto +ulong getSize(R)(auto ref R name) +if (isDirEntry!R) +{ + version (Windows) + return getSize(name.absoluteName); + else + return getSize(name.name); +} + @safe unittest { static assert(__traits(compiles, getSize(TestAliasedString("foo")))); From d6d971a3d1c995580db59a1cd8081ed54141de61 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 04:13:35 +0200 Subject: [PATCH 15/60] Harden file existence tests --- std/file.d | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/std/file.d b/std/file.d index 1c510687c8a..15054ec4e08 100644 --- a/std/file.d +++ b/std/file.d @@ -157,9 +157,9 @@ version (Windows) @safe unittest // DirEntry existance test runIn(root, { auto entry = ".".dirEntries(SpanMode.shallow).front; - assert(entry.exists); + assert(exists(entry)); runIn(nirvana, () { - assert(entry.exists); + assert(exists(entry)); }); }); @@ -264,7 +264,9 @@ version (Windows) @safe unittest assert(file.exists); runIn(nirvana, { + assert(entry.exists); remove(entry); + assert(!entry.exists); }); assert(!file.exists); }); From 9f533b39fd0add11cd264c868bf00e3f2e3b6016 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 04:48:58 +0200 Subject: [PATCH 16/60] Port `getTimes()` --- std/file.d | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 15054ec4e08..74e6d758746 100644 --- a/std/file.d +++ b/std/file.d @@ -285,6 +285,42 @@ version (Windows) @safe unittest remove(file); assert(!file.exists); }); + + // File-time querying test + runIn(root, { + import std.datetime : Clock; + + auto now = Clock.currTime(); + const string file = "1/2/test.txt"; + write(file, "…"); + + auto entry = DirEntry(file); + runIn(nirvana, { + assert(!file.exists); + SysTime accessTime, modificationTime; + getTimes(entry, accessTime, modificationTime); + + if (accessTime < now) + { + import std.stdio : stderr; + () @trusted + { + stderr.writeln("Unexpected access time; probably caused by time-sync or filesystem."); + }(); + } + if (modificationTime < now) + { + import std.stdio : stderr; + () @trusted + { + stderr.writeln("Unexpected modification time; probably caused by time-sync or filesystem."); + }(); + } + }); + + remove(file); + assert(!file.exists); + }); } // Purposefully not documented. Use at your own risk @@ -1480,11 +1516,23 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) void getTimes(R)(auto ref R name, out SysTime accessTime, out SysTime modificationTime) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return getTimes!(StringTypeOf!R)(name, accessTime, modificationTime); } +/// ditto +void getTimes(R)(auto ref R name, + out SysTime accessTime, + out SysTime modificationTime) +if (isDirEntry!R) +{ + version (Windows) + return getTimes(name.absoluteName, accessTime, modificationTime); + else + return getTimes(name.name, accessTime, modificationTime); +} + /// @safe unittest { From aec92e0eb088c3fae287595206d264c8da0c00bf Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 04:49:28 +0200 Subject: [PATCH 17/60] Add yet another file existence test --- std/file.d | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/std/file.d b/std/file.d index 74e6d758746..822da6f801f 100644 --- a/std/file.d +++ b/std/file.d @@ -161,6 +161,13 @@ version (Windows) @safe unittest runIn(nirvana, () { assert(exists(entry)); }); + + const file2 = "3/5/6"; + auto entry2 = DirEntry(file2); + runIn(nirvana, () { + assert(!exists(file2)); + assert( exists(entry2)); + }); }); // Directory tree traversal test From 06a3b173775d85ba7124a313076ba28cd5a32f99 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 04:57:02 +0200 Subject: [PATCH 18/60] Port `getTimesWin()` --- std/file.d | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 822da6f801f..9911fefa588 100644 --- a/std/file.d +++ b/std/file.d @@ -328,6 +328,59 @@ version (Windows) @safe unittest remove(file); assert(!file.exists); }); + + // Windows File-time querying test + version (Windows) runIn(root, { + import std.datetime : Clock; + + auto now = Clock.currTime(); + const string file = "1/2/test.txt"; + write(file, "…"); + + auto entry = DirEntry(file); + runIn(nirvana, { + assert(!file.exists); + SysTime creationTime, accessTime, modificationTime; + getTimesWin(entry, creationTime, accessTime, modificationTime); + + if (creationTime < now) + { + import std.stdio : stderr; + () @trusted + { + stderr.writeln( + __FILE__, ":", __LINE__, + "Unexpected creation time; probably caused by time-sync or filesystem.", + ); + }(); + } + if (accessTime < now) + { + import std.stdio : stderr; + () @trusted + { + stderr.writeln( + __FILE__, ":", __LINE__, + "Unexpected access time; probably caused by time-sync or filesystem.", + ); + }(); + } + if (modificationTime < now) + { + import std.stdio : stderr; + () @trusted + { + stderr.writeln( + __FILE__, ":", __LINE__, + "Unexpected modification time; probably caused by time-sync or filesystem.", + ); + }(); + } + }); + + remove(file); + assert(!file.exists); + }); } // Purposefully not documented. Use at your own risk @@ -1672,10 +1725,19 @@ else version (Windows) out SysTime fileCreationTime, out SysTime fileAccessTime, out SysTime fileModificationTime) - if (isConvertibleToString!R) + if (isConvertibleToStringButNoDirEntry!R) { getTimesWin!(StringTypeOf!R)(name, fileCreationTime, fileAccessTime, fileModificationTime); } + + void getTimesWin(R)(auto ref R name, + out SysTime fileCreationTime, + out SysTime fileAccessTime, + out SysTime fileModificationTime) + if (isDirEntry!R) + { + return getTimesWin(name.absoluteName, fileCreationTime, fileAccessTime, fileModificationTime); + } } version (Windows) @system unittest From fd7ccb2ad3fbba2aba40bb73d51964c97c0cad09 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 04:57:20 +0200 Subject: [PATCH 19/60] Update `getTimes()` test to match test of `getTimesWin()` --- std/file.d | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/std/file.d b/std/file.d index 9911fefa588..e049f262b34 100644 --- a/std/file.d +++ b/std/file.d @@ -312,7 +312,10 @@ version (Windows) @safe unittest import std.stdio : stderr; () @trusted { - stderr.writeln("Unexpected access time; probably caused by time-sync or filesystem."); + stderr.writeln( + __FILE__, ":", __LINE__, + "Unexpected access time; probably caused by time-sync or filesystem. ", + ); }(); } if (modificationTime < now) @@ -320,7 +323,10 @@ version (Windows) @safe unittest import std.stdio : stderr; () @trusted { - stderr.writeln("Unexpected modification time; probably caused by time-sync or filesystem."); + stderr.writeln( + __FILE__, ":", __LINE__, + "Unexpected modification time; probably caused by time-sync or filesystem.", + ); }(); } }); From c93ee7db609cb33ffc629cb1b1576aa53b6e0b76 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 05:02:10 +0200 Subject: [PATCH 20/60] Refactor `getTimes()` unittests --- std/file.d | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/std/file.d b/std/file.d index e049f262b34..83d2fe098d7 100644 --- a/std/file.d +++ b/std/file.d @@ -296,6 +296,7 @@ version (Windows) @safe unittest // File-time querying test runIn(root, { import std.datetime : Clock; + import std.stdio : stderr; auto now = Clock.currTime(); const string file = "1/2/test.txt"; @@ -309,22 +310,20 @@ version (Windows) @safe unittest if (accessTime < now) { - import std.stdio : stderr; () @trusted { stderr.writeln( - __FILE__, ":", __LINE__, + __FILE__, "(", __LINE__, "): ", "Unexpected access time; probably caused by time-sync or filesystem. ", ); }(); } if (modificationTime < now) { - import std.stdio : stderr; () @trusted { stderr.writeln( - __FILE__, ":", __LINE__, + __FILE__, "(", __LINE__, "): ", "Unexpected modification time; probably caused by time-sync or filesystem.", ); }(); @@ -338,6 +337,7 @@ version (Windows) @safe unittest // Windows File-time querying test version (Windows) runIn(root, { import std.datetime : Clock; + import std.stdio : stderr; auto now = Clock.currTime(); const string file = "1/2/test.txt"; @@ -351,33 +351,30 @@ version (Windows) @safe unittest if (creationTime < now) { - import std.stdio : stderr; () @trusted { stderr.writeln( - __FILE__, ":", __LINE__, + __FILE__, "(", __LINE__, "): ", "Unexpected creation time; probably caused by time-sync or filesystem.", ); }(); } if (accessTime < now) { - import std.stdio : stderr; () @trusted { stderr.writeln( - __FILE__, ":", __LINE__, + __FILE__, "(", __LINE__, "): ", "Unexpected access time; probably caused by time-sync or filesystem.", ); }(); } if (modificationTime < now) { - import std.stdio : stderr; () @trusted { stderr.writeln( - __FILE__, ":", __LINE__, + __FILE__, "(", __LINE__, "): ", "Unexpected modification time; probably caused by time-sync or filesystem.", ); }(); From a968264b93f43ebabf528453ee1a0c0d969951d3 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 05:54:50 +0200 Subject: [PATCH 21/60] Port `setTimes()` --- std/file.d | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 83d2fe098d7..ac9637b2d8e 100644 --- a/std/file.d +++ b/std/file.d @@ -384,6 +384,52 @@ version (Windows) @safe unittest remove(file); assert(!file.exists); }); + + // File-time application test + runIn(root, { + import std.datetime : Clock, dur, SysTime; + import std.stdio : stderr; + + const oneYear = 900.dur!"days"; + const now = Clock.currTime(); + const thePast = now - oneYear; + const theFuture = now + oneYear; + + const string file = "1/2/test.txt"; + write(file, "…"); + + auto entry = DirEntry(file); + runIn(nirvana, { + setTimes(entry, thePast, theFuture); + }); + + SysTime accessTime, modificationTime; + getTimes(entry, accessTime, modificationTime); + + if (accessTime != thePast) + { + () @trusted + { + stderr.writeln( + __FILE__, "(", __LINE__, "): ", + "Unexpected access time; probably caused by time-sync or filesystem.", + ); + }(); + } + if (modificationTime != theFuture) + { + () @trusted + { + stderr.writeln( + __FILE__, "(", __LINE__, "): ", + "Unexpected modification time; probably caused by time-sync or filesystem.", + ); + }(); + } + + remove(file); + assert(!file.exists); + }); } // Purposefully not documented. Use at your own risk @@ -1875,11 +1921,23 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) void setTimes(R)(auto ref R name, SysTime accessTime, SysTime modificationTime) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { setTimes!(StringTypeOf!R)(name, accessTime, modificationTime); } +/// ditto +void setTimes(R)(auto ref R name, + SysTime accessTime, + SysTime modificationTime) +if (isDirEntry!R) +{ + version (Windows) + return setTimes(name.absoluteName, accessTime, modificationTime); + else + return setTimes(name.name, accessTime, modificationTime); +} + private void setTimesImpl(scope const(char)[] names, scope const(FSChar)* namez, SysTime accessTime, SysTime modificationTime) @trusted { From 859e7b9a600ad220423a2cf5b1a873fc6e4c9b98 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 05:55:52 +0200 Subject: [PATCH 22/60] Cleanup unittest --- std/file.d | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/std/file.d b/std/file.d index ac9637b2d8e..bad1da729a7 100644 --- a/std/file.d +++ b/std/file.d @@ -295,16 +295,15 @@ version (Windows) @safe unittest // File-time querying test runIn(root, { - import std.datetime : Clock; + import std.datetime : Clock, SysTime; import std.stdio : stderr; - auto now = Clock.currTime(); + const now = Clock.currTime(); const string file = "1/2/test.txt"; write(file, "…"); auto entry = DirEntry(file); runIn(nirvana, { - assert(!file.exists); SysTime accessTime, modificationTime; getTimes(entry, accessTime, modificationTime); @@ -336,7 +335,7 @@ version (Windows) @safe unittest // Windows File-time querying test version (Windows) runIn(root, { - import std.datetime : Clock; + import std.datetime : Clock, SysTime; import std.stdio : stderr; auto now = Clock.currTime(); @@ -345,7 +344,6 @@ version (Windows) @safe unittest auto entry = DirEntry(file); runIn(nirvana, { - assert(!file.exists); SysTime creationTime, accessTime, modificationTime; getTimesWin(entry, creationTime, accessTime, modificationTime); From 2128737e212653c166af11a31abeec4d62b22bad Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 06:57:02 +0200 Subject: [PATCH 23/60] Port `timeLastModified()` --- std/file.d | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/std/file.d b/std/file.d index bad1da729a7..f28a5cbfde7 100644 --- a/std/file.d +++ b/std/file.d @@ -303,8 +303,8 @@ version (Windows) @safe unittest write(file, "…"); auto entry = DirEntry(file); + SysTime accessTime, modificationTime; runIn(nirvana, { - SysTime accessTime, modificationTime; getTimes(entry, accessTime, modificationTime); if (accessTime < now) @@ -329,8 +329,17 @@ version (Windows) @safe unittest } }); + runIn(nirvana, { + assert(timeLastModified(entry) == modificationTime); + }); + remove(file); assert(!file.exists); + + runIn(nirvana, { + // non-existent file + assert(timeLastModified(entry, now) == now); + }); }); // Windows File-time querying test @@ -2083,11 +2092,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto SysTime timeLastModified(R)(auto ref R name) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return timeLastModified!(StringTypeOf!R)(name); } +/// ditto +SysTime timeLastModified(R)(auto ref R name) +if (isDirEntry!R) +{ + version (Windows) + return timeLastModified(name.absoluteName); + else + return timeLastModified(name.name); +} + /// @safe unittest { @@ -2139,7 +2158,7 @@ else -------------------- +/ SysTime timeLastModified(R)(R name, SysTime returnIfMissing) -if (isSomeFiniteCharInputRange!R) +if (isSomeFiniteCharInputRange!R && !isDirEntry!R) { version (Windows) { @@ -2164,6 +2183,16 @@ if (isSomeFiniteCharInputRange!R) } } +/// ditto +SysTime timeLastModified(R)(R name, SysTime returnIfMissing) +if (isDirEntry!R) +{ + version (Windows) + return timeLastModified(name.absoluteName, returnIfMissing); + else + return timeLastModified(name.name, returnIfMissing); +} + /// @safe unittest { From b5259c94e1479adcbe74dbd996450bda84bedff8 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 06:57:47 +0200 Subject: [PATCH 24/60] Use different filenames --- std/file.d | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/std/file.d b/std/file.d index f28a5cbfde7..9f259bc87cd 100644 --- a/std/file.d +++ b/std/file.d @@ -133,6 +133,18 @@ version (Windows) @safe unittest { import std.path : absolutePath, buildPath; + template lineNumberString(size_t line = __LINE__) + { + import std.conv : to; + enum lineNumberString = to!string(line); + } + + { + import std.conv : to; + assert(lineNumberString!().to!size_t() == __LINE__); + assert(lineNumberString!().to!size_t() == __LINE__); + } + static void runIn(string dir, void delegate() @safe callback) { const origWD = getcwd(); @@ -265,7 +277,7 @@ version (Windows) @safe unittest // Removal test runIn(root, { - const string file = "1/2/test.txt"; + const string file = "1/2/test_" ~ lineNumberString!(); write(file, "…"); auto entry = DirEntry(file); assert(file.exists); @@ -280,7 +292,7 @@ version (Windows) @safe unittest // File-size querying test runIn(root, { - const string file = "1/2/test.txt"; + const string file = "1/2/test_" ~ lineNumberString!(); static immutable data = cast(immutable(ubyte)[]) "foobar"; write(file, data); @@ -299,7 +311,7 @@ version (Windows) @safe unittest import std.stdio : stderr; const now = Clock.currTime(); - const string file = "1/2/test.txt"; + const string file = "1/2/test_" ~ lineNumberString!(); write(file, "…"); auto entry = DirEntry(file); @@ -348,7 +360,7 @@ version (Windows) @safe unittest import std.stdio : stderr; auto now = Clock.currTime(); - const string file = "1/2/test.txt"; + const string file = "1/2/test_" ~ lineNumberString!(); write(file, "…"); auto entry = DirEntry(file); @@ -402,7 +414,7 @@ version (Windows) @safe unittest const thePast = now - oneYear; const theFuture = now + oneYear; - const string file = "1/2/test.txt"; + const string file = "1/2/test_" ~ lineNumberString!(); write(file, "…"); auto entry = DirEntry(file); From 9f32604b4ddd65d4560b1fa78c74eac4d6dd8f59 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 07:00:51 +0200 Subject: [PATCH 25/60] Simplify file-time warnings code in unittest --- std/file.d | 76 ++++++++++-------------------------------------------- 1 file changed, 13 insertions(+), 63 deletions(-) diff --git a/std/file.d b/std/file.d index 9f259bc87cd..4f3d5cd28c4 100644 --- a/std/file.d +++ b/std/file.d @@ -154,6 +154,12 @@ version (Windows) @safe unittest callback(); } + static void warnAbout(string msg, string file = __FILE__, size_t line = __LINE__) @trusted + { + import std.stdio : stderr; + stderr.writefln!"%s(%s): %s"(file, line, msg); + } + string parent = deleteme().absolutePath; string root = parent.buildPath("r"); mkdirRecurse(root); @@ -320,25 +326,9 @@ version (Windows) @safe unittest getTimes(entry, accessTime, modificationTime); if (accessTime < now) - { - () @trusted - { - stderr.writeln( - __FILE__, "(", __LINE__, "): ", - "Unexpected access time; probably caused by time-sync or filesystem. ", - ); - }(); - } + warnAbout("Unexpected access time; probably caused by time-sync or filesystem."); if (modificationTime < now) - { - () @trusted - { - stderr.writeln( - __FILE__, "(", __LINE__, "): ", - "Unexpected modification time; probably caused by time-sync or filesystem.", - ); - }(); - } + warnAbout("Unexpected modification time; probably caused by time-sync or filesystem."); }); runIn(nirvana, { @@ -369,35 +359,11 @@ version (Windows) @safe unittest getTimesWin(entry, creationTime, accessTime, modificationTime); if (creationTime < now) - { - () @trusted - { - stderr.writeln( - __FILE__, "(", __LINE__, "): ", - "Unexpected creation time; probably caused by time-sync or filesystem.", - ); - }(); - } + warnAbout("Unexpected creation time; probably caused by time-sync or filesystem."); if (accessTime < now) - { - () @trusted - { - stderr.writeln( - __FILE__, "(", __LINE__, "): ", - "Unexpected access time; probably caused by time-sync or filesystem.", - ); - }(); - } + warnAbout("Unexpected access time; probably caused by time-sync or filesystem."); if (modificationTime < now) - { - () @trusted - { - stderr.writeln( - __FILE__, "(", __LINE__, "): ", - "Unexpected modification time; probably caused by time-sync or filesystem.", - ); - }(); - } + warnAbout("Unexpected modification time; probably caused by time-sync or filesystem."); }); remove(file); @@ -426,25 +392,9 @@ version (Windows) @safe unittest getTimes(entry, accessTime, modificationTime); if (accessTime != thePast) - { - () @trusted - { - stderr.writeln( - __FILE__, "(", __LINE__, "): ", - "Unexpected access time; probably caused by time-sync or filesystem.", - ); - }(); - } + warnAbout("Unexpected access time; probably caused by time-sync or filesystem."); if (modificationTime != theFuture) - { - () @trusted - { - stderr.writeln( - __FILE__, "(", __LINE__, "): ", - "Unexpected modification time; probably caused by time-sync or filesystem.", - ); - }(); - } + warnAbout("Unexpected modification time; probably caused by time-sync or filesystem."); remove(file); assert(!file.exists); From 55af39de4d9b7481b060b09e1cc9d3ac2bc7ca03 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 07:09:34 +0200 Subject: [PATCH 26/60] Improve comment --- std/file.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 4f3d5cd28c4..f2eb019369c 100644 --- a/std/file.d +++ b/std/file.d @@ -344,7 +344,7 @@ version (Windows) @safe unittest }); }); - // Windows File-time querying test + // Windows-only file-time querying test version (Windows) runIn(root, { import std.datetime : Clock, SysTime; import std.stdio : stderr; From cd1a14cf0a920a84880bb1e4a3f8f7189d0c62fc Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 07:23:17 +0200 Subject: [PATCH 27/60] Port `getAttributes()` and `getLinkAttributes()` --- std/file.d | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/std/file.d b/std/file.d index f2eb019369c..b9bebf92c4f 100644 --- a/std/file.d +++ b/std/file.d @@ -399,6 +399,22 @@ version (Windows) @safe unittest remove(file); assert(!file.exists); }); + + // Attribute querying test + runIn(root, { + const string path = "1/2"; + auto entry = DirEntry(path); + + runIn(nirvana, { + const attributes = getAttributes(entry); + assert( attributes.attrIsDir); + assert(!attributes.attrIsFile); + + const linkAttributes = getLinkAttributes(entry); + assert( linkAttributes.attrIsDir); + assert(!linkAttributes.attrIsFile); + }); + }); } // Purposefully not documented. Use at your own risk @@ -2437,11 +2453,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto uint getAttributes(R)(auto ref R name) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return getAttributes!(StringTypeOf!R)(name); } +/// ditto +uint getAttributes(R)(auto ref R name) +if (isDirEntry!R) +{ + version (Windows) + return getAttributes(name.absoluteName); + else + return getAttributes(name.name); +} + /// getAttributes with a file @safe unittest { @@ -2526,11 +2552,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto uint getLinkAttributes(R)(auto ref R name) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return getLinkAttributes!(StringTypeOf!R)(name); } +/// ditto +uint getLinkAttributes(R)(auto ref R name) +if (isDirEntry!R) +{ + version (Windows) + return getLinkAttributes(name.absoluteName); + else + return getLinkAttributes(name.name); +} + /// @safe unittest { From 0ec63ae103a6350b04884f919e6f771808c4818f Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 23:04:29 +0200 Subject: [PATCH 28/60] Port `setAttributes()` --- std/file.d | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index b9bebf92c4f..75e80631629 100644 --- a/std/file.d +++ b/std/file.d @@ -415,6 +415,30 @@ version (Windows) @safe unittest assert(!linkAttributes.attrIsFile); }); }); + + // Attribute appliance test + runIn(root, { + import std.conv : octal; + + const string file = "1/2/test_" ~ lineNumberString!(); + write(file, "…"); + auto entry = DirEntry(file); + + runIn(nirvana, { + const attributes0 = getAttributes(entry); + version (Posix) + const attributes1 = (attributes0 & uint(octal!"37777777000")) | octal!"400"; + version (Windows) + const attributes1 = attributes0 | FILE_ATTRIBUTE_READONLY; + + setAttributes(entry, attributes1); + scope (exit) setAttributes(entry, attributes0); + + assert(getAttributes(entry) == attributes1); + }); + + remove(file); + }); } // Purposefully not documented. Use at your own risk @@ -2678,11 +2702,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto void setAttributes(R)(auto ref R name, uint attributes) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return setAttributes!(StringTypeOf!R)(name, attributes); } +/// ditto +void setAttributes(R)(auto ref R name, uint attributes) +if (isDirEntry!R) +{ + version (Windows) + return setAttributes(name.absoluteName, attributes); + else + return setAttributes(name.name, attributes); +} + @safe unittest { static assert(__traits(compiles, setAttributes(TestAliasedString(null), 0))); From 0d024cb7a819ab019eb98a22d51acbdb68a5eebf Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 23:21:54 +0200 Subject: [PATCH 29/60] Port `isFile()` and `isDir()` --- std/file.d | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/std/file.d b/std/file.d index 75e80631629..e86f5be154f 100644 --- a/std/file.d +++ b/std/file.d @@ -439,6 +439,25 @@ version (Windows) @safe unittest remove(file); }); + + // Type identification test + runIn(root, { + const string filePath = "1/2/test_" ~ lineNumberString!(); + const string dirPath = "3/4"; + write(filePath, "…"); + + const fileEntry = DirEntry(filePath); + const dirEntry = DirEntry(dirPath); + + runIn(nirvana, { + assert( isFile(fileEntry)); + assert(!isDir (fileEntry)); + assert(!isFile( dirEntry)); + assert( isDir ( dirEntry)); + }); + + remove(filePath); + }); } // Purposefully not documented. Use at your own risk @@ -2801,11 +2820,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto @property bool isDir(R)(auto ref R name) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return name.isDir!(StringTypeOf!R); } +/// ditto +@property bool isDir(R)(auto ref R name) +if (isDirEntry!R) +{ + version (Windows) + return isDir(name.absoluteName); + else + return isDir(name.name); +} + /// @safe unittest { @@ -2975,11 +3004,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto @property bool isFile(R)(auto ref R name) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return isFile!(StringTypeOf!R)(name); } +/// ditto +@property bool isFile(R)(auto ref R name) +if (isDirEntry!R) +{ + version (Windows) + return isFile(name.absoluteName); + else + return isFile(name.name); +} + /// @safe unittest { From 22bb4419db59795754203b053dfabdcee400f2c6 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 30 Mar 2025 23:23:27 +0200 Subject: [PATCH 30/60] Improve `isDirEntry` traits --- std/file.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/std/file.d b/std/file.d index e86f5be154f..94211d17f02 100644 --- a/std/file.d +++ b/std/file.d @@ -126,8 +126,8 @@ else version (Posix) else static assert(0); -private enum isConvertibleToStringButNoDirEntry(T) = !is(T == DirEntry) && isConvertibleToString!T; -private enum isDirEntry(T) = is(T == DirEntry); +private enum isDirEntry(T) = is(T == DirEntry) || is(T == const(DirEntry)) || is(T == immutable(DirEntry)); +private enum isConvertibleToStringButNoDirEntry(T) = !isDirEntry!T && isConvertibleToString!T; version (Windows) @safe unittest { From 16608096970d158f4041d4e6d27448bf41395ab0 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 00:14:49 +0200 Subject: [PATCH 31/60] Refactor scope guards --- std/file.d | 52 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/std/file.d b/std/file.d index 94211d17f02..6adff63d3fb 100644 --- a/std/file.d +++ b/std/file.d @@ -149,7 +149,7 @@ version (Windows) @safe unittest { const origWD = getcwd(); chdir(dir); - scope(exit) chdir(origWD); + scope (exit) chdir(origWD); callback(); } @@ -285,14 +285,17 @@ version (Windows) @safe unittest runIn(root, { const string file = "1/2/test_" ~ lineNumberString!(); write(file, "…"); + scope (failure) { if (file.exists) remove(file); } + auto entry = DirEntry(file); assert(file.exists); runIn(nirvana, { - assert(entry.exists); - remove(entry); + assert( entry.exists); + remove( entry); assert(!entry.exists); }); + assert(!file.exists); }); @@ -301,14 +304,12 @@ version (Windows) @safe unittest const string file = "1/2/test_" ~ lineNumberString!(); static immutable data = cast(immutable(ubyte)[]) "foobar"; write(file, data); + scope (exit) remove(file); auto entry = DirEntry(file); runIn(nirvana, { assert(getSize(entry) == data.length); }); - - remove(file); - assert(!file.exists); }); // File-time querying test @@ -319,6 +320,7 @@ version (Windows) @safe unittest const now = Clock.currTime(); const string file = "1/2/test_" ~ lineNumberString!(); write(file, "…"); + scope (failure) { if (file.exists) remove(file); } auto entry = DirEntry(file); SysTime accessTime, modificationTime; @@ -352,6 +354,7 @@ version (Windows) @safe unittest auto now = Clock.currTime(); const string file = "1/2/test_" ~ lineNumberString!(); write(file, "…"); + scope (exit) remove(file); auto entry = DirEntry(file); runIn(nirvana, { @@ -365,9 +368,6 @@ version (Windows) @safe unittest if (modificationTime < now) warnAbout("Unexpected modification time; probably caused by time-sync or filesystem."); }); - - remove(file); - assert(!file.exists); }); // File-time application test @@ -382,6 +382,7 @@ version (Windows) @safe unittest const string file = "1/2/test_" ~ lineNumberString!(); write(file, "…"); + scope (exit) remove(file); auto entry = DirEntry(file); runIn(nirvana, { @@ -395,9 +396,6 @@ version (Windows) @safe unittest warnAbout("Unexpected access time; probably caused by time-sync or filesystem."); if (modificationTime != theFuture) warnAbout("Unexpected modification time; probably caused by time-sync or filesystem."); - - remove(file); - assert(!file.exists); }); // Attribute querying test @@ -445,6 +443,7 @@ version (Windows) @safe unittest const string filePath = "1/2/test_" ~ lineNumberString!(); const string dirPath = "3/4"; write(filePath, "…"); + scope (exit) remove(filePath); const fileEntry = DirEntry(filePath); const dirEntry = DirEntry(dirPath); @@ -455,8 +454,23 @@ version (Windows) @safe unittest assert(!isFile( dirEntry)); assert( isDir ( dirEntry)); }); + }); + + // Working directory change test + runIn(root, { + const string dirPath = "3/5"; - remove(filePath); + const string sentinel = lineNumberString!(); + const string sentinelPath = dirPath.buildPath(sentinel); + write(sentinelPath, "…"); + scope (exit) remove(sentinelPath); + + const dirEntry = DirEntry(dirPath); + runIn(nirvana, { + chdir(dirEntry); + assert(sentinel.exists); + assert(sentinel.isFile); + }); }); } @@ -3387,11 +3401,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto void chdir(R)(auto ref R pathname) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return chdir!(StringTypeOf!R)(pathname); } +/// ditto +void chdir(R)(auto ref R pathname) +if (isDirEntry!R) +{ + version (Windows) + return chdir(pathname.absoluteName); + else + return chdir(pathname.name); +} + /// @system unittest { From c2005e4dbfecd9b1ff1720872fa0fc7a752b2a1d Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 00:20:01 +0200 Subject: [PATCH 32/60] Port `isSymlink()` --- std/file.d | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/std/file.d b/std/file.d index 6adff63d3fb..3c4fefafc2d 100644 --- a/std/file.d +++ b/std/file.d @@ -449,10 +449,12 @@ version (Windows) @safe unittest const dirEntry = DirEntry(dirPath); runIn(nirvana, { - assert( isFile(fileEntry)); - assert(!isDir (fileEntry)); - assert(!isFile( dirEntry)); - assert( isDir ( dirEntry)); + assert( isFile (fileEntry)); + assert(!isDir (fileEntry)); + assert(!isSymlink(fileEntry)); + assert(!isFile ( dirEntry)); + assert( isDir ( dirEntry)); + assert(!isSymlink( dirEntry)); }); }); @@ -3203,11 +3205,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto @property bool isSymlink(R)(auto ref R name) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return name.isSymlink!(StringTypeOf!R); } +/// ditto +@property bool isSymlink(R)(auto ref R name) +if (isDirEntry!R) +{ + version (Windows) + return isSymlink(name.absoluteName); + else + return isSymlink(name.name); +} + @safe unittest { static assert(__traits(compiles, TestAliasedString(null).isSymlink)); From bfc23c2e4fe6fc3b80fa18e1bb48559f2075e998 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 00:31:31 +0200 Subject: [PATCH 33/60] Port `mkdir()` --- std/file.d | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 3c4fefafc2d..905567aa893 100644 --- a/std/file.d +++ b/std/file.d @@ -474,6 +474,22 @@ version (Windows) @safe unittest assert(sentinel.isFile); }); }); + + // Directory creation test + runIn(root, { + const string dirPath = "3/5/" ~ lineNumberString!(); + mkdir(dirPath); + + const dirEntry = DirEntry(dirPath); + rmdir(dirPath); + assert(!dirPath.exists); + + runIn(nirvana, { + mkdir(dirEntry); + }); + assert(dirPath.exists); + rmdir(dirPath); + }); } // Purposefully not documented. Use at your own risk @@ -3500,11 +3516,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto void mkdir(R)(auto ref R pathname) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { return mkdir!(StringTypeOf!R)(pathname); } +/// ditto +void mkdir(R)(auto ref R pathname) +if (isDirEntry!R) +{ + version (Windows) + return mkdir(pathname.absoluteName); + else + return mkdir(pathname.name); +} + @safe unittest { import std.file : mkdir; From 41adc421f613ac98c19998800a91d1ad2694f65a Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 00:40:17 +0200 Subject: [PATCH 34/60] Port `rmdir()` --- std/file.d | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 905567aa893..5a7cab160a7 100644 --- a/std/file.d +++ b/std/file.d @@ -490,6 +490,32 @@ version (Windows) @safe unittest assert(dirPath.exists); rmdir(dirPath); }); + + // Directory removal test + runIn(root, { + const string dirPath = "3/5/" ~ lineNumberString!(); + mkdir(dirPath); + + assert(dirPath.exists); + const dirEntry = DirEntry(dirPath); + runIn(nirvana, { + rmdir(dirEntry); + }); + assert(!dirPath.exists); + }); + + // Directory non-removal test + runIn(root, { + import std.exception : assertThrown; + + const string dirPath = "3"; + assert(dirPath.exists); + const dirEntry = DirEntry(dirPath); + runIn(nirvana, { + assertThrown(rmdir(dirEntry)); + }); + assert(dirPath.exists); + }); } // Purposefully not documented. Use at your own risk @@ -3722,11 +3748,21 @@ if (isSomeFiniteCharInputRange!R && !isConvertibleToString!R) /// ditto void rmdir(R)(auto ref R pathname) -if (isConvertibleToString!R) +if (isConvertibleToStringButNoDirEntry!R) { rmdir!(StringTypeOf!R)(pathname); } +/// ditto +void rmdir(R)(auto ref R pathname) +if (isDirEntry!R) +{ + version (Windows) + return rmdir(pathname.absoluteName); + else + return rmdir(pathname.name); +} + @safe unittest { static assert(__traits(compiles, rmdir(TestAliasedString(null)))); From 60550e3b0f43c752092183d1cc1ed15defbeaad1 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 01:09:53 +0200 Subject: [PATCH 35/60] Port `rmdirRecurse()` --- std/file.d | 56 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/std/file.d b/std/file.d index 5a7cab160a7..19c0d5d0170 100644 --- a/std/file.d +++ b/std/file.d @@ -516,6 +516,52 @@ version (Windows) @safe unittest }); assert(dirPath.exists); }); + + // Directory tree removal test + version (none) // TODO: port `dirEntries()` + runIn(root, { + import std.exception : assertThrown; + + const string treeRoot = "tree_" ~ lineNumberString!(); + mkdir(treeRoot); + + const string treeBranch1 = treeRoot ~ "/ab/cd/ef"; + const string treeBranch2 = treeRoot ~ "/ab/gh"; + const string treeBranch3 = treeRoot ~ "/ij"; + const string treeBranch4 = treeRoot ~ "/ij/kl.mno"; + + mkdirRecurse(treeBranch1); + mkdirRecurse(treeBranch2); + mkdirRecurse(treeBranch3); + write (treeBranch4, "…"); + + assert(treeBranch1.exists); + assert(treeBranch2.exists); + assert(treeBranch3.exists); + assert(treeBranch4.exists); + + const entryRoot = DirEntry(treeRoot); + const entryBranch2 = DirEntry(treeBranch2); + + runIn(nirvana, { + rmdirRecurse(entryBranch2); + }); + assert( treeBranch1.exists); + assert(!treeBranch2.exists); + assert( treeBranch3.exists); + assert( treeBranch4.exists); + assert( treeRoot.exists); + + runIn(nirvana, { + assertThrown(rmdir(entryRoot)); + rmdirRecurse(entryRoot); + }); + assert(!treeBranch1.exists); + assert(!treeBranch2.exists); + assert(!treeBranch3.exists); + assert(!treeBranch4.exists); + assert(!treeRoot.exists); + }); } // Purposefully not documented. Use at your own risk @@ -5068,9 +5114,9 @@ void rmdirRecurse(ref scope DirEntry de) @safe if (de.isSymlink) { version (Windows) - rmdir(de.name); + rmdir(de); else - remove(de.name); + remove(de); } else { @@ -5080,14 +5126,14 @@ void rmdirRecurse(ref scope DirEntry de) @safe // be @trusted () @trusted { // all children, recursively depth-first - foreach (DirEntry e; dirEntries(de.name, SpanMode.depth, false)) + foreach (DirEntry e; dirEntries(de, SpanMode.depth, false)) { - attrIsDir(e.linkAttributes) ? rmdir(e.name) : remove(e.name); + attrIsDir(e.linkAttributes) ? rmdir(e) : remove(e); } }(); // the dir itself - rmdir(de.name); + rmdir(de); } } ///ditto From ecd7765a9ccdbbc14c8c498e29dedcb5f35a54f5 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 01:10:23 +0200 Subject: [PATCH 36/60] Improve tiny details --- std/file.d | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/std/file.d b/std/file.d index 19c0d5d0170..b7d74e2321b 100644 --- a/std/file.d +++ b/std/file.d @@ -462,7 +462,7 @@ version (Windows) @safe unittest runIn(root, { const string dirPath = "3/5"; - const string sentinel = lineNumberString!(); + const string sentinel = "sentinel_" ~ lineNumberString!(); const string sentinelPath = dirPath.buildPath(sentinel); write(sentinelPath, "…"); scope (exit) remove(sentinelPath); @@ -477,7 +477,7 @@ version (Windows) @safe unittest // Directory creation test runIn(root, { - const string dirPath = "3/5/" ~ lineNumberString!(); + const string dirPath = "3/5/test_" ~ lineNumberString!(); mkdir(dirPath); const dirEntry = DirEntry(dirPath); @@ -493,7 +493,7 @@ version (Windows) @safe unittest // Directory removal test runIn(root, { - const string dirPath = "3/5/" ~ lineNumberString!(); + const string dirPath = "3/5/test_" ~ lineNumberString!(); mkdir(dirPath); assert(dirPath.exists); @@ -504,7 +504,7 @@ version (Windows) @safe unittest assert(!dirPath.exists); }); - // Directory non-removal test + // Directory tree non-removal test runIn(root, { import std.exception : assertThrown; From 4aaa8d1c2025e171facf82ef4469d73167b332bf Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 01:20:33 +0200 Subject: [PATCH 37/60] Port `mkdirRecurse()` --- std/file.d | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index b7d74e2321b..bf10a27f908 100644 --- a/std/file.d +++ b/std/file.d @@ -491,6 +491,42 @@ version (Windows) @safe unittest rmdir(dirPath); }); + // Directory tree creation test + runIn(root, { + import std.exception : assertThrown; + + const string treeRoot = "tree_" ~ lineNumberString!(); + mkdir(treeRoot); + scope (exit) { if (treeRoot.exists) rmdirRecurse(treeRoot); } + + const string treeBranch1 = treeRoot ~ "/ab/cd/ef"; + const string treeBranch2 = treeRoot ~ "/ab/gh"; + const string treeBranch3 = treeRoot ~ "/ij"; + mkdirRecurse(treeBranch1); + mkdirRecurse(treeBranch2); + mkdirRecurse(treeBranch3); + const entry1 = DirEntry(treeBranch1); + const entry2 = DirEntry(treeBranch2); + const entry3 = DirEntry(treeBranch3); + + rmdirRecurse(treeRoot); + assert(!treeRoot.exists); + assert(!treeBranch1.exists); + assert(!treeBranch2.exists); + assert(!treeBranch3.exists); + + runIn(nirvana, { + mkdirRecurse(entry1); + mkdirRecurse(entry2); + mkdirRecurse(entry3); + }); + + assert(treeRoot.exists); + assert(treeBranch1.exists); + assert(treeBranch2.exists); + assert(treeBranch3.exists); + }); + // Directory removal test runIn(root, { const string dirPath = "3/5/test_" ~ lineNumberString!(); @@ -3676,10 +3712,19 @@ void mkdirRecurse(scope const(char)[] pathname) @safe } if (!baseName(pathname).empty) { - ensureDirExists(pathname); + cast(void) ensureDirExists(pathname); } } +/// ditto +void mkdirRecurse(scope const DirEntry pathname) @safe +{ + version (Windows) + return mkdirRecurse(pathname.absoluteName); + else + return mkdirRecurse(pathname.name); +} + /// @safe unittest { From 88362b552bd823cde230ba45a96b4f5eb694d641 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 01:33:32 +0200 Subject: [PATCH 38/60] Port `getAvailableDiskSpace()` --- std/file.d | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/std/file.d b/std/file.d index bf10a27f908..33b47644d92 100644 --- a/std/file.d +++ b/std/file.d @@ -598,6 +598,28 @@ version (Windows) @safe unittest assert(!treeBranch4.exists); assert(!treeRoot.exists); }); + + // Disk space querying test + runIn(root, { + const string path1 = "test1_" ~ lineNumberString!(); + const string path2 = "test2_" ~ lineNumberString!(); + mkdir(path1); + scope (exit) rmdir(path1); + mkdir(path2); + scope (exit) { if (path2.exists) rmdir(path2); } + + const entry1 = DirEntry(path1); + const entry2 = DirEntry(path2); + + rmdir(path2); + + runIn(nirvana, { + import std.exception : assertThrown; + + assert(getAvailableDiskSpace(entry1) >= 0); + assertThrown(getAvailableDiskSpace(entry2)); + }); + }); } // Purposefully not documented. Use at your own risk @@ -6191,6 +6213,15 @@ ulong getAvailableDiskSpace(scope const(char)[] path) @safe else static assert(0, "Unsupported platform"); } +/// ditto +ulong getAvailableDiskSpace(scope const DirEntry path) @safe +{ + version (Windows) + return getAvailableDiskSpace(path.absoluteName); + else + return getAvailableDiskSpace(path.name); +} + /// @safe unittest { From c63c66e44b6280ecd57cbb6340aa66619bdfa3f4 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 01:52:06 +0200 Subject: [PATCH 39/60] Port `slurp()` --- std/file.d | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/std/file.d b/std/file.d index 33b47644d92..b9b174708cb 100644 --- a/std/file.d +++ b/std/file.d @@ -620,6 +620,18 @@ version (Windows) @safe unittest assertThrown(getAvailableDiskSpace(entry2)); }); }); + + // Slurping test + runIn(root, { + const string filePath = "1/test_" ~ lineNumberString!(); + write(filePath, "10\r\n20"); + scope (exit) remove(filePath); + + auto entry = DirEntry(filePath); + runIn(nirvana, () @trusted { + assert(slurp!(int)(entry, "%d") == [10, 20]); + }); + }); } // Purposefully not documented. Use at your own risk @@ -5992,6 +6004,15 @@ slurp(Types...)(string filename, scope const(char)[] format) return app.data; } +/// ditto +auto slurp(Types...)(const DirEntry filename, scope const(char)[] format) +{ + version (Windows) + return slurp!(Types)(filename.absoluteName, format); + else + return slurp!(Types)(filename.name, format); +} + /// @system unittest { From 99d8266aa08267a705c9bd2cc0b70d148ca9f9de Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 01:52:17 +0200 Subject: [PATCH 40/60] Assert expectations --- std/file.d | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index b9b174708cb..53391d7d7e2 100644 --- a/std/file.d +++ b/std/file.d @@ -131,7 +131,7 @@ private enum isConvertibleToStringButNoDirEntry(T) = !isDirEntry!T && isConverti version (Windows) @safe unittest { - import std.path : absolutePath, buildPath; + import std.path : absolutePath, buildPath, isAbsolute; template lineNumberString(size_t line = __LINE__) { @@ -148,6 +148,8 @@ version (Windows) @safe unittest static void runIn(string dir, void delegate() @safe callback) { const origWD = getcwd(); + assert(origWD.isAbsolute); + chdir(dir); scope (exit) chdir(origWD); From d5b389e4f41e4eecd9acd3cf8a682da5f594e772 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 02:39:02 +0200 Subject: [PATCH 41/60] Port `absolutePath()` and `asAbsolutePath()` --- std/file.d | 21 +++++++++++++++++++++ std/path.d | 21 +++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/std/file.d b/std/file.d index 53391d7d7e2..91eda0818ef 100644 --- a/std/file.d +++ b/std/file.d @@ -241,6 +241,27 @@ version (Windows) @safe unittest assert(found[5] == 0); }); + // Path determination test + runIn(root, { + import std.path : asAbsolutePath, asNormalizedPath; + + const string relative = "1"; + const string absolute = absolutePath(relative); + + const entry = DirEntry(relative); + runIn(nirvana, { + import std.algorithm.comparison : equal; + assert(equal( + absolutePath(entry).asNormalizedPath, + absolute.asNormalizedPath + )); + assert(equal( + asAbsolutePath(entry).asNormalizedPath, + absolute.asNormalizedPath + )); + }); + }); + // Renaming tests runIn(root, { // string-based renaming diff --git a/std/path.d b/std/path.d index 5fb31566baf..a627ea06e40 100644 --- a/std/path.d +++ b/std/path.d @@ -96,7 +96,7 @@ $(TR $(TD Other) $(TD module std.path; -import std.file : getcwd; +import std.file : DirEntry, getcwd; static import std.meta; import std.range; import std.traits; @@ -2756,6 +2756,14 @@ string absolutePath(return scope const string path, lazy string base = getcwd()) return chainPath(baseVar, path).array; } +/// ditto +version (Windows) // There's a chance this cannot be made pure on Posix. +string absolutePath(return scope const DirEntry path, lazy string base = getcwd()) + @safe pure +{ + return absolutePath(path.absoluteName, base); +} + /// @safe unittest { @@ -2856,11 +2864,20 @@ if ((isRandomAccessRange!R && isSomeChar!(ElementType!R) || } auto asAbsolutePath(R)(auto ref R path) -if (isConvertibleToString!R) +if (isConvertibleToString!R && !is(Unconst!R == DirEntry)) { return asAbsolutePath!(StringTypeOf!R)(path); } +auto asAbsolutePath(R)(auto ref scope const R path) +if (is(R == DirEntry)) +{ + version (Windows) + return asAbsolutePath(path.absoluteName); + else + return asAbsolutePath(path.name); +} + @system unittest { assert(testAliasedString!asAbsolutePath(null)); From a4ded70d5ed6711528981bb414bc21944f197ced Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 02:40:04 +0200 Subject: [PATCH 42/60] Use proper `Unconst!T` --- std/file.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 91eda0818ef..1083e236159 100644 --- a/std/file.d +++ b/std/file.d @@ -126,7 +126,7 @@ else version (Posix) else static assert(0); -private enum isDirEntry(T) = is(T == DirEntry) || is(T == const(DirEntry)) || is(T == immutable(DirEntry)); +private enum isDirEntry(T) = is(Unconst!T == DirEntry); private enum isConvertibleToStringButNoDirEntry(T) = !isDirEntry!T && isConvertibleToString!T; version (Windows) @safe unittest From 970550f523198e245e466fb2953c2407cc90db1f Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 03:14:35 +0200 Subject: [PATCH 43/60] Fix `rename()` --- std/file.d | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/std/file.d b/std/file.d index 1083e236159..4ca138e1153 100644 --- a/std/file.d +++ b/std/file.d @@ -1469,21 +1469,40 @@ if ((isSomeFiniteCharInputRange!RF || isSomeString!RF) && !isConvertibleToString /// ditto void rename(RF, RT)(auto ref RF from, auto ref RT to) -if ((isConvertibleToString!RF || isConvertibleToString!RT) && !isDirEntry!RF) +if (isConvertibleToString!RF || isConvertibleToString!RT) { - import std.meta : staticMap; - alias Types = staticMap!(convertToString, RF, RT); - rename!Types(from, to); -} + static if (isDirEntry!RF && isDirEntry!RT) + { + version (Windows) + return rename(from.absoluteName, to.absoluteName); + else + return rename(from.name, to.name); + } + else static if (isDirEntry!RF) + { + alias Types = AliasSeq!(string, convertToString!RT); -/// ditto -void rename(RF, RT)(auto ref RF from, auto ref RT to) -if ((isConvertibleToString!RF || isConvertibleToString!RT) && isDirEntry!RF) -{ - version (Windows) - return rename(from.absoluteName, to); + version (Windows) + return rename!Types(from.absoluteName, to); + else + return rename!Types(from.name, to); + } + else static if (isDirEntry!RT) + { + alias Types = AliasSeq!(convertToString!RF, string); + + version (Windows) + return rename!Types(from, to.absoluteName); + else + return rename!Types(from, to.name); + } else - return rename(from.name, to); + { + import std.meta : staticMap; + + alias Types = staticMap!(convertToString, RF, RT); + rename!Types(from, to); + } } @safe unittest From 2fe45568bf33f0f05aa0087a107c3620dd8956e1 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 03:20:30 +0200 Subject: [PATCH 44/60] Add `rename()` test for dual-`DirEntry`s --- std/file.d | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/std/file.d b/std/file.d index 4ca138e1153..3872091edda 100644 --- a/std/file.d +++ b/std/file.d @@ -302,6 +302,22 @@ version (Windows) @safe unittest assert( "1".exists); assert(!"x".exists); } + // Dual-DirEntry renaming with `chdir()` + { + auto de1 = DirEntry("1"); + runIn(nirvana, { + rename(de1, root.buildPath("x")); + }); + assert(!"1".exists); + assert( "x".exists); + + auto deX = DirEntry("x"); + runIn(nirvana, { + rename(deX, de1); + }); + assert( "1".exists); + assert(!"x".exists); + } }); // Removal test From e6dfd943dfd2900f601d3347c6342264fb9709b8 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 03:26:57 +0200 Subject: [PATCH 45/60] Also test `rename()`ing from `string` to `DirEntry` --- std/file.d | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/std/file.d b/std/file.d index 3872091edda..072f5bb049f 100644 --- a/std/file.d +++ b/std/file.d @@ -302,6 +302,20 @@ version (Windows) @safe unittest assert( "1".exists); assert(!"x".exists); } + // string-based path to `DirEntry` renaming with `chdir()` + { + auto de1 = DirEntry("1"); + rename("1", "x"); + + assert(!"1".exists); + assert( "x".exists); + + runIn(nirvana, { + rename(root.buildPath("x"), de1); + }); + assert( "1".exists); + assert(!"x".exists); + } // Dual-DirEntry renaming with `chdir()` { auto de1 = DirEntry("1"); From 4d643d695d5a9d2ec810d07ead779a468565fa84 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 03:52:20 +0200 Subject: [PATCH 46/60] Port `copy()` --- std/file.d | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 4 deletions(-) diff --git a/std/file.d b/std/file.d index 072f5bb049f..4804eb0b403 100644 --- a/std/file.d +++ b/std/file.d @@ -334,6 +334,69 @@ version (Windows) @safe unittest } }); + // Copying tests + runIn(parent, { + const string copyingDir = "dir_" ~ lineNumberString!(); + mkdir(copyingDir); + scope (exit) rmdirRecurse(copyingDir); + + runIn(copyingDir, { + const string path1 = lineNumberString!() ~ ".f00"; + const string path2 = lineNumberString!() ~ ".f01"; + const string path1Abs = absolutePath(path1); + const string path2Abs = absolutePath(path2); + + static immutable demoData = "foobar\nOachkatzlschwoaf\n"; + write(path1, demoData); + write(path2, "-"); + + const entry1 = DirEntry(path1); + const entry2 = DirEntry(path2); + + // Overwriting files with copies + { + write(path2, "-"); + runIn(nirvana, { + copy(entry1, path2Abs); + }); + assert(readText(path2) == demoData); + + write(path2, "-"); + runIn(nirvana, { + copy(path1Abs, entry2); + }); + assert(readText(path2) == demoData); + + write(path2, "-"); + runIn(nirvana, { + copy(entry1, entry2); + }); + assert(readText(path2) == demoData); + } + + // Creating new files through copying + { + remove(path2); + runIn(nirvana, { + copy(entry1, path2Abs); + }); + assert(readText(path2) == demoData); + + remove(path2); + runIn(nirvana, { + copy(path1Abs, entry2); + }); + assert(readText(path2) == demoData); + + remove(path2); + runIn(nirvana, { + copy(entry1, entry2); + }); + assert(readText(path2) == demoData); + } + }); + }); + // Removal test runIn(root, { const string file = "1/2/test_" ~ lineNumberString!(); @@ -5049,11 +5112,40 @@ if (isSomeFiniteCharInputRange!RF && !isConvertibleToString!RF && /// ditto void copy(RF, RT)(auto ref RF from, auto ref RT to, PreserveAttributes preserve = preserveAttributesDefault) -if (isConvertibleToString!RF || isConvertibleToString!RT) +if (isConvertibleToString!RF || isConvertibleToString!RT || isDirEntry!RF || isDirEntry!RT) { - import std.meta : staticMap; - alias Types = staticMap!(convertToString, RF, RT); - copy!Types(from, to, preserve); + static if (isDirEntry!RF && isDirEntry!RT) + { + version (Windows) + return copy(from.absoluteName, to.absoluteName); + else + return copy(from.name, to.name); + } + else static if (isDirEntry!RF) + { + alias Types = AliasSeq!(string, Unconst!(convertToString!RT)); + + version (Windows) + return copy!Types(from.absoluteName, to); + else + return copy!Types(from.name, to); + } + else static if (isDirEntry!RT) + { + alias Types = AliasSeq!(Unconst!(convertToString!RF), string); + + version (Windows) + return copy!Types(from, to.absoluteName); + else + return copy!Types(from, to.name); + } + else + { + import std.meta : staticMap; + + alias Types = staticMap!(convertToString, RF, RT); + copy!Types(from, to, preserve); + } } /// From ce4010a2729000c07f02263d3a5cce6aefd8725c Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 04:04:56 +0200 Subject: [PATCH 47/60] Harden testing of `rename()` --- std/file.d | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/std/file.d b/std/file.d index 4804eb0b403..9f9372bb5ea 100644 --- a/std/file.d +++ b/std/file.d @@ -316,6 +316,31 @@ version (Windows) @safe unittest assert( "1".exists); assert(!"x".exists); } + // `const` variation + { + const string rp1 = "1"; + const string rpX = "x"; + const string ap1 = absolutePath(rp1); + const string apX = absolutePath(rpX); + const de1 = DirEntry("1"); + rename("1", "x"); + const deX = DirEntry("x"); + + runIn(nirvana, { + rename(apX, de1); + }); + assert( "1".exists); + assert(!"x".exists); + + runIn(nirvana, { + rename(de1, apX); + }); + runIn(nirvana, { + rename(deX, ap1); + }); + assert( "1".exists); + assert(!"x".exists); + } // Dual-DirEntry renaming with `chdir()` { auto de1 = DirEntry("1"); From b8dcae2c5a2e908c76e03b581d1c8b82fde561b1 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 04:08:34 +0200 Subject: [PATCH 48/60] Port `readLink()` --- std/file.d | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/std/file.d b/std/file.d index 9f9372bb5ea..508398a5976 100644 --- a/std/file.d +++ b/std/file.d @@ -4166,11 +4166,18 @@ version (Posix) @safe unittest $(LREF FileException) on error. +/ version (StdDdoc) string readLink(R)(R link) -if (isSomeFiniteCharInputRange!R || isConvertibleToString!R); +if (isSomeFiniteCharInputRange!R || isConvertibleToString!R || isDirEntry!R); else version (Posix) string readLink(R)(R link) -if (isSomeFiniteCharInputRange!R || isConvertibleToString!R) +if (isSomeFiniteCharInputRange!R || isConvertibleToString!R || isDirEntry!R) { - static if (isConvertibleToString!R) + static if (isDirEntry!R) + { + version (Windows) // hypothetically, currently ruled out by `version (Posix)` + return readLink(link.absoluteName); + else + return readLink(link.name); + } + else static if (isConvertibleToString!R) { return readLink!(convertToString!R)(link); } From 3bca43ca7089579de8196ef80aeaaf07c6bea27f Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 05:05:30 +0200 Subject: [PATCH 49/60] Port `relativePath()` and `asRelativePath()` --- std/file.d | 18 ++++++++++++++++++ std/path.d | 30 +++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/std/file.d b/std/file.d index 508398a5976..56ec8c200fc 100644 --- a/std/file.d +++ b/std/file.d @@ -262,6 +262,24 @@ version (Windows) @safe unittest }); }); + // Path determination test + runIn(root, { + import std.path : asRelativePath, relativePath, asNormalizedPath; + + const string relative = "1"; + const string absolute = absolutePath(relative); + const string expected = relativePath(absolute, nirvana); + + const entry = DirEntry(relative); + + runIn(nirvana, { + import std.algorithm.comparison : equal; + assert(equal( relativePath(entry ), expected)); + assert(equal( relativePath(entry, getcwd()), expected)); + assert(equal(asRelativePath(entry, getcwd()), expected)); + }); + }); + // Renaming tests runIn(root, { // string-based renaming diff --git a/std/path.d b/std/path.d index a627ea06e40..76a51ff7f89 100644 --- a/std/path.d +++ b/std/path.d @@ -2936,6 +2936,15 @@ string relativePath(CaseSensitive cs = CaseSensitive.osDefault) return asRelativePath!cs(path, baseVar).to!string; } +string relativePath(CaseSensitive cs = CaseSensitive.osDefault) + (const DirEntry path, lazy string base = getcwd()) +{ + version (Windows) + return relativePath!cs(path.absoluteName, base); + else + return relativePath!cs(path.name, base); +} + /// @safe unittest { @@ -3111,9 +3120,24 @@ auto asRelativePath(CaseSensitive cs = CaseSensitive.osDefault, R1, R2) (auto ref R1 path, auto ref R2 base) if (isConvertibleToString!R1 || isConvertibleToString!R2) { - import std.meta : staticMap; - alias Types = staticMap!(convertToString, R1, R2); - return asRelativePath!(cs, Types)(path, base); + enum r1IsDirEntry = is(Unconst!R1 == DirEntry); + + static if (r1IsDirEntry) + { + import std.meta : AliasSeq; + + alias Types = AliasSeq!(string, convertToString!R2); + version (Windows) + return asRelativePath!(cs, Types)(path.absoluteName, base); + else + return asRelativePath!(cs, Types)(path.name, base); + } + else + { + import std.meta : staticMap; + alias Types = staticMap!(convertToString, R1, R2); + return asRelativePath!(cs, Types)(path, base); + } } @safe unittest From 67de1e72232ab0678f9f96fbcf11466abf99dcc1 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 06:34:34 +0200 Subject: [PATCH 50/60] Port `buildNormalizedPath()` and `asNormalizedPath()` This documents a previously undocumented function. --- std/file.d | 26 ++++++++++++++++++++++++++ std/path.d | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index 56ec8c200fc..cb88f561850 100644 --- a/std/file.d +++ b/std/file.d @@ -280,6 +280,32 @@ version (Windows) @safe unittest }); }); + // Path normalization test + runIn(root, { + import std.path : asNormalizedPath, buildNormalizedPath; + import std.range : array; + + const string relative = "./3/4/../5/../5/6"; + assert(!isAbsolute(relative)); + + assert(!isAbsolute(buildNormalizedPath(relative))); + assert(!isAbsolute(asNormalizedPath(relative).array)); + + const entry = DirEntry(relative); + runIn(nirvana, { + import std.algorithm.comparison : equal; + + static immutable expected = buildNormalizedPath("../r/3/5/6"); + assert(buildNormalizedPath(entry) == expected); + assert(buildNormalizedPath(entry, "../6") == expected); + assert(buildNormalizedPath(entry, "../6", ".") == expected); + assert(equal(asNormalizedPath(entry), expected)); + + assert(!isAbsolute(buildNormalizedPath(entry))); + assert(!isAbsolute(asNormalizedPath(entry).array)); + }); + }); + // Renaming tests runIn(root, { // string-based renaming diff --git a/std/path.d b/std/path.d index 76a51ff7f89..57e5bc09436 100644 --- a/std/path.d +++ b/std/path.d @@ -1769,6 +1769,23 @@ if (isSomeChar!C) return result.array; } +/// ditto +immutable(char)[] buildNormalizedPath(const DirEntry path0, const(char[])[] paths...) + @safe +{ + version (Windows) + { + const name = path0.name; + if (name.isAbsolute) + return buildNormalizedPath!char(name ~ paths); + + const arg0 = relativePath(path0.absoluteName, getcwd()); + return buildNormalizedPath!char(arg0 ~ paths); + } + else + return buildNormalizedPath!char(arg0 ~ paths); +} + /// @safe unittest { @@ -1808,6 +1825,7 @@ if (isSomeChar!C) assert(buildNormalizedPath("", null) == ""); assert(buildNormalizedPath(null, "") == ""); assert(buildNormalizedPath!(char)(null, null) == ""); + assert(buildNormalizedPath!(char)() == ""); version (Posix) { @@ -2072,12 +2090,29 @@ if (isSomeChar!(ElementEncodingType!R) && } } +/// ditto auto asNormalizedPath(R)(return scope auto ref R path) -if (isConvertibleToString!R) +if (isConvertibleToString!R && !is(Unconst!R == DirEntry)) { return asNormalizedPath!(StringTypeOf!R)(path); } +/// ditto +auto asNormalizedPath(R)(return scope auto ref R path) +if (is(Unconst!R == DirEntry)) +{ + version (Windows) + { + const name = path.name; + if (name.isAbsolute) + return asNormalizedPath(name); + + return asNormalizedPath(relativePath(path.absoluteName, getcwd())); + } + else + return asNormalizedPath(path.name); +} + @safe unittest { assert(testAliasedString!asNormalizedPath(null)); From 7d1c4f689272fe516458635514bebf76493370dc Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 06:52:55 +0200 Subject: [PATCH 51/60] Add test for `dirEntries()` with `chdir()` calls --- std/file.d | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/std/file.d b/std/file.d index cb88f561850..ff55bfdeca5 100644 --- a/std/file.d +++ b/std/file.d @@ -817,6 +817,38 @@ version (Windows) @safe unittest assert(slurp!(int)(entry, "%d") == [10, 20]); }); }); + + // Mild chaos directory tree traversal test + runIn(root, { + auto iterator = ".".dirEntries(SpanMode.shallow); + + int found1 = 0; + int found3 = 0; + int foundOthers = 0; + + runIn(nirvana, { + foreach (DirEntry entry; iterator) + { + switch (entry.name[$-1]) + { + case '1': + ++found1; + break; + case '3': + ++found3; + break; + default: + ++foundOthers; + break; + } + chdir(parent); + } + }); + + assert(found1 == 1); + assert(found3 == 1); + assert(foundOthers == 0); + }); } // Purposefully not documented. Use at your own risk From 2ab938e7a5e239453ae80524b499bc54a2ce02f8 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Mon, 31 Mar 2025 06:53:20 +0200 Subject: [PATCH 52/60] Document unittest-internal `runIn` function --- std/file.d | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/std/file.d b/std/file.d index ff55bfdeca5..3d5045f0bf9 100644 --- a/std/file.d +++ b/std/file.d @@ -145,13 +145,21 @@ version (Windows) @safe unittest assert(lineNumberString!().to!size_t() == __LINE__); } + /++ + Saves the current working directory, + changes it to `dir` before executing `callback` + and eventually restores the original working directory. + + The provided callback is free to call `chdir()` as it needs. + This function will always restore the original working directory. + +/ static void runIn(string dir, void delegate() @safe callback) { const origWD = getcwd(); assert(origWD.isAbsolute); chdir(dir); - scope (exit) chdir(origWD); + scope (exit) chdir(origWD); // always(!) restore the original working directory callback(); } From e426eae6294b244a9cce052d9268397b6dc9b44a Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 4 Apr 2025 20:33:20 +0200 Subject: [PATCH 53/60] Add changelog entry --- changelog/direntries-ng_windows.dd | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 changelog/direntries-ng_windows.dd diff --git a/changelog/direntries-ng_windows.dd b/changelog/direntries-ng_windows.dd new file mode 100644 index 00000000000..f58b5768b5d --- /dev/null +++ b/changelog/direntries-ng_windows.dd @@ -0,0 +1,47 @@ +Increase more functions accept `DirEntry` on Windows. + +The `DirEntry` structure from `std.file` will now capture its absolute path +upon construction on Windows targets. + +Unlike POSIX systems, absolute paths are unproblematic on Windows, +as filesystem permissions are evaluated only on the node itself, not by +traversing the whole directory tree along the path. +This means, even if a user has no permission to access to `C:\some-dir\`, +they can still access `C:\some-dir\file` if sufficient permissions are granted +to them for the file entry itself. The same applies to directories. + +A good chunk of file-related Phobos functions are “made compatible” with +`DirEntry` through the implicit conversion mechanism provided by `alias this` +found in `DirEntry`. This allows `DirEntry` to become a `string` as needed. + +Unfortunately, this comes at the downside that functions can receive a relative +path that is not relative to the current working directory if it has changed +since the construction of the `DirEntry` handle. This does not always align +with the expectations of unsuspecting users — especially in the case where they +have passed a “fat” `DirEntry` handle instead of a raw path string. + +Not much can be done in the case where users convert the `DirEntry` into a +`string` on their own. When Phobos functions are called with an actual +`DirEntry`, however, the library can make use of an absolute path that has been +captured in advance and stored in the `DirEntry`. + +This describes exactly what has been changed. `std.file` and a number of +functions in `std.path` have been overloaded or modified to also accept a +`DirEntry` parameter in addition to the existing `string` path overloads. + +The old behavior can be easily restored either by manually converting a +`DirEntry` to `string` by casting or using the `name` property of said struct. + +While this was implemented to be as backwards compatible as reasonably +possible, in certain edge cases a simple adaption to user code might be +necessary. This is the case when users relied on the fact that a relative path +held by a `DirEntry` structure would always be relative to the current working +directory at the time of use. Additionally, rare meta programming code which +assumed that the `string` path overload of applicable functions would get +matched by `DirEntry` structures will have to be updated. +As outlined before, manually converting the `DirEntry` to `string` will do the +trick. + +Evidence for the confusing nature of the previous behavior can be found on +the issue tracker: +$(LINK2 https://github.com/dlang/phobos/issues/9584, #9584) From 5f1a5688c896e8b2b29055fece42ee9ff0212197 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 4 Apr 2025 20:42:44 +0200 Subject: [PATCH 54/60] Fix Posix branch of `buildNormalizedPath()` --- std/path.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/path.d b/std/path.d index 57e5bc09436..b2297590c85 100644 --- a/std/path.d +++ b/std/path.d @@ -1783,7 +1783,7 @@ immutable(char)[] buildNormalizedPath(const DirEntry path0, const(char[])[] path return buildNormalizedPath!char(arg0 ~ paths); } else - return buildNormalizedPath!char(arg0 ~ paths); + return buildNormalizedPath!char(path0 ~ paths); } /// From 9576d0d7da481f58c6356a3c0ca88f2c5a527d3f Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Fri, 4 Apr 2025 20:47:35 +0200 Subject: [PATCH 55/60] Add backwards compatibility test --- changelog/direntries-ng_windows.dd | 5 +++++ std/file.d | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/changelog/direntries-ng_windows.dd b/changelog/direntries-ng_windows.dd index f58b5768b5d..ca0d04892b7 100644 --- a/changelog/direntries-ng_windows.dd +++ b/changelog/direntries-ng_windows.dd @@ -45,3 +45,8 @@ trick. Evidence for the confusing nature of the previous behavior can be found on the issue tracker: $(LINK2 https://github.com/dlang/phobos/issues/9584, #9584) + +On POSIX the newly introduced overloads currently still emulate the old +behavior. This might be subject to change and should not be relied on. +In general, it is preferable to explicitly use the `name` property of +`DirEntry` in cases where the old behavior is desired. diff --git a/std/file.d b/std/file.d index 3d5045f0bf9..00abcfa6287 100644 --- a/std/file.d +++ b/std/file.d @@ -182,6 +182,19 @@ version (Windows) @safe unittest mkdirRecurse(root.buildPath("3", "4")); mkdirRecurse(root.buildPath("3", "5", "6")); + // DirEntry backwards compatibility test + runIn(root, { + runIn(nirvana, () { + foreach(entry; ".".dirEntries(SpanMode.shallow)) + { + // This does not make sense on its own but asserts that + // existing (quirky) code works like it did in the past. + assert(absolutePath(entry.name ) == nirvana.buildPath(entry.name)); + assert(absolutePath(cast(string) entry) == nirvana.buildPath(entry.name)); + } + }); + }); + // DirEntry existance test runIn(root, { auto entry = ".".dirEntries(SpanMode.shallow).front; From 7bee62fd38c8bf756b8a7e4bee263d8c0df31a69 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 6 Apr 2025 03:14:37 +0200 Subject: [PATCH 56/60] Port `dirEntries()` --- std/file.d | 102 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 17 deletions(-) diff --git a/std/file.d b/std/file.d index 00abcfa6287..db74209560a 100644 --- a/std/file.d +++ b/std/file.d @@ -127,6 +127,7 @@ else static assert(0); private enum isDirEntry(T) = is(Unconst!T == DirEntry); +private enum isString(T) = is(immutable T == immutable C[], C) && is(C == char); private enum isConvertibleToStringButNoDirEntry(T) = !isDirEntry!T && isConvertibleToString!T; version (Windows) @safe unittest @@ -4617,7 +4618,7 @@ version (StdDdoc) version (Windows) { - private this(string path, in WIN32_FIND_DATAW *fd); + private this(string name, string absolutePrefix, in WIN32_FIND_DATAW *fd); } else version (Posix) { @@ -4779,13 +4780,14 @@ else version (Windows) { import std.datetime.systime : FILETIMEToSysTime; - if (!path.exists()) + scope const effectivePath = (path == "") ? "." : path; + if (!effectivePath.exists()) throw new FileException(path, "File does not exist"); _name = path; this.absolutizeName(); - with (getFileAttributesWin(path)) + with (getFileAttributesWin(_name)) { _size = makeUlong(nFileSizeLow, nFileSizeHigh); _timeCreated = FILETIMEToSysTime(&ftCreationTime); @@ -4795,7 +4797,7 @@ else version (Windows) } } - private this(string path, WIN32_FIND_DATAW *fd) @trusted + private this(string path, string absolutePrefix, WIN32_FIND_DATAW *fd) @trusted { import core.stdc.wchar_ : wcslen; import std.conv : to; @@ -4812,13 +4814,22 @@ else version (Windows) _timeLastModified = FILETIMEToSysTime(&fd.ftLastWriteTime); _attributes = fd.dwFileAttributes; - this.absolutizeName(); + if (absolutePrefix is null) + this.absolutizeName(); + else + _absolutePrefix = absolutePrefix; } private void absolutizeName() pure return scope { import std.path : absolutePath; + if (_name == "") + { + _name = absolutePath("."); + _absolutePrefix = _name; + } + const rel = _name; alias abs = _name; _name = _name.absolutePath; @@ -5656,7 +5667,7 @@ private struct DirIteratorImpl DirEntry _cur; DirHandle[] _stack; DirEntry[] _stashed; //used in depth first mode - string _pathPrefix = null; + version (Windows) string _absolutePrefix = null; //stack helpers void pushExtra(DirEntry de) @@ -5704,6 +5715,11 @@ private struct DirIteratorImpl return toNext(false, &_findinfo); } + bool stepIn(const DirEntry directory) + { + return this.stepIn(directory.absoluteName); + } + bool next() { if (_stack.length == 0) @@ -5730,7 +5746,7 @@ private struct DirIteratorImpl popDirStack(); return false; } - _cur = DirEntry(_stack[$-1].dirpath, findinfo); + _cur = DirEntry(_stack[$-1].dirpath, _absolutePrefix, findinfo); return true; } @@ -5773,6 +5789,11 @@ private struct DirIteratorImpl return next(); } + bool stepIn(DirEntry directory) + { + return this.stepIn(directory.name); + } + bool next() @trusted { if (_stack.length == 0) @@ -5812,18 +5833,44 @@ private struct DirIteratorImpl } } - this(string pathname, SpanMode mode, bool followSymlink) + this(const DirEntry entry, SpanMode mode, bool followSymlink) { _mode = mode; _followSymlink = followSymlink; + version (Windows) + { + const pathname = entry.absoluteName; + _absolutePrefix = entry._absolutePrefix; + } + else + { + const pathname = entry.name; + } + + this.initialStepIn(pathname); + } + + version (Windows) { /* Leaving this overload available on Windows has the tendency to introduce regressions. */ } + else + { + this(string pathname, SpanMode mode, bool followSymlink) + { + _mode = mode; + _followSymlink = followSymlink; + this.initialStepIn(pathname); + } + } + + private void initialStepIn(string pathname) + { if (stepIn(pathname)) { if (_mode == SpanMode.depth) while (mayStepIn()) { auto thisDir = _cur; - if (stepIn(_cur.name)) + if (stepIn(_cur)) { pushExtra(thisDir); } @@ -5853,7 +5900,7 @@ private struct DirIteratorImpl while (mayStepIn()) { auto thisDir = _cur; - if (stepIn(_cur.name)) + if (stepIn(_cur)) { pushExtra(thisDir); } @@ -5867,7 +5914,7 @@ private struct DirIteratorImpl case SpanMode.breadth: if (mayStepIn()) { - if (!stepIn(_cur.name)) + if (!stepIn(_cur)) while (!empty && !next()){} } else @@ -5895,10 +5942,21 @@ struct _DirIterator(bool useDIP1000) private: SafeRefCounted!(DirIteratorImpl, RefCountedAutoInitialize.no) impl; - this(string pathname, SpanMode mode, bool followSymlink) @trusted + this(Path)(Path pathname, SpanMode mode, bool followSymlink) @trusted + if (isString!Path || isConvertibleToStringButNoDirEntry!Path) { - impl = typeof(impl)(pathname, mode, followSymlink); + version (Windows) + impl = typeof(impl)(DirEntry(pathname), mode, followSymlink); + else + impl = typeof(impl)(pathname, mode, followSymlink); } + + this(Path)(Path entry, SpanMode mode, bool followSymlink) @trusted + if (isDirEntry!Path) + { + impl = typeof(impl)(entry, mode, followSymlink); + } + public: @property bool empty() @trusted { return impl.empty; } @property DirEntry front() @trusted { return impl.front; } @@ -6016,8 +6074,17 @@ scan(""); // For some reason, doing the same alias-to-a-template trick as with DirIterator // does not work here. -auto dirEntries(bool useDIP1000 = dip1000Enabled) - (string path, SpanMode mode, bool followSymlink = true) +auto dirEntries(Path, bool useDIP1000 = dip1000Enabled) + (Path path, SpanMode mode, bool followSymlink = true) +if (isString!Path || isConvertibleToStringButNoDirEntry!Path) +{ + return _DirIterator!useDIP1000(path, mode, followSymlink); +} + +/// ditto +auto dirEntries(Path, bool useDIP1000 = dip1000Enabled) + (const Path path, SpanMode mode, bool followSymlink = true) +if (isDirEntry!Path) { return _DirIterator!useDIP1000(path, mode, followSymlink); } @@ -6128,9 +6195,10 @@ auto dirEntries(bool useDIP1000 = dip1000Enabled) } /// Ditto -auto dirEntries(bool useDIP1000 = dip1000Enabled) - (string path, string pattern, SpanMode mode, +auto dirEntries(Path, bool useDIP1000 = dip1000Enabled) + (Path path, string pattern, SpanMode mode, bool followSymlink = true) +if (isString!Path || isConvertibleToString!Path || isDirEntry!Path) { import std.algorithm.iteration : filter; import std.path : globMatch, baseName; From 69d8d24b12bedce607f7c5bb5a7269a6eaf700c3 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 6 Apr 2025 03:14:58 +0200 Subject: [PATCH 57/60] Enable `rmdirRecurse()` test --- std/file.d | 1 - 1 file changed, 1 deletion(-) diff --git a/std/file.d b/std/file.d index db74209560a..dd248545816 100644 --- a/std/file.d +++ b/std/file.d @@ -761,7 +761,6 @@ version (Windows) @safe unittest }); // Directory tree removal test - version (none) // TODO: port `dirEntries()` runIn(root, { import std.exception : assertThrown; From 90d2283c52b9ac93d10d8d91210166a581ca14c7 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 6 Apr 2025 03:24:58 +0200 Subject: [PATCH 58/60] Fix accidental match of an unexpected function overload --- std/file.d | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/std/file.d b/std/file.d index dd248545816..6e42c5b4d91 100644 --- a/std/file.d +++ b/std/file.d @@ -5698,7 +5698,8 @@ private struct DirIteratorImpl HANDLE h; } - bool stepIn(string directory) @safe + bool stepIn(Path)(Path directory) @safe + if (isString!Path) { import std.path : chainPath; auto searchPattern = chainPath(directory, "*.*"); @@ -5714,7 +5715,8 @@ private struct DirIteratorImpl return toNext(false, &_findinfo); } - bool stepIn(const DirEntry directory) + bool stepIn(Path)(Path directory) + if (isDirEntry!Path) { return this.stepIn(directory.absoluteName); } From a10ad0ea87f7f98b3f18a46212f17f268e1353c5 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sun, 6 Apr 2025 03:36:54 +0200 Subject: [PATCH 59/60] Add chaotic directory tree traversal test --- std/file.d | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/std/file.d b/std/file.d index 6e42c5b4d91..af4985d8218 100644 --- a/std/file.d +++ b/std/file.d @@ -870,6 +870,16 @@ version (Windows) @safe unittest assert(found3 == 1); assert(foundOthers == 0); }); + + // Chaotic directory tree traversal test + runIn(root, { + // + foreach (DirEntry entry; ".".dirEntries("*", SpanMode.shallow)) + if (entry.isDir) + foreach (DirEntry subEntry; entry.dirEntries("*", SpanMode.shallow)) + if (subEntry.isDir) + chdir(subEntry); + }); } // Purposefully not documented. Use at your own risk From fbfdce6172922c63e06601bdcc6163ca83d8ae19 Mon Sep 17 00:00:00 2001 From: Elias Batek Date: Sat, 19 Apr 2025 04:14:47 +0200 Subject: [PATCH 60/60] Revert "Temporarily deprecate `alias this` of `DirEntry`" This reverts commit 7484666757ce20426731b8ba4917aa0fc9e9d904. --- std/file.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/std/file.d b/std/file.d index af4985d8218..1254c1d9de9 100644 --- a/std/file.d +++ b/std/file.d @@ -4783,7 +4783,7 @@ else version (Windows) { @safe: public: - deprecated("0xEAB") alias name this; // TODO: undo temporary deprecation + alias name this; this(return scope string path) { @@ -4928,7 +4928,7 @@ else version (Posix) { @safe: public: - deprecated("0xEAB") alias name this; // TODO: undo temporary deprecation + alias name this; this(return scope string path) {