diff options
author | Iain Buclaw <ibuclaw@gcc.gnu.org> | 2018-10-28 19:51:47 +0000 |
---|---|---|
committer | Iain Buclaw <ibuclaw@gcc.gnu.org> | 2018-10-28 19:51:47 +0000 |
commit | b4c522fabd0df7be08882d2207df8b2765026110 (patch) | |
tree | b5ffc312b0a441c1ba24323152aec463fdbe5e9f /libphobos/src/std/path.d | |
parent | 01ce9e31a02c8039d88e90f983735104417bf034 (diff) | |
download | gcc-b4c522fabd0df7be08882d2207df8b2765026110.zip gcc-b4c522fabd0df7be08882d2207df8b2765026110.tar.gz gcc-b4c522fabd0df7be08882d2207df8b2765026110.tar.bz2 |
Add D front-end, libphobos library, and D2 testsuite.
ChangeLog:
* Makefile.def (target_modules): Add libphobos.
(flags_to_pass): Add GDC, GDCFLAGS, GDC_FOR_TARGET and
GDCFLAGS_FOR_TARGET.
(dependencies): Make libphobos depend on libatomic, libbacktrace
configure, and zlib configure.
(language): Add language d.
* Makefile.in: Rebuild.
* Makefile.tpl (BUILD_EXPORTS): Add GDC and GDCFLAGS.
(HOST_EXPORTS): Add GDC.
(POSTSTAGE1_HOST_EXPORTS): Add GDC and GDC_FOR_BUILD.
(BASE_TARGET_EXPORTS): Add GDC.
(GDC_FOR_BUILD, GDC, GDCFLAGS): New variables.
(GDC_FOR_TARGET, GDC_FLAGS_FOR_TARGET): New variables.
(EXTRA_HOST_FLAGS): Add GDC.
(STAGE1_FLAGS_TO_PASS): Add GDC.
(EXTRA_TARGET_FLAGS): Add GDC and GDCFLAGS.
* config-ml.in: Treat GDC and GDCFLAGS like other compiler/flag
environment variables.
* configure: Rebuild.
* configure.ac: Add target-libphobos to target_libraries. Set and
substitute GDC_FOR_BUILD and GDC_FOR_TARGET.
config/ChangeLog:
* multi.m4: Set GDC.
gcc/ChangeLog:
* Makefile.in (tm_d_file_list, tm_d_include_list): New variables.
(TM_D_H, D_TARGET_DEF, D_TARGET_H, D_TARGET_OBJS): New variables.
(tm_d.h, cs-tm_d.h, default-d.o): New rules.
(d/d-target-hooks-def.h, s-d-target-hooks-def-h): New rules.
(s-tm-texi): Also check timestamp on d-target.def.
(generated_files): Add TM_D_H and d-target-hooks-def.h.
(build/genhooks.o): Also depend on D_TARGET_DEF.
* config.gcc (tm_d_file, d_target_objs, target_has_targetdm): New
variables.
* config/aarch64/aarch64-d.c: New file.
* config/aarch64/aarch64-linux.h (GNU_USER_TARGET_D_CRITSEC_SIZE):
Define.
* config/aarch64/aarch64-protos.h (aarch64_d_target_versions): New
prototype.
* config/aarch64/aarch64.h (TARGET_D_CPU_VERSIONS): Define.
* config/aarch64/t-aarch64 (aarch64-d.o): New rule.
* config/arm/arm-d.c: New file.
* config/arm/arm-protos.h (arm_d_target_versions): New prototype.
* config/arm/arm.h (TARGET_D_CPU_VERSIONS): Define.
* config/arm/linux-eabi.h (EXTRA_TARGET_D_OS_VERSIONS): Define.
* config/arm/t-arm (arm-d.o): New rule.
* config/default-d.c: New file.
* config/glibc-d.c: New file.
* config/gnu.h (GNU_USER_TARGET_D_OS_VERSIONS): Define.
* config/i386/i386-d.c: New file.
* config/i386/i386-protos.h (ix86_d_target_versions): New prototype.
* config/i386/i386.h (TARGET_D_CPU_VERSIONS): Define.
* config/i386/linux-common.h (EXTRA_TARGET_D_OS_VERSIONS): Define.
(GNU_USER_TARGET_D_CRITSEC_SIZE): Define.
* config/i386/t-i386 (i386-d.o): New rule.
* config/kfreebsd-gnu.h (GNU_USER_TARGET_D_OS_VERSIONS): Define.
* config/kopensolaris-gnu.h (GNU_USER_TARGET_D_OS_VERSIONS): Define.
* config/linux-android.h (ANDROID_TARGET_D_OS_VERSIONS): Define.
* config/linux.h (GNU_USER_TARGET_D_OS_VERSIONS): Define.
* config/mips/linux-common.h (EXTRA_TARGET_D_OS_VERSIONS): Define.
* config/mips/mips-d.c: New file.
* config/mips/mips-protos.h (mips_d_target_versions): New prototype.
* config/mips/mips.h (TARGET_D_CPU_VERSIONS): Define.
* config/mips/t-mips (mips-d.o): New rule.
* config/powerpcspe/linux.h (GNU_USER_TARGET_D_OS_VERSIONS): Define.
* config/powerpcspe/linux64.h (GNU_USER_TARGET_D_OS_VERSIONS): Define.
* config/powerpcspe/powerpcspe-d.c: New file.
* config/powerpcspe/powerpcspe-protos.h (rs6000_d_target_versions):
New prototype.
* config/powerpcspe/powerpcspe.c (rs6000_output_function_epilogue):
Support GNU D by using 0 as the language type.
* config/powerpcspe/powerpcspe.h (TARGET_D_CPU_VERSIONS): Define.
* config/powerpcspe/t-powerpcspe (powerpcspe-d.o): New rule.
* config/riscv/riscv-d.c: New file.
* config/riscv/riscv-protos.h (riscv_d_target_versions): New
prototype.
* config/riscv/riscv.h (TARGET_D_CPU_VERSIONS): Define.
* config/riscv/t-riscv (riscv-d.o): New rule.
* config/rs6000/linux.h (GNU_USER_TARGET_D_OS_VERSIONS): Define.
* config/rs6000/linux64.h (GNU_USER_TARGET_D_OS_VERSIONS): Define.
* config/rs6000/rs6000-d.c: New file.
* config/rs6000/rs6000-protos.h (rs6000_d_target_versions): New
prototype.
* config/rs6000/rs6000.c (rs6000_output_function_epilogue):
Support GNU D by using 0 as the language type.
* config/rs6000/rs6000.h (TARGET_D_CPU_VERSIONS): Define.
* config/rs6000/t-rs6000 (rs6000-d.o): New rule.
* config/s390/s390-d.c: New file.
* config/s390/s390-protos.h (s390_d_target_versions): New prototype.
* config/s390/s390.h (TARGET_D_CPU_VERSIONS): Define.
* config/s390/t-s390 (s390-d.o): New rule.
* config/sparc/sparc-d.c: New file.
* config/sparc/sparc-protos.h (sparc_d_target_versions): New
prototype.
* config/sparc/sparc.h (TARGET_D_CPU_VERSIONS): Define.
* config/sparc/t-sparc (sparc-d.o): New rule.
* config/t-glibc (glibc-d.o): New rule.
* configure: Regenerated.
* configure.ac (tm_d_file): New variable.
(tm_d_file_list, tm_d_include_list, d_target_objs): Add substitutes.
* doc/contrib.texi (Contributors): Add self for the D frontend.
* doc/frontends.texi (G++ and GCC): Mention D as a supported language.
* doc/install.texi (Configuration): Mention libphobos as an option for
--enable-shared. Mention d as an option for --enable-languages.
(Testing): Mention check-d as a target.
* doc/invoke.texi (Overall Options): Mention .d, .dd, and .di as file
name suffixes. Mention d as a -x option.
* doc/sourcebuild.texi (Top Level): Mention libphobos.
* doc/standards.texi (Standards): Add section on D language.
* doc/tm.texi: Regenerated.
* doc/tm.texi.in: Add @node for D language and ABI, and @hook for
TARGET_CPU_VERSIONS, TARGET_D_OS_VERSIONS, and TARGET_D_CRITSEC_SIZE.
* dwarf2out.c (is_dlang): New function.
(gen_compile_unit_die): Use DW_LANG_D for D.
(declare_in_namespace): Return module die for D, instead of adding
extra declarations into the namespace.
(gen_namespace_die): Generate DW_TAG_module for D.
(gen_decl_die): Handle CONST_DECLSs for D.
(dwarf2out_decl): Likewise.
(prune_unused_types_walk_local_classes): Handle DW_tag_interface_type.
(prune_unused_types_walk): Handle DW_tag_interface_type same as other
kinds of aggregates.
* gcc.c (default_compilers): Add entries for .d, .dd and .di.
* genhooks.c: Include d/d-target.def.
gcc/po/ChangeLog:
* EXCLUDES: Add sources from d/dmd.
gcc/testsuite/ChangeLog:
* gcc.misc-tests/help.exp: Add D to option descriptions check.
* gdc.dg/asan/asan.exp: New file.
* gdc.dg/asan/gdc272.d: New test.
* gdc.dg/compilable.d: New test.
* gdc.dg/dg.exp: New file.
* gdc.dg/gdc254.d: New test.
* gdc.dg/gdc260.d: New test.
* gdc.dg/gdc270a.d: New test.
* gdc.dg/gdc270b.d: New test.
* gdc.dg/gdc282.d: New test.
* gdc.dg/gdc283.d: New test.
* gdc.dg/imports/gdc170.d: New test.
* gdc.dg/imports/gdc231.d: New test.
* gdc.dg/imports/gdc239.d: New test.
* gdc.dg/imports/gdc241a.d: New test.
* gdc.dg/imports/gdc241b.d: New test.
* gdc.dg/imports/gdc251a.d: New test.
* gdc.dg/imports/gdc251b.d: New test.
* gdc.dg/imports/gdc253.d: New test.
* gdc.dg/imports/gdc254a.d: New test.
* gdc.dg/imports/gdc256.d: New test.
* gdc.dg/imports/gdc27.d: New test.
* gdc.dg/imports/gdcpkg256/package.d: New test.
* gdc.dg/imports/runnable.d: New test.
* gdc.dg/link.d: New test.
* gdc.dg/lto/lto.exp: New file.
* gdc.dg/lto/ltotests_0.d: New test.
* gdc.dg/lto/ltotests_1.d: New test.
* gdc.dg/runnable.d: New test.
* gdc.dg/simd.d: New test.
* gdc.test/gdc-test.exp: New file.
* lib/gdc-dg.exp: New file.
* lib/gdc.exp: New file.
libphobos/ChangeLog:
* Makefile.am: New file.
* Makefile.in: New file.
* acinclude.m4: New file.
* aclocal.m4: New file.
* config.h.in: New file.
* configure: New file.
* configure.ac: New file.
* d_rules.am: New file.
* libdruntime/Makefile.am: New file.
* libdruntime/Makefile.in: New file.
* libdruntime/__entrypoint.di: New file.
* libdruntime/__main.di: New file.
* libdruntime/gcc/attribute.d: New file.
* libdruntime/gcc/backtrace.d: New file.
* libdruntime/gcc/builtins.d: New file.
* libdruntime/gcc/config.d.in: New file.
* libdruntime/gcc/deh.d: New file.
* libdruntime/gcc/libbacktrace.d.in: New file.
* libdruntime/gcc/unwind/arm.d: New file.
* libdruntime/gcc/unwind/arm_common.d: New file.
* libdruntime/gcc/unwind/c6x.d: New file.
* libdruntime/gcc/unwind/generic.d: New file.
* libdruntime/gcc/unwind/package.d: New file.
* libdruntime/gcc/unwind/pe.d: New file.
* m4/autoconf.m4: New file.
* m4/druntime.m4: New file.
* m4/druntime/cpu.m4: New file.
* m4/druntime/libraries.m4: New file.
* m4/druntime/os.m4: New file.
* m4/gcc_support.m4: New file.
* m4/gdc.m4: New file.
* m4/libtool.m4: New file.
* src/Makefile.am: New file.
* src/Makefile.in: New file.
* src/libgphobos.spec.in: New file.
* testsuite/Makefile.am: New file.
* testsuite/Makefile.in: New file.
* testsuite/config/default.exp: New file.
* testsuite/lib/libphobos-dg.exp: New file.
* testsuite/lib/libphobos.exp: New file.
* testsuite/testsuite_flags.in: New file.
From-SVN: r265573
Diffstat (limited to 'libphobos/src/std/path.d')
-rw-r--r-- | libphobos/src/std/path.d | 4115 |
1 files changed, 4115 insertions, 0 deletions
diff --git a/libphobos/src/std/path.d b/libphobos/src/std/path.d new file mode 100644 index 0000000..32870ea --- /dev/null +++ b/libphobos/src/std/path.d @@ -0,0 +1,4115 @@ +// Written in the D programming language. + +/** This module is used to manipulate _path strings. + + All functions, with the exception of $(LREF expandTilde) (and in some + cases $(LREF absolutePath) and $(LREF relativePath)), are pure + string manipulation functions; they don't depend on any state outside + the program, nor do they perform any actual file system actions. + This has the consequence that the module does not make any distinction + between a _path that points to a directory and a _path that points to a + file, and it does not know whether or not the object pointed to by the + _path actually exists in the file system. + To differentiate between these cases, use $(REF isDir, std,file) and + $(REF exists, std,file). + + Note that on Windows, both the backslash ($(D `\`)) and the slash ($(D `/`)) + are in principle valid directory separators. This module treats them + both on equal footing, but in cases where a $(I new) separator is + added, a backslash will be used. Furthermore, the $(LREF buildNormalizedPath) + function will replace all slashes with backslashes on that platform. + + In general, the functions in this module assume that the input paths + are well-formed. (That is, they should not contain invalid characters, + they should follow the file system's _path format, etc.) The result + of calling a function on an ill-formed _path is undefined. When there + is a chance that a _path or a file name is invalid (for instance, when it + has been input by the user), it may sometimes be desirable to use the + $(LREF isValidFilename) and $(LREF isValidPath) functions to check + this. + + Most functions do not perform any memory allocations, and if a string is + returned, it is usually a slice of an input string. If a function + allocates, this is explicitly mentioned in the documentation. + +$(SCRIPT inhibitQuickIndex = 1;) +$(DIVC quickindex, +$(BOOKTABLE, +$(TR $(TH Category) $(TH Functions)) +$(TR $(TD Normalization) $(TD + $(LREF absolutePath) + $(LREF asAbsolutePath) + $(LREF asNormalizedPath) + $(LREF asRelativePath) + $(LREF buildNormalizedPath) + $(LREF buildPath) + $(LREF chainPath) + $(LREF expandTilde) +)) +$(TR $(TD Partitioning) $(TD + $(LREF baseName) + $(LREF dirName) + $(LREF dirSeparator) + $(LREF driveName) + $(LREF pathSeparator) + $(LREF pathSplitter) + $(LREF relativePath) + $(LREF rootName) + $(LREF stripDrive) +)) +$(TR $(TD Validation) $(TD + $(LREF isAbsolute) + $(LREF isDirSeparator) + $(LREF isRooted) + $(LREF isValidFilename) + $(LREF isValidPath) +)) +$(TR $(TD Extension) $(TD + $(LREF defaultExtension) + $(LREF extension) + $(LREF setExtension) + $(LREF stripExtension) + $(LREF withDefaultExtension) + $(LREF withExtension) +)) +$(TR $(TD Other) $(TD + $(LREF filenameCharCmp) + $(LREF filenameCmp) + $(LREF globMatch) + $(LREF CaseSensitive) +)) +)) + + Authors: + Lars Tandle Kyllingstad, + $(HTTP digitalmars.com, Walter Bright), + Grzegorz Adam Hankiewicz, + Thomas K$(UUML)hne, + $(HTTP erdani.org, Andrei Alexandrescu) + Copyright: + Copyright (c) 2000-2014, the authors. All rights reserved. + License: + $(HTTP boost.org/LICENSE_1_0.txt, Boost License 1.0) + Source: + $(PHOBOSSRC std/_path.d) +*/ +module std.path; + + +// FIXME +import std.file; //: getcwd; +static import std.meta; +import std.range.primitives; +import std.traits; + +version (unittest) +{ +private: + struct TestAliasedString + { + string get() @safe @nogc pure nothrow { return _s; } + alias get this; + @disable this(this); + string _s; + } + + bool testAliasedString(alias func, Args...)(string s, Args args) + { + return func(TestAliasedString(s), args) == func(s, args); + } +} + +/** String used to separate directory names in a path. Under + POSIX this is a slash, under Windows a backslash. +*/ +version (Posix) enum string dirSeparator = "/"; +else version (Windows) enum string dirSeparator = "\\"; +else static assert(0, "unsupported platform"); + + + + +/** Path separator string. A colon under POSIX, a semicolon + under Windows. +*/ +version (Posix) enum string pathSeparator = ":"; +else version (Windows) enum string pathSeparator = ";"; +else static assert(0, "unsupported platform"); + + + + +/** Determines whether the given character is a directory separator. + + On Windows, this includes both $(D `\`) and $(D `/`). + On POSIX, it's just $(D `/`). +*/ +bool isDirSeparator(dchar c) @safe pure nothrow @nogc +{ + if (c == '/') return true; + version (Windows) if (c == '\\') return true; + return false; +} + + +/* Determines whether the given character is a drive separator. + + On Windows, this is true if c is the ':' character that separates + the drive letter from the rest of the path. On POSIX, this always + returns false. +*/ +private bool isDriveSeparator(dchar c) @safe pure nothrow @nogc +{ + version (Windows) return c == ':'; + else return false; +} + + +/* Combines the isDirSeparator and isDriveSeparator tests. */ +version (Windows) private bool isSeparator(dchar c) @safe pure nothrow @nogc +{ + return isDirSeparator(c) || isDriveSeparator(c); +} +version (Posix) private alias isSeparator = isDirSeparator; + + +/* Helper function that determines the position of the last + drive/directory separator in a string. Returns -1 if none + is found. +*/ +private ptrdiff_t lastSeparator(R)(R path) +if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || + isNarrowString!R) +{ + auto i = (cast(ptrdiff_t) path.length) - 1; + while (i >= 0 && !isSeparator(path[i])) --i; + return i; +} + + +version (Windows) +{ + private bool isUNC(R)(R path) + if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || + isNarrowString!R) + { + return path.length >= 3 && isDirSeparator(path[0]) && isDirSeparator(path[1]) + && !isDirSeparator(path[2]); + } + + private ptrdiff_t uncRootLength(R)(R path) + if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || + isNarrowString!R) + in { assert(isUNC(path)); } + body + { + ptrdiff_t i = 3; + while (i < path.length && !isDirSeparator(path[i])) ++i; + if (i < path.length) + { + auto j = i; + do { ++j; } while (j < path.length && isDirSeparator(path[j])); + if (j < path.length) + { + do { ++j; } while (j < path.length && !isDirSeparator(path[j])); + i = j; + } + } + return i; + } + + private bool hasDrive(R)(R path) + if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || + isNarrowString!R) + { + return path.length >= 2 && isDriveSeparator(path[1]); + } + + private bool isDriveRoot(R)(R path) + if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || + isNarrowString!R) + { + return path.length >= 3 && isDriveSeparator(path[1]) + && isDirSeparator(path[2]); + } +} + + +/* Helper functions that strip leading/trailing slashes and backslashes + from a path. +*/ +private auto ltrimDirSeparators(R)(R path) +if (isInputRange!R && !isInfinite!R && isSomeChar!(ElementType!R) || + isNarrowString!R) +{ + static if (isRandomAccessRange!R && hasSlicing!R || isNarrowString!R) + { + int i = 0; + while (i < path.length && isDirSeparator(path[i])) + ++i; + return path[i .. path.length]; + } + else + { + while (!path.empty && isDirSeparator(path.front)) + path.popFront(); + return path; + } +} + +@system unittest +{ + import std.array; + import std.utf : byDchar; + + assert(ltrimDirSeparators("//abc//").array == "abc//"); + assert(ltrimDirSeparators("//abc//"d).array == "abc//"d); + assert(ltrimDirSeparators("//abc//".byDchar).array == "abc//"d); +} + +private auto rtrimDirSeparators(R)(R path) +if (isBidirectionalRange!R && isSomeChar!(ElementType!R) || + isNarrowString!R) +{ + static if (isRandomAccessRange!R && hasSlicing!R && hasLength!R || isNarrowString!R) + { + auto i = (cast(ptrdiff_t) path.length) - 1; + while (i >= 0 && isDirSeparator(path[i])) + --i; + return path[0 .. i+1]; + } + else + { + while (!path.empty && isDirSeparator(path.back)) + path.popBack(); + return path; + } +} + +@system unittest +{ + import std.array; + import std.utf : byDchar; + + assert(rtrimDirSeparators("//abc//").array == "//abc"); + assert(rtrimDirSeparators("//abc//"d).array == "//abc"d); + + assert(rtrimDirSeparators(MockBiRange!char("//abc//")).array == "//abc"); +} + +private auto trimDirSeparators(R)(R path) +if (isBidirectionalRange!R && isSomeChar!(ElementType!R) || + isNarrowString!R) +{ + return ltrimDirSeparators(rtrimDirSeparators(path)); +} + +@system unittest +{ + import std.array; + import std.utf : byDchar; + + assert(trimDirSeparators("//abc//").array == "abc"); + assert(trimDirSeparators("//abc//"d).array == "abc"d); + + assert(trimDirSeparators(MockBiRange!char("//abc//")).array == "abc"); +} + + + + +/** This $(D enum) is used as a template argument to functions which + compare file names, and determines whether the comparison is + case sensitive or not. +*/ +enum CaseSensitive : bool +{ + /// File names are case insensitive + no = false, + + /// File names are case sensitive + yes = true, + + /** The default (or most common) setting for the current platform. + That is, $(D no) on Windows and Mac OS X, and $(D yes) on all + POSIX systems except OS X (Linux, *BSD, etc.). + */ + osDefault = osDefaultCaseSensitivity +} +version (Windows) private enum osDefaultCaseSensitivity = false; +else version (OSX) private enum osDefaultCaseSensitivity = false; +else version (Posix) private enum osDefaultCaseSensitivity = true; +else static assert(0); + + + + +/** + Params: + cs = Whether or not suffix matching is case-sensitive. + path = A path name. It can be a string, or any random-access range of + characters. + suffix = An optional suffix to be removed from the file name. + Returns: The name of the file in the path name, without any leading + directory and with an optional suffix chopped off. + + If $(D suffix) is specified, it will be compared to $(D path) + using $(D filenameCmp!cs), + where $(D cs) is an optional template parameter determining whether + the comparison is case sensitive or not. See the + $(LREF filenameCmp) documentation for details. + + Example: + --- + assert(baseName("dir/file.ext") == "file.ext"); + assert(baseName("dir/file.ext", ".ext") == "file"); + assert(baseName("dir/file.ext", ".xyz") == "file.ext"); + assert(baseName("dir/filename", "name") == "file"); + assert(baseName("dir/subdir/") == "subdir"); + + version (Windows) + { + assert(baseName(`d:file.ext`) == "file.ext"); + assert(baseName(`d:\dir\file.ext`) == "file.ext"); + } + --- + + Note: + This function $(I only) strips away the specified suffix, which + doesn't necessarily have to represent an extension. + To remove the extension from a path, regardless of what the extension + is, use $(LREF stripExtension). + To obtain the filename without leading directories and without + an extension, combine the functions like this: + --- + assert(baseName(stripExtension("dir/file.ext")) == "file"); + --- + + Standards: + This function complies with + $(LINK2 http://pubs.opengroup.org/onlinepubs/9699919799/utilities/basename.html, + the POSIX requirements for the 'basename' shell utility) + (with suitable adaptations for Windows paths). +*/ +auto baseName(R)(R path) +if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) && !isSomeString!R) +{ + return _baseName(path); +} + +/// ditto +auto baseName(C)(C[] path) +if (isSomeChar!C) +{ + return _baseName(path); +} + +private R _baseName(R)(R path) +if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || isNarrowString!R) +{ + auto p1 = stripDrive(path); + if (p1.empty) + { + version (Windows) if (isUNC(path)) + return path[0 .. 1]; + static if (isSomeString!R) + return null; + else + return p1; // which is empty + } + + auto p2 = rtrimDirSeparators(p1); + if (p2.empty) return p1[0 .. 1]; + + return p2[lastSeparator(p2)+1 .. p2.length]; +} + +/// ditto +inout(C)[] baseName(CaseSensitive cs = CaseSensitive.osDefault, C, C1) + (inout(C)[] path, in C1[] suffix) + @safe pure //TODO: nothrow (because of filenameCmp()) +if (isSomeChar!C && isSomeChar!C1) +{ + auto p = baseName(path); + if (p.length > suffix.length + && filenameCmp!cs(cast(const(C)[])p[$-suffix.length .. $], suffix) == 0) + { + return p[0 .. $-suffix.length]; + } + else return p; +} + +@safe unittest +{ + assert(baseName("").empty); + assert(baseName("file.ext"w) == "file.ext"); + assert(baseName("file.ext"d, ".ext") == "file"); + assert(baseName("file", "file"w.dup) == "file"); + assert(baseName("dir/file.ext"d.dup) == "file.ext"); + assert(baseName("dir/file.ext", ".ext"d) == "file"); + assert(baseName("dir/file"w, "file"d) == "file"); + assert(baseName("dir///subdir////") == "subdir"); + assert(baseName("dir/subdir.ext/", ".ext") == "subdir"); + assert(baseName("dir/subdir/".dup, "subdir") == "subdir"); + assert(baseName("/"w.dup) == "/"); + assert(baseName("//"d.dup) == "/"); + assert(baseName("///") == "/"); + + assert(baseName!(CaseSensitive.yes)("file.ext", ".EXT") == "file.ext"); + assert(baseName!(CaseSensitive.no)("file.ext", ".EXT") == "file"); + + { + auto r = MockRange!(immutable(char))(`dir/file.ext`); + auto s = r.baseName(); + foreach (i, c; `file`) + assert(s[i] == c); + } + + version (Windows) + { + assert(baseName(`dir\file.ext`) == `file.ext`); + assert(baseName(`dir\file.ext`, `.ext`) == `file`); + assert(baseName(`dir\file`, `file`) == `file`); + assert(baseName(`d:file.ext`) == `file.ext`); + assert(baseName(`d:file.ext`, `.ext`) == `file`); + assert(baseName(`d:file`, `file`) == `file`); + assert(baseName(`dir\\subdir\\\`) == `subdir`); + assert(baseName(`dir\subdir.ext\`, `.ext`) == `subdir`); + assert(baseName(`dir\subdir\`, `subdir`) == `subdir`); + assert(baseName(`\`) == `\`); + assert(baseName(`\\`) == `\`); + assert(baseName(`\\\`) == `\`); + assert(baseName(`d:\`) == `\`); + assert(baseName(`d:`).empty); + assert(baseName(`\\server\share\file`) == `file`); + assert(baseName(`\\server\share\`) == `\`); + assert(baseName(`\\server\share`) == `\`); + + auto r = MockRange!(immutable(char))(`\\server\share`); + auto s = r.baseName(); + foreach (i, c; `\`) + assert(s[i] == c); + } + + assert(baseName(stripExtension("dir/file.ext")) == "file"); + + static assert(baseName("dir/file.ext") == "file.ext"); + static assert(baseName("dir/file.ext", ".ext") == "file"); + + static struct DirEntry { string s; alias s this; } + assert(baseName(DirEntry("dir/file.ext")) == "file.ext"); +} + +@safe unittest +{ + assert(testAliasedString!baseName("file")); + + enum S : string { a = "file/path/to/test" } + assert(S.a.baseName == "test"); + + char[S.a.length] sa = S.a[]; + assert(sa.baseName == "test"); +} + +/** Returns the directory part of a path. On Windows, this + includes the drive letter if present. + + Params: + path = A path name. + + Returns: + A slice of $(D path) or ".". + + Standards: + This function complies with + $(LINK2 http://pubs.opengroup.org/onlinepubs/9699919799/utilities/dirname.html, + the POSIX requirements for the 'dirname' shell utility) + (with suitable adaptations for Windows paths). +*/ +auto dirName(R)(R path) +if (isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) && !isSomeString!R) +{ + return _dirName(path); +} + +/// ditto +auto dirName(C)(C[] path) +if (isSomeChar!C) +{ + return _dirName(path); +} + +private auto _dirName(R)(R path) +if (isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || + isNarrowString!R) +{ + static auto result(bool dot, typeof(path[0 .. 1]) p) + { + static if (isSomeString!R) + return dot ? "." : p; + else + { + import std.range : choose, only; + return choose(dot, only(cast(ElementEncodingType!R)'.'), p); + } + } + + if (path.empty) + return result(true, path[0 .. 0]); + + auto p = rtrimDirSeparators(path); + if (p.empty) + return result(false, path[0 .. 1]); + + version (Windows) + { + if (isUNC(p) && uncRootLength(p) == p.length) + return result(false, p); + + if (p.length == 2 && isDriveSeparator(p[1]) && path.length > 2) + return result(false, path[0 .. 3]); + } + + auto i = lastSeparator(p); + if (i == -1) + return result(true, p); + if (i == 0) + return result(false, p[0 .. 1]); + + version (Windows) + { + // If the directory part is either d: or d:\ + // do not chop off the last symbol. + if (isDriveSeparator(p[i]) || isDriveSeparator(p[i-1])) + return result(false, p[0 .. i+1]); + } + // Remove any remaining trailing (back)slashes. + return result(false, rtrimDirSeparators(p[0 .. i])); +} + +/// +@safe unittest +{ + assert(dirName("") == "."); + assert(dirName("file"w) == "."); + assert(dirName("dir/"d) == "."); + assert(dirName("dir///") == "."); + assert(dirName("dir/file"w.dup) == "dir"); + assert(dirName("dir///file"d.dup) == "dir"); + assert(dirName("dir/subdir/") == "dir"); + assert(dirName("/dir/file"w) == "/dir"); + assert(dirName("/file"d) == "/"); + assert(dirName("/") == "/"); + assert(dirName("///") == "/"); + + version (Windows) + { + assert(dirName(`dir\`) == `.`); + assert(dirName(`dir\\\`) == `.`); + assert(dirName(`dir\file`) == `dir`); + assert(dirName(`dir\\\file`) == `dir`); + assert(dirName(`dir\subdir\`) == `dir`); + assert(dirName(`\dir\file`) == `\dir`); + assert(dirName(`\file`) == `\`); + assert(dirName(`\`) == `\`); + assert(dirName(`\\\`) == `\`); + assert(dirName(`d:`) == `d:`); + assert(dirName(`d:file`) == `d:`); + assert(dirName(`d:\`) == `d:\`); + assert(dirName(`d:\file`) == `d:\`); + assert(dirName(`d:\dir\file`) == `d:\dir`); + assert(dirName(`\\server\share\dir\file`) == `\\server\share\dir`); + assert(dirName(`\\server\share\file`) == `\\server\share`); + assert(dirName(`\\server\share\`) == `\\server\share`); + assert(dirName(`\\server\share`) == `\\server\share`); + } +} + +@safe unittest +{ + assert(testAliasedString!dirName("file")); + + enum S : string { a = "file/path/to/test" } + assert(S.a.dirName == "file/path/to"); + + char[S.a.length] sa = S.a[]; + assert(sa.dirName == "file/path/to"); +} + +@system unittest +{ + static assert(dirName("dir/file") == "dir"); + + import std.array; + import std.utf : byChar, byWchar, byDchar; + + assert(dirName("".byChar).array == "."); + assert(dirName("file"w.byWchar).array == "."w); + assert(dirName("dir/"d.byDchar).array == "."d); + assert(dirName("dir///".byChar).array == "."); + assert(dirName("dir/subdir/".byChar).array == "dir"); + assert(dirName("/dir/file"w.byWchar).array == "/dir"w); + assert(dirName("/file"d.byDchar).array == "/"d); + assert(dirName("/".byChar).array == "/"); + assert(dirName("///".byChar).array == "/"); + + version (Windows) + { + assert(dirName(`dir\`.byChar).array == `.`); + assert(dirName(`dir\\\`.byChar).array == `.`); + assert(dirName(`dir\file`.byChar).array == `dir`); + assert(dirName(`dir\\\file`.byChar).array == `dir`); + assert(dirName(`dir\subdir\`.byChar).array == `dir`); + assert(dirName(`\dir\file`.byChar).array == `\dir`); + assert(dirName(`\file`.byChar).array == `\`); + assert(dirName(`\`.byChar).array == `\`); + assert(dirName(`\\\`.byChar).array == `\`); + assert(dirName(`d:`.byChar).array == `d:`); + assert(dirName(`d:file`.byChar).array == `d:`); + assert(dirName(`d:\`.byChar).array == `d:\`); + assert(dirName(`d:\file`.byChar).array == `d:\`); + assert(dirName(`d:\dir\file`.byChar).array == `d:\dir`); + assert(dirName(`\\server\share\dir\file`.byChar).array == `\\server\share\dir`); + assert(dirName(`\\server\share\file`) == `\\server\share`); + assert(dirName(`\\server\share\`.byChar).array == `\\server\share`); + assert(dirName(`\\server\share`.byChar).array == `\\server\share`); + } + + //static assert(dirName("dir/file".byChar).array == "dir"); +} + + + + +/** Returns the root directory of the specified path, or $(D null) if the + path is not rooted. + + Params: + path = A path name. + + Returns: + A slice of $(D path). +*/ +auto rootName(R)(R path) +if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || + isNarrowString!R) && + !isConvertibleToString!R) +{ + if (path.empty) + goto Lnull; + + version (Posix) + { + if (isDirSeparator(path[0])) return path[0 .. 1]; + } + else version (Windows) + { + if (isDirSeparator(path[0])) + { + if (isUNC(path)) return path[0 .. uncRootLength(path)]; + else return path[0 .. 1]; + } + else if (path.length >= 3 && isDriveSeparator(path[1]) && + isDirSeparator(path[2])) + { + return path[0 .. 3]; + } + } + else static assert(0, "unsupported platform"); + + assert(!isRooted(path)); +Lnull: + static if (is(StringTypeOf!R)) + return null; // legacy code may rely on null return rather than slice + else + return path[0 .. 0]; +} + +/// +@safe unittest +{ + assert(rootName("") is null); + assert(rootName("foo") is null); + assert(rootName("/") == "/"); + assert(rootName("/foo/bar") == "/"); + + version (Windows) + { + assert(rootName("d:foo") is null); + assert(rootName(`d:\foo`) == `d:\`); + assert(rootName(`\\server\share\foo`) == `\\server\share`); + assert(rootName(`\\server\share`) == `\\server\share`); + } +} + +@safe unittest +{ + assert(testAliasedString!rootName("/foo/bar")); +} + +@safe unittest +{ + import std.array; + import std.utf : byChar; + + assert(rootName("".byChar).array == ""); + assert(rootName("foo".byChar).array == ""); + assert(rootName("/".byChar).array == "/"); + assert(rootName("/foo/bar".byChar).array == "/"); + + version (Windows) + { + assert(rootName("d:foo".byChar).array == ""); + assert(rootName(`d:\foo`.byChar).array == `d:\`); + assert(rootName(`\\server\share\foo`.byChar).array == `\\server\share`); + assert(rootName(`\\server\share`.byChar).array == `\\server\share`); + } +} + +auto rootName(R)(R path) +if (isConvertibleToString!R) +{ + return rootName!(StringTypeOf!R)(path); +} + + +/** + Get the drive portion of a path. + + Params: + path = string or range of characters + + Returns: + A slice of $(D _path) that is the drive, or an empty range if the drive + is not specified. In the case of UNC paths, the network share + is returned. + + Always returns an empty range on POSIX. +*/ +auto driveName(R)(R path) +if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || + isNarrowString!R) && + !isConvertibleToString!R) +{ + version (Windows) + { + if (hasDrive(path)) + return path[0 .. 2]; + else if (isUNC(path)) + return path[0 .. uncRootLength(path)]; + } + static if (isSomeString!R) + return cast(ElementEncodingType!R[]) null; // legacy code may rely on null return rather than slice + else + return path[0 .. 0]; +} + +/// +@safe unittest +{ + import std.range : empty; + version (Posix) assert(driveName("c:/foo").empty); + version (Windows) + { + assert(driveName(`dir\file`).empty); + assert(driveName(`d:file`) == "d:"); + assert(driveName(`d:\file`) == "d:"); + assert(driveName("d:") == "d:"); + assert(driveName(`\\server\share\file`) == `\\server\share`); + assert(driveName(`\\server\share\`) == `\\server\share`); + assert(driveName(`\\server\share`) == `\\server\share`); + + static assert(driveName(`d:\file`) == "d:"); + } +} + +auto driveName(R)(auto ref R path) +if (isConvertibleToString!R) +{ + return driveName!(StringTypeOf!R)(path); +} + +@safe unittest +{ + assert(testAliasedString!driveName(`d:\file`)); +} + +@safe unittest +{ + import std.array; + import std.utf : byChar; + + version (Posix) assert(driveName("c:/foo".byChar).empty); + version (Windows) + { + assert(driveName(`dir\file`.byChar).empty); + assert(driveName(`d:file`.byChar).array == "d:"); + assert(driveName(`d:\file`.byChar).array == "d:"); + assert(driveName("d:".byChar).array == "d:"); + assert(driveName(`\\server\share\file`.byChar).array == `\\server\share`); + assert(driveName(`\\server\share\`.byChar).array == `\\server\share`); + assert(driveName(`\\server\share`.byChar).array == `\\server\share`); + + static assert(driveName(`d:\file`).array == "d:"); + } +} + + +/** Strips the drive from a Windows path. On POSIX, the path is returned + unaltered. + + Params: + path = A pathname + + Returns: A slice of path without the drive component. +*/ +auto stripDrive(R)(R path) +if ((isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || + isNarrowString!R) && + !isConvertibleToString!R) +{ + version (Windows) + { + if (hasDrive!(BaseOf!R)(path)) return path[2 .. path.length]; + else if (isUNC!(BaseOf!R)(path)) return path[uncRootLength!(BaseOf!R)(path) .. path.length]; + } + return path; +} + +/// +@safe unittest +{ + version (Windows) + { + assert(stripDrive(`d:\dir\file`) == `\dir\file`); + assert(stripDrive(`\\server\share\dir\file`) == `\dir\file`); + } +} + +auto stripDrive(R)(auto ref R path) +if (isConvertibleToString!R) +{ + return stripDrive!(StringTypeOf!R)(path); +} + +@safe unittest +{ + assert(testAliasedString!stripDrive(`d:\dir\file`)); +} + +@safe unittest +{ + version (Windows) + { + assert(stripDrive(`d:\dir\file`) == `\dir\file`); + assert(stripDrive(`\\server\share\dir\file`) == `\dir\file`); + static assert(stripDrive(`d:\dir\file`) == `\dir\file`); + + auto r = MockRange!(immutable(char))(`d:\dir\file`); + auto s = r.stripDrive(); + foreach (i, c; `\dir\file`) + assert(s[i] == c); + } + version (Posix) + { + assert(stripDrive(`d:\dir\file`) == `d:\dir\file`); + + auto r = MockRange!(immutable(char))(`d:\dir\file`); + auto s = r.stripDrive(); + foreach (i, c; `d:\dir\file`) + assert(s[i] == c); + } +} + + +/* Helper function that returns the position of the filename/extension + separator dot in path. + + Params: + path = file spec as string or indexable range + Returns: + index of extension separator (the dot), or -1 if not found +*/ +private ptrdiff_t extSeparatorPos(R)(const R path) +if (isRandomAccessRange!R && hasLength!R && isSomeChar!(ElementType!R) || + isNarrowString!R) +{ + for (auto i = path.length; i-- > 0 && !isSeparator(path[i]); ) + { + if (path[i] == '.' && i > 0 && !isSeparator(path[i-1])) + return i; + } + return -1; +} + +@safe unittest +{ + assert(extSeparatorPos("file") == -1); + assert(extSeparatorPos("file.ext"w) == 4); + assert(extSeparatorPos("file.ext1.ext2"d) == 9); + assert(extSeparatorPos(".foo".dup) == -1); + assert(extSeparatorPos(".foo.ext"w.dup) == 4); +} + +@safe unittest +{ + assert(extSeparatorPos("dir/file"d.dup) == -1); + assert(extSeparatorPos("dir/file.ext") == 8); + assert(extSeparatorPos("dir/file.ext1.ext2"w) == 13); + assert(extSeparatorPos("dir/.foo"d) == -1); + assert(extSeparatorPos("dir/.foo.ext".dup) == 8); + + version (Windows) + { + assert(extSeparatorPos("dir\\file") == -1); + assert(extSeparatorPos("dir\\file.ext") == 8); + assert(extSeparatorPos("dir\\file.ext1.ext2") == 13); + assert(extSeparatorPos("dir\\.foo") == -1); + assert(extSeparatorPos("dir\\.foo.ext") == 8); + + assert(extSeparatorPos("d:file") == -1); + assert(extSeparatorPos("d:file.ext") == 6); + assert(extSeparatorPos("d:file.ext1.ext2") == 11); + assert(extSeparatorPos("d:.foo") == -1); + assert(extSeparatorPos("d:.foo.ext") == 6); + } + + static assert(extSeparatorPos("file") == -1); + static assert(extSeparatorPos("file.ext"w) == 4); +} + + +/** + Params: path = A path name. + Returns: The _extension part of a file name, including the dot. + + If there is no _extension, $(D null) is returned. +*/ +auto extension(R)(R path) +if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || + is(StringTypeOf!R)) +{ + auto i = extSeparatorPos!(BaseOf!R)(path); + if (i == -1) + { + static if (is(StringTypeOf!R)) + return StringTypeOf!R.init[]; // which is null + else + return path[0 .. 0]; + } + else return path[i .. path.length]; +} + +/// +@safe unittest +{ + import std.range : empty; + assert(extension("file").empty); + assert(extension("file.") == "."); + assert(extension("file.ext"w) == ".ext"); + assert(extension("file.ext1.ext2"d) == ".ext2"); + assert(extension(".foo".dup).empty); + assert(extension(".foo.ext"w.dup) == ".ext"); + + static assert(extension("file").empty); + static assert(extension("file.ext") == ".ext"); +} + +@safe unittest +{ + { + auto r = MockRange!(immutable(char))(`file.ext1.ext2`); + auto s = r.extension(); + foreach (i, c; `.ext2`) + assert(s[i] == c); + } + + static struct DirEntry { string s; alias s this; } + assert(extension(DirEntry("file")).empty); +} + + +/** Remove extension from path. + + Params: + path = string or range to be sliced + + Returns: + slice of path with the extension (if any) stripped off +*/ +auto stripExtension(R)(R path) +if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || + isNarrowString!R) && + !isConvertibleToString!R) +{ + auto i = extSeparatorPos(path); + return (i == -1) ? path : path[0 .. i]; +} + +/// +@safe unittest +{ + assert(stripExtension("file") == "file"); + assert(stripExtension("file.ext") == "file"); + assert(stripExtension("file.ext1.ext2") == "file.ext1"); + assert(stripExtension("file.") == "file"); + assert(stripExtension(".file") == ".file"); + assert(stripExtension(".file.ext") == ".file"); + assert(stripExtension("dir/file.ext") == "dir/file"); +} + +auto stripExtension(R)(auto ref R path) +if (isConvertibleToString!R) +{ + return stripExtension!(StringTypeOf!R)(path); +} + +@safe unittest +{ + assert(testAliasedString!stripExtension("file")); +} + +@safe unittest +{ + assert(stripExtension("file.ext"w) == "file"); + assert(stripExtension("file.ext1.ext2"d) == "file.ext1"); + + import std.array; + import std.utf : byChar, byWchar, byDchar; + + assert(stripExtension("file".byChar).array == "file"); + assert(stripExtension("file.ext"w.byWchar).array == "file"); + assert(stripExtension("file.ext1.ext2"d.byDchar).array == "file.ext1"); +} + + +/** Sets or replaces an extension. + + If the filename already has an extension, it is replaced. If not, the + extension is simply appended to the filename. Including a leading dot + in $(D ext) is optional. + + If the extension is empty, this function is equivalent to + $(LREF stripExtension). + + This function normally allocates a new string (the possible exception + being the case when path is immutable and doesn't already have an + extension). + + Params: + path = A path name + ext = The new extension + + Returns: A string containing the _path given by $(D path), but where + the extension has been set to $(D ext). + + See_Also: + $(LREF withExtension) which does not allocate and returns a lazy range. +*/ +immutable(Unqual!C1)[] setExtension(C1, C2)(in C1[] path, in C2[] ext) +if (isSomeChar!C1 && !is(C1 == immutable) && is(Unqual!C1 == Unqual!C2)) +{ + try + { + import std.conv : to; + return withExtension(path, ext).to!(typeof(return)); + } + catch (Exception e) + { + assert(0); + } +} + +///ditto +immutable(C1)[] setExtension(C1, C2)(immutable(C1)[] path, const(C2)[] ext) +if (isSomeChar!C1 && is(Unqual!C1 == Unqual!C2)) +{ + if (ext.length == 0) + return stripExtension(path); + + try + { + import std.conv : to; + return withExtension(path, ext).to!(typeof(return)); + } + catch (Exception e) + { + assert(0); + } +} + +/// +@safe unittest +{ + assert(setExtension("file", "ext") == "file.ext"); + assert(setExtension("file"w, ".ext"w) == "file.ext"); + assert(setExtension("file."d, "ext"d) == "file.ext"); + assert(setExtension("file.", ".ext") == "file.ext"); + assert(setExtension("file.old"w, "new"w) == "file.new"); + assert(setExtension("file.old"d, ".new"d) == "file.new"); +} + +@safe unittest +{ + assert(setExtension("file"w.dup, "ext"w) == "file.ext"); + assert(setExtension("file"w.dup, ".ext"w) == "file.ext"); + assert(setExtension("file."w, "ext"w.dup) == "file.ext"); + assert(setExtension("file."w, ".ext"w.dup) == "file.ext"); + assert(setExtension("file.old"d.dup, "new"d) == "file.new"); + assert(setExtension("file.old"d.dup, ".new"d) == "file.new"); + + static assert(setExtension("file", "ext") == "file.ext"); + static assert(setExtension("file.old", "new") == "file.new"); + + static assert(setExtension("file"w.dup, "ext"w) == "file.ext"); + static assert(setExtension("file.old"d.dup, "new"d) == "file.new"); + + // Issue 10601 + assert(setExtension("file", "") == "file"); + assert(setExtension("file.ext", "") == "file"); +} + +/************ + * Replace existing extension on filespec with new one. + * + * Params: + * path = string or random access range representing a filespec + * ext = the new extension + * Returns: + * Range with $(D path)'s extension (if any) replaced with $(D ext). + * The element encoding type of the returned range will be the same as $(D path)'s. + * See_Also: + * $(LREF setExtension) + */ +auto withExtension(R, C)(R path, C[] ext) +if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || + isNarrowString!R) && + !isConvertibleToString!R && + isSomeChar!C) +{ + import std.range : only, chain; + import std.utf : byUTF; + + alias CR = Unqual!(ElementEncodingType!R); + auto dot = only(CR('.')); + if (ext.length == 0 || ext[0] == '.') + dot.popFront(); // so dot is an empty range, too + return chain(stripExtension(path).byUTF!CR, dot, ext.byUTF!CR); +} + +/// +@safe unittest +{ + import std.array; + assert(withExtension("file", "ext").array == "file.ext"); + assert(withExtension("file"w, ".ext"w).array == "file.ext"); + assert(withExtension("file.ext"w, ".").array == "file."); + + import std.utf : byChar, byWchar; + assert(withExtension("file".byChar, "ext").array == "file.ext"); + assert(withExtension("file"w.byWchar, ".ext"w).array == "file.ext"w); + assert(withExtension("file.ext"w.byWchar, ".").array == "file."w); +} + +auto withExtension(R, C)(auto ref R path, C[] ext) +if (isConvertibleToString!R) +{ + return withExtension!(StringTypeOf!R)(path, ext); +} + +@safe unittest +{ + assert(testAliasedString!withExtension("file", "ext")); +} + +/** Params: + path = A path name. + ext = The default extension to use. + + Returns: The _path given by $(D path), with the extension given by $(D ext) + appended if the path doesn't already have one. + + Including the dot in the extension is optional. + + This function always allocates a new string, except in the case when + path is immutable and already has an extension. +*/ +immutable(Unqual!C1)[] defaultExtension(C1, C2)(in C1[] path, in C2[] ext) +if (isSomeChar!C1 && is(Unqual!C1 == Unqual!C2)) +{ + import std.conv : to; + return withDefaultExtension(path, ext).to!(typeof(return)); +} + +/// +@safe unittest +{ + assert(defaultExtension("file", "ext") == "file.ext"); + assert(defaultExtension("file", ".ext") == "file.ext"); + assert(defaultExtension("file.", "ext") == "file."); + assert(defaultExtension("file.old", "new") == "file.old"); + assert(defaultExtension("file.old", ".new") == "file.old"); +} + +@safe unittest +{ + assert(defaultExtension("file"w.dup, "ext"w) == "file.ext"); + assert(defaultExtension("file.old"d.dup, "new"d) == "file.old"); + + static assert(defaultExtension("file", "ext") == "file.ext"); + static assert(defaultExtension("file.old", "new") == "file.old"); + + static assert(defaultExtension("file"w.dup, "ext"w) == "file.ext"); + static assert(defaultExtension("file.old"d.dup, "new"d) == "file.old"); +} + + +/******************************** + * Set the extension of $(D path) to $(D ext) if $(D path) doesn't have one. + * + * Params: + * path = filespec as string or range + * ext = extension, may have leading '.' + * Returns: + * range with the result + */ +auto withDefaultExtension(R, C)(R path, C[] ext) +if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || + isNarrowString!R) && + !isConvertibleToString!R && + isSomeChar!C) +{ + import std.range : only, chain; + import std.utf : byUTF; + + alias CR = Unqual!(ElementEncodingType!R); + auto dot = only(CR('.')); + auto i = extSeparatorPos(path); + if (i == -1) + { + if (ext.length > 0 && ext[0] == '.') + ext = ext[1 .. $]; // remove any leading . from ext[] + } + else + { + // path already has an extension, so make these empty + ext = ext[0 .. 0]; + dot.popFront(); + } + return chain(path.byUTF!CR, dot, ext.byUTF!CR); +} + +/// +@safe unittest +{ + import std.array; + assert(withDefaultExtension("file", "ext").array == "file.ext"); + assert(withDefaultExtension("file"w, ".ext").array == "file.ext"w); + assert(withDefaultExtension("file.", "ext").array == "file."); + assert(withDefaultExtension("file", "").array == "file."); + + import std.utf : byChar, byWchar; + assert(withDefaultExtension("file".byChar, "ext").array == "file.ext"); + assert(withDefaultExtension("file"w.byWchar, ".ext").array == "file.ext"w); + assert(withDefaultExtension("file.".byChar, "ext"d).array == "file."); + assert(withDefaultExtension("file".byChar, "").array == "file."); +} + +auto withDefaultExtension(R, C)(auto ref R path, C[] ext) +if (isConvertibleToString!R) +{ + return withDefaultExtension!(StringTypeOf!R, C)(path, ext); +} + +@safe unittest +{ + assert(testAliasedString!withDefaultExtension("file", "ext")); +} + +/** Combines one or more path segments. + + This function takes a set of path segments, given as an input + range of string elements or as a set of string arguments, + and concatenates them with each other. Directory separators + are inserted between segments if necessary. If any of the + path segments are absolute (as defined by $(LREF isAbsolute)), the + preceding segments will be dropped. + + On Windows, if one of the path segments are rooted, but not absolute + (e.g. $(D `\foo`)), all preceding path segments down to the previous + root will be dropped. (See below for an example.) + + This function always allocates memory to hold the resulting path. + The variadic overload is guaranteed to only perform a single + allocation, as is the range version if $(D paths) is a forward + range. + + Params: + segments = An input range of segments to assemble the path from. + Returns: The assembled path. +*/ +immutable(ElementEncodingType!(ElementType!Range))[] + buildPath(Range)(Range segments) + if (isInputRange!Range && !isInfinite!Range && isSomeString!(ElementType!Range)) +{ + if (segments.empty) return null; + + // If this is a forward range, we can pre-calculate a maximum length. + static if (isForwardRange!Range) + { + auto segments2 = segments.save; + size_t precalc = 0; + foreach (segment; segments2) precalc += segment.length + 1; + } + // Otherwise, just venture a guess and resize later if necessary. + else size_t precalc = 255; + + auto buf = new Unqual!(ElementEncodingType!(ElementType!Range))[](precalc); + size_t pos = 0; + foreach (segment; segments) + { + if (segment.empty) continue; + static if (!isForwardRange!Range) + { + immutable neededLength = pos + segment.length + 1; + if (buf.length < neededLength) + buf.length = reserve(buf, neededLength + buf.length/2); + } + auto r = chainPath(buf[0 .. pos], segment); + size_t i; + foreach (c; r) + { + buf[i] = c; + ++i; + } + pos = i; + } + static U trustedCast(U, V)(V v) @trusted pure nothrow { return cast(U) v; } + return trustedCast!(typeof(return))(buf[0 .. pos]); +} + +/// ditto +immutable(C)[] buildPath(C)(const(C)[][] paths...) + @safe pure nothrow +if (isSomeChar!C) +{ + return buildPath!(typeof(paths))(paths); +} + +/// +@safe unittest +{ + version (Posix) + { + assert(buildPath("foo", "bar", "baz") == "foo/bar/baz"); + assert(buildPath("/foo/", "bar/baz") == "/foo/bar/baz"); + assert(buildPath("/foo", "/bar") == "/bar"); + } + + version (Windows) + { + assert(buildPath("foo", "bar", "baz") == `foo\bar\baz`); + assert(buildPath(`c:\foo`, `bar\baz`) == `c:\foo\bar\baz`); + assert(buildPath("foo", `d:\bar`) == `d:\bar`); + assert(buildPath("foo", `\bar`) == `\bar`); + assert(buildPath(`c:\foo`, `\bar`) == `c:\bar`); + } +} + +@system unittest // non-documented +{ + import std.range; + // ir() wraps an array in a plain (i.e. non-forward) input range, so that + // we can test both code paths + InputRange!(C[]) ir(C)(C[][] p...) { return inputRangeObject(p); } + version (Posix) + { + assert(buildPath("foo") == "foo"); + assert(buildPath("/foo/") == "/foo/"); + assert(buildPath("foo", "bar") == "foo/bar"); + assert(buildPath("foo", "bar", "baz") == "foo/bar/baz"); + assert(buildPath("foo/".dup, "bar") == "foo/bar"); + assert(buildPath("foo///", "bar".dup) == "foo///bar"); + assert(buildPath("/foo"w, "bar"w) == "/foo/bar"); + assert(buildPath("foo"w.dup, "/bar"w) == "/bar"); + assert(buildPath("foo"w, "bar/"w.dup) == "foo/bar/"); + assert(buildPath("/"d, "foo"d) == "/foo"); + assert(buildPath(""d.dup, "foo"d) == "foo"); + assert(buildPath("foo"d, ""d.dup) == "foo"); + assert(buildPath("foo", "bar".dup, "baz") == "foo/bar/baz"); + assert(buildPath("foo"w, "/bar"w, "baz"w.dup) == "/bar/baz"); + + static assert(buildPath("foo", "bar", "baz") == "foo/bar/baz"); + static assert(buildPath("foo", "/bar", "baz") == "/bar/baz"); + + // The following are mostly duplicates of the above, except that the + // range version does not accept mixed constness. + assert(buildPath(ir("foo")) == "foo"); + assert(buildPath(ir("/foo/")) == "/foo/"); + assert(buildPath(ir("foo", "bar")) == "foo/bar"); + assert(buildPath(ir("foo", "bar", "baz")) == "foo/bar/baz"); + assert(buildPath(ir("foo/".dup, "bar".dup)) == "foo/bar"); + assert(buildPath(ir("foo///".dup, "bar".dup)) == "foo///bar"); + assert(buildPath(ir("/foo"w, "bar"w)) == "/foo/bar"); + assert(buildPath(ir("foo"w.dup, "/bar"w.dup)) == "/bar"); + assert(buildPath(ir("foo"w.dup, "bar/"w.dup)) == "foo/bar/"); + assert(buildPath(ir("/"d, "foo"d)) == "/foo"); + assert(buildPath(ir(""d.dup, "foo"d.dup)) == "foo"); + assert(buildPath(ir("foo"d, ""d)) == "foo"); + assert(buildPath(ir("foo", "bar", "baz")) == "foo/bar/baz"); + assert(buildPath(ir("foo"w.dup, "/bar"w.dup, "baz"w.dup)) == "/bar/baz"); + } + version (Windows) + { + assert(buildPath("foo") == "foo"); + assert(buildPath(`\foo/`) == `\foo/`); + assert(buildPath("foo", "bar", "baz") == `foo\bar\baz`); + assert(buildPath("foo", `\bar`) == `\bar`); + assert(buildPath(`c:\foo`, "bar") == `c:\foo\bar`); + assert(buildPath("foo"w, `d:\bar`w.dup) == `d:\bar`); + assert(buildPath(`c:\foo\bar`, `\baz`) == `c:\baz`); + assert(buildPath(`\\foo\bar\baz`d, `foo`d, `\bar`d) == `\\foo\bar\bar`d); + + static assert(buildPath("foo", "bar", "baz") == `foo\bar\baz`); + static assert(buildPath("foo", `c:\bar`, "baz") == `c:\bar\baz`); + + assert(buildPath(ir("foo")) == "foo"); + assert(buildPath(ir(`\foo/`)) == `\foo/`); + assert(buildPath(ir("foo", "bar", "baz")) == `foo\bar\baz`); + assert(buildPath(ir("foo", `\bar`)) == `\bar`); + assert(buildPath(ir(`c:\foo`, "bar")) == `c:\foo\bar`); + assert(buildPath(ir("foo"w.dup, `d:\bar`w.dup)) == `d:\bar`); + assert(buildPath(ir(`c:\foo\bar`, `\baz`)) == `c:\baz`); + assert(buildPath(ir(`\\foo\bar\baz`d, `foo`d, `\bar`d)) == `\\foo\bar\bar`d); + } + + // Test that allocation works as it should. + auto manyShort = "aaa".repeat(1000).array(); + auto manyShortCombined = join(manyShort, dirSeparator); + assert(buildPath(manyShort) == manyShortCombined); + assert(buildPath(ir(manyShort)) == manyShortCombined); + + auto fewLong = 'b'.repeat(500).array().repeat(10).array(); + auto fewLongCombined = join(fewLong, dirSeparator); + assert(buildPath(fewLong) == fewLongCombined); + assert(buildPath(ir(fewLong)) == fewLongCombined); +} + +@safe unittest +{ + // Test for issue 7397 + string[] ary = ["a", "b"]; + version (Posix) + { + assert(buildPath(ary) == "a/b"); + } + else version (Windows) + { + assert(buildPath(ary) == `a\b`); + } +} + + +/** + * Concatenate path segments together to form one path. + * + * Params: + * r1 = first segment + * r2 = second segment + * ranges = 0 or more segments + * Returns: + * Lazy range which is the concatenation of r1, r2 and ranges with path separators. + * The resulting element type is that of r1. + * See_Also: + * $(LREF buildPath) + */ +auto chainPath(R1, R2, Ranges...)(R1 r1, R2 r2, Ranges ranges) +if ((isRandomAccessRange!R1 && hasSlicing!R1 && hasLength!R1 && isSomeChar!(ElementType!R1) || + isNarrowString!R1 && + !isConvertibleToString!R1) && + (isRandomAccessRange!R2 && hasSlicing!R2 && hasLength!R2 && isSomeChar!(ElementType!R2) || + isNarrowString!R2 && + !isConvertibleToString!R2) && + (Ranges.length == 0 || is(typeof(chainPath(r2, ranges)))) + ) +{ + static if (Ranges.length) + { + return chainPath(chainPath(r1, r2), ranges); + } + else + { + import std.range : only, chain; + import std.utf : byUTF; + + alias CR = Unqual!(ElementEncodingType!R1); + auto sep = only(CR(dirSeparator[0])); + bool usesep = false; + + auto pos = r1.length; + + if (pos) + { + if (isRooted(r2)) + { + version (Posix) + { + pos = 0; + } + else version (Windows) + { + if (isAbsolute(r2)) + pos = 0; + else + { + pos = rootName(r1).length; + if (pos > 0 && isDirSeparator(r1[pos - 1])) + --pos; + } + } + else + static assert(0); + } + else if (!isDirSeparator(r1[pos - 1])) + usesep = true; + } + if (!usesep) + sep.popFront(); + // Return r1 ~ '/' ~ r2 + return chain(r1[0 .. pos].byUTF!CR, sep, r2.byUTF!CR); + } +} + +/// +@safe unittest +{ + import std.array; + version (Posix) + { + assert(chainPath("foo", "bar", "baz").array == "foo/bar/baz"); + assert(chainPath("/foo/", "bar/baz").array == "/foo/bar/baz"); + assert(chainPath("/foo", "/bar").array == "/bar"); + } + + version (Windows) + { + assert(chainPath("foo", "bar", "baz").array == `foo\bar\baz`); + assert(chainPath(`c:\foo`, `bar\baz`).array == `c:\foo\bar\baz`); + assert(chainPath("foo", `d:\bar`).array == `d:\bar`); + assert(chainPath("foo", `\bar`).array == `\bar`); + assert(chainPath(`c:\foo`, `\bar`).array == `c:\bar`); + } + + import std.utf : byChar; + version (Posix) + { + assert(chainPath("foo", "bar", "baz").array == "foo/bar/baz"); + assert(chainPath("/foo/".byChar, "bar/baz").array == "/foo/bar/baz"); + assert(chainPath("/foo", "/bar".byChar).array == "/bar"); + } + + version (Windows) + { + assert(chainPath("foo", "bar", "baz").array == `foo\bar\baz`); + assert(chainPath(`c:\foo`.byChar, `bar\baz`).array == `c:\foo\bar\baz`); + assert(chainPath("foo", `d:\bar`).array == `d:\bar`); + assert(chainPath("foo", `\bar`.byChar).array == `\bar`); + assert(chainPath(`c:\foo`, `\bar`w).array == `c:\bar`); + } +} + +auto chainPath(Ranges...)(auto ref Ranges ranges) +if (Ranges.length >= 2 && + std.meta.anySatisfy!(isConvertibleToString, Ranges)) +{ + import std.meta : staticMap; + alias Types = staticMap!(convertToString, Ranges); + return chainPath!Types(ranges); +} + +@safe unittest +{ + assert(chainPath(TestAliasedString(null), TestAliasedString(null), TestAliasedString(null)).empty); + assert(chainPath(TestAliasedString(null), TestAliasedString(null), "").empty); + assert(chainPath(TestAliasedString(null), "", TestAliasedString(null)).empty); + static struct S { string s; } + static assert(!__traits(compiles, chainPath(TestAliasedString(null), S(""), TestAliasedString(null)))); +} + +/** Performs the same task as $(LREF buildPath), + while at the same time resolving current/parent directory + symbols ($(D ".") and $(D "..")) and removing superfluous + directory separators. + It will return "." if the path leads to the starting directory. + On Windows, slashes are replaced with backslashes. + + Using buildNormalizedPath on null paths will always return null. + + Note that this function does not resolve symbolic links. + + This function always allocates memory to hold the resulting path. + Use $(LREF asNormalizedPath) to not allocate memory. + + Params: + paths = An array of paths to assemble. + + Returns: The assembled path. +*/ +immutable(C)[] buildNormalizedPath(C)(const(C[])[] paths...) + @trusted pure nothrow +if (isSomeChar!C) +{ + import std.array : array; + + const(C)[] result; + foreach (path; paths) + { + if (result) + result = chainPath(result, path).array; + else + result = path; + } + result = asNormalizedPath(result).array; + return cast(typeof(return)) result; +} + +/// +@safe unittest +{ + assert(buildNormalizedPath("foo", "..") == "."); + + version (Posix) + { + assert(buildNormalizedPath("/foo/./bar/..//baz/") == "/foo/baz"); + assert(buildNormalizedPath("../foo/.") == "../foo"); + assert(buildNormalizedPath("/foo", "bar/baz/") == "/foo/bar/baz"); + assert(buildNormalizedPath("/foo", "/bar/..", "baz") == "/baz"); + assert(buildNormalizedPath("foo/./bar", "../../", "../baz") == "../baz"); + assert(buildNormalizedPath("/foo/./bar", "../../baz") == "/baz"); + } + + version (Windows) + { + assert(buildNormalizedPath(`c:\foo\.\bar/..\\baz\`) == `c:\foo\baz`); + assert(buildNormalizedPath(`..\foo\.`) == `..\foo`); + assert(buildNormalizedPath(`c:\foo`, `bar\baz\`) == `c:\foo\bar\baz`); + assert(buildNormalizedPath(`c:\foo`, `bar/..`) == `c:\foo`); + assert(buildNormalizedPath(`\\server\share\foo`, `..\bar`) == + `\\server\share\bar`); + } +} + +@safe unittest +{ + assert(buildNormalizedPath(".", ".") == "."); + assert(buildNormalizedPath("foo", "..") == "."); + assert(buildNormalizedPath("", "") is null); + assert(buildNormalizedPath("", ".") == "."); + assert(buildNormalizedPath(".", "") == "."); + assert(buildNormalizedPath(null, "foo") == "foo"); + assert(buildNormalizedPath("", "foo") == "foo"); + assert(buildNormalizedPath("", "") == ""); + assert(buildNormalizedPath("", null) == ""); + assert(buildNormalizedPath(null, "") == ""); + assert(buildNormalizedPath!(char)(null, null) == ""); + + version (Posix) + { + assert(buildNormalizedPath("/", "foo", "bar") == "/foo/bar"); + assert(buildNormalizedPath("foo", "bar", "baz") == "foo/bar/baz"); + assert(buildNormalizedPath("foo", "bar/baz") == "foo/bar/baz"); + assert(buildNormalizedPath("foo", "bar//baz///") == "foo/bar/baz"); + assert(buildNormalizedPath("/foo", "bar/baz") == "/foo/bar/baz"); + assert(buildNormalizedPath("/foo", "/bar/baz") == "/bar/baz"); + assert(buildNormalizedPath("/foo/..", "/bar/./baz") == "/bar/baz"); + assert(buildNormalizedPath("/foo/..", "bar/baz") == "/bar/baz"); + assert(buildNormalizedPath("/foo/../../", "bar/baz") == "/bar/baz"); + assert(buildNormalizedPath("/foo/bar", "../baz") == "/foo/baz"); + assert(buildNormalizedPath("/foo/bar", "../../baz") == "/baz"); + assert(buildNormalizedPath("/foo/bar", ".././/baz/..", "wee/") == "/foo/wee"); + assert(buildNormalizedPath("//foo/bar", "baz///wee") == "/foo/bar/baz/wee"); + static assert(buildNormalizedPath("/foo/..", "/bar/./baz") == "/bar/baz"); + } + else version (Windows) + { + assert(buildNormalizedPath(`\`, `foo`, `bar`) == `\foo\bar`); + assert(buildNormalizedPath(`foo`, `bar`, `baz`) == `foo\bar\baz`); + assert(buildNormalizedPath(`foo`, `bar\baz`) == `foo\bar\baz`); + assert(buildNormalizedPath(`foo`, `bar\\baz\\\`) == `foo\bar\baz`); + assert(buildNormalizedPath(`\foo`, `bar\baz`) == `\foo\bar\baz`); + assert(buildNormalizedPath(`\foo`, `\bar\baz`) == `\bar\baz`); + assert(buildNormalizedPath(`\foo\..`, `\bar\.\baz`) == `\bar\baz`); + assert(buildNormalizedPath(`\foo\..`, `bar\baz`) == `\bar\baz`); + assert(buildNormalizedPath(`\foo\..\..\`, `bar\baz`) == `\bar\baz`); + assert(buildNormalizedPath(`\foo\bar`, `..\baz`) == `\foo\baz`); + assert(buildNormalizedPath(`\foo\bar`, `../../baz`) == `\baz`); + assert(buildNormalizedPath(`\foo\bar`, `..\.\/baz\..`, `wee\`) == `\foo\wee`); + + assert(buildNormalizedPath(`c:\`, `foo`, `bar`) == `c:\foo\bar`); + assert(buildNormalizedPath(`c:foo`, `bar`, `baz`) == `c:foo\bar\baz`); + assert(buildNormalizedPath(`c:foo`, `bar\baz`) == `c:foo\bar\baz`); + assert(buildNormalizedPath(`c:foo`, `bar\\baz\\\`) == `c:foo\bar\baz`); + assert(buildNormalizedPath(`c:\foo`, `bar\baz`) == `c:\foo\bar\baz`); + assert(buildNormalizedPath(`c:\foo`, `\bar\baz`) == `c:\bar\baz`); + assert(buildNormalizedPath(`c:\foo\..`, `\bar\.\baz`) == `c:\bar\baz`); + assert(buildNormalizedPath(`c:\foo\..`, `bar\baz`) == `c:\bar\baz`); + assert(buildNormalizedPath(`c:\foo\..\..\`, `bar\baz`) == `c:\bar\baz`); + assert(buildNormalizedPath(`c:\foo\bar`, `..\baz`) == `c:\foo\baz`); + assert(buildNormalizedPath(`c:\foo\bar`, `..\..\baz`) == `c:\baz`); + assert(buildNormalizedPath(`c:\foo\bar`, `..\.\\baz\..`, `wee\`) == `c:\foo\wee`); + + assert(buildNormalizedPath(`\\server\share`, `foo`, `bar`) == `\\server\share\foo\bar`); + assert(buildNormalizedPath(`\\server\share\`, `foo`, `bar`) == `\\server\share\foo\bar`); + assert(buildNormalizedPath(`\\server\share\foo`, `bar\baz`) == `\\server\share\foo\bar\baz`); + assert(buildNormalizedPath(`\\server\share\foo`, `\bar\baz`) == `\\server\share\bar\baz`); + assert(buildNormalizedPath(`\\server\share\foo\..`, `\bar\.\baz`) == `\\server\share\bar\baz`); + assert(buildNormalizedPath(`\\server\share\foo\..`, `bar\baz`) == `\\server\share\bar\baz`); + assert(buildNormalizedPath(`\\server\share\foo\..\..\`, `bar\baz`) == `\\server\share\bar\baz`); + assert(buildNormalizedPath(`\\server\share\foo\bar`, `..\baz`) == `\\server\share\foo\baz`); + assert(buildNormalizedPath(`\\server\share\foo\bar`, `..\..\baz`) == `\\server\share\baz`); + assert(buildNormalizedPath(`\\server\share\foo\bar`, `..\.\\baz\..`, `wee\`) == `\\server\share\foo\wee`); + + static assert(buildNormalizedPath(`\foo\..\..\`, `bar\baz`) == `\bar\baz`); + } + else static assert(0); +} + +@safe unittest +{ + // Test for issue 7397 + string[] ary = ["a", "b"]; + version (Posix) + { + assert(buildNormalizedPath(ary) == "a/b"); + } + else version (Windows) + { + assert(buildNormalizedPath(ary) == `a\b`); + } +} + + +/** Normalize a path by resolving current/parent directory + symbols ($(D ".") and $(D "..")) and removing superfluous + directory separators. + It will return "." if the path leads to the starting directory. + On Windows, slashes are replaced with backslashes. + + Using asNormalizedPath on empty paths will always return an empty path. + + Does not resolve symbolic links. + + This function always allocates memory to hold the resulting path. + Use $(LREF buildNormalizedPath) to allocate memory and return a string. + + Params: + path = string or random access range representing the _path to normalize + + Returns: + normalized path as a forward range +*/ + +auto asNormalizedPath(R)(R path) +if (isSomeChar!(ElementEncodingType!R) && + (isRandomAccessRange!R && hasSlicing!R && hasLength!R || isNarrowString!R) && + !isConvertibleToString!R) +{ + alias C = Unqual!(ElementEncodingType!R); + alias S = typeof(path[0 .. 0]); + + static struct Result + { + @property bool empty() + { + return c == c.init; + } + + @property C front() + { + return c; + } + + void popFront() + { + C lastc = c; + c = c.init; + if (!element.empty) + { + c = getElement0(); + return; + } + L1: + while (1) + { + if (elements.empty) + { + element = element[0 .. 0]; + return; + } + element = elements.front; + elements.popFront(); + if (isDot(element) || (rooted && isDotDot(element))) + continue; + + if (rooted || !isDotDot(element)) + { + int n = 1; + auto elements2 = elements.save; + while (!elements2.empty) + { + auto e = elements2.front; + elements2.popFront(); + if (isDot(e)) + continue; + if (isDotDot(e)) + { + --n; + if (n == 0) + { + elements = elements2; + element = element[0 .. 0]; + continue L1; + } + } + else + ++n; + } + } + break; + } + + static assert(dirSeparator.length == 1); + if (lastc == dirSeparator[0] || lastc == lastc.init) + c = getElement0(); + else + c = dirSeparator[0]; + } + + static if (isForwardRange!R) + { + @property auto save() + { + auto result = this; + result.element = element.save; + result.elements = elements.save; + return result; + } + } + + private: + this(R path) + { + element = rootName(path); + auto i = element.length; + while (i < path.length && isDirSeparator(path[i])) + ++i; + rooted = i > 0; + elements = pathSplitter(path[i .. $]); + popFront(); + if (c == c.init && path.length) + c = C('.'); + } + + C getElement0() + { + static if (isNarrowString!S) // avoid autodecode + { + C c = element[0]; + element = element[1 .. $]; + } + else + { + C c = element.front; + element.popFront(); + } + version (Windows) + { + if (c == '/') // can appear in root element + c = '\\'; // use native Windows directory separator + } + return c; + } + + // See if elem is "." + static bool isDot(S elem) + { + return elem.length == 1 && elem[0] == '.'; + } + + // See if elem is ".." + static bool isDotDot(S elem) + { + return elem.length == 2 && elem[0] == '.' && elem[1] == '.'; + } + + bool rooted; // the path starts with a root directory + C c; + S element; + typeof(pathSplitter(path[0 .. 0])) elements; + } + + return Result(path); +} + +/// +@safe unittest +{ + import std.array; + assert(asNormalizedPath("foo/..").array == "."); + + version (Posix) + { + assert(asNormalizedPath("/foo/./bar/..//baz/").array == "/foo/baz"); + assert(asNormalizedPath("../foo/.").array == "../foo"); + assert(asNormalizedPath("/foo/bar/baz/").array == "/foo/bar/baz"); + assert(asNormalizedPath("/foo/./bar/../../baz").array == "/baz"); + } + + version (Windows) + { + assert(asNormalizedPath(`c:\foo\.\bar/..\\baz\`).array == `c:\foo\baz`); + assert(asNormalizedPath(`..\foo\.`).array == `..\foo`); + assert(asNormalizedPath(`c:\foo\bar\baz\`).array == `c:\foo\bar\baz`); + assert(asNormalizedPath(`c:\foo\bar/..`).array == `c:\foo`); + assert(asNormalizedPath(`\\server\share\foo\..\bar`).array == + `\\server\share\bar`); + } +} + +auto asNormalizedPath(R)(auto ref R path) +if (isConvertibleToString!R) +{ + return asNormalizedPath!(StringTypeOf!R)(path); +} + +@safe unittest +{ + assert(testAliasedString!asNormalizedPath(null)); +} + +@safe unittest +{ + import std.array; + import std.utf : byChar; + + assert(asNormalizedPath("").array is null); + assert(asNormalizedPath("foo").array == "foo"); + assert(asNormalizedPath(".").array == "."); + assert(asNormalizedPath("./.").array == "."); + assert(asNormalizedPath("foo/..").array == "."); + + auto save = asNormalizedPath("fob").save; + save.popFront(); + assert(save.front == 'o'); + + version (Posix) + { + assert(asNormalizedPath("/foo/bar").array == "/foo/bar"); + assert(asNormalizedPath("foo/bar/baz").array == "foo/bar/baz"); + assert(asNormalizedPath("foo/bar/baz").array == "foo/bar/baz"); + assert(asNormalizedPath("foo/bar//baz///").array == "foo/bar/baz"); + assert(asNormalizedPath("/foo/bar/baz").array == "/foo/bar/baz"); + assert(asNormalizedPath("/foo/../bar/baz").array == "/bar/baz"); + assert(asNormalizedPath("/foo/../..//bar/baz").array == "/bar/baz"); + assert(asNormalizedPath("/foo/bar/../baz").array == "/foo/baz"); + assert(asNormalizedPath("/foo/bar/../../baz").array == "/baz"); + assert(asNormalizedPath("/foo/bar/.././/baz/../wee/").array == "/foo/wee"); + assert(asNormalizedPath("//foo/bar/baz///wee").array == "/foo/bar/baz/wee"); + + assert(asNormalizedPath("foo//bar").array == "foo/bar"); + assert(asNormalizedPath("foo/bar").array == "foo/bar"); + + //Curent dir path + assert(asNormalizedPath("./").array == "."); + assert(asNormalizedPath("././").array == "."); + assert(asNormalizedPath("./foo/..").array == "."); + assert(asNormalizedPath("foo/..").array == "."); + } + else version (Windows) + { + assert(asNormalizedPath(`\foo\bar`).array == `\foo\bar`); + assert(asNormalizedPath(`foo\bar\baz`).array == `foo\bar\baz`); + assert(asNormalizedPath(`foo\bar\baz`).array == `foo\bar\baz`); + assert(asNormalizedPath(`foo\bar\\baz\\\`).array == `foo\bar\baz`); + assert(asNormalizedPath(`\foo\bar\baz`).array == `\foo\bar\baz`); + assert(asNormalizedPath(`\foo\..\\bar\.\baz`).array == `\bar\baz`); + assert(asNormalizedPath(`\foo\..\bar\baz`).array == `\bar\baz`); + assert(asNormalizedPath(`\foo\..\..\\bar\baz`).array == `\bar\baz`); + + assert(asNormalizedPath(`\foo\bar\..\baz`).array == `\foo\baz`); + assert(asNormalizedPath(`\foo\bar\../../baz`).array == `\baz`); + assert(asNormalizedPath(`\foo\bar\..\.\/baz\..\wee\`).array == `\foo\wee`); + + assert(asNormalizedPath(`c:\foo\bar`).array == `c:\foo\bar`); + assert(asNormalizedPath(`c:foo\bar\baz`).array == `c:foo\bar\baz`); + assert(asNormalizedPath(`c:foo\bar\baz`).array == `c:foo\bar\baz`); + assert(asNormalizedPath(`c:foo\bar\\baz\\\`).array == `c:foo\bar\baz`); + assert(asNormalizedPath(`c:\foo\bar\baz`).array == `c:\foo\bar\baz`); + + assert(asNormalizedPath(`c:\foo\..\\bar\.\baz`).array == `c:\bar\baz`); + assert(asNormalizedPath(`c:\foo\..\bar\baz`).array == `c:\bar\baz`); + assert(asNormalizedPath(`c:\foo\..\..\\bar\baz`).array == `c:\bar\baz`); + assert(asNormalizedPath(`c:\foo\bar\..\baz`).array == `c:\foo\baz`); + assert(asNormalizedPath(`c:\foo\bar\..\..\baz`).array == `c:\baz`); + assert(asNormalizedPath(`c:\foo\bar\..\.\\baz\..\wee\`).array == `c:\foo\wee`); + assert(asNormalizedPath(`\\server\share\foo\bar`).array == `\\server\share\foo\bar`); + assert(asNormalizedPath(`\\server\share\\foo\bar`).array == `\\server\share\foo\bar`); + assert(asNormalizedPath(`\\server\share\foo\bar\baz`).array == `\\server\share\foo\bar\baz`); + assert(asNormalizedPath(`\\server\share\foo\..\\bar\.\baz`).array == `\\server\share\bar\baz`); + assert(asNormalizedPath(`\\server\share\foo\..\bar\baz`).array == `\\server\share\bar\baz`); + assert(asNormalizedPath(`\\server\share\foo\..\..\\bar\baz`).array == `\\server\share\bar\baz`); + assert(asNormalizedPath(`\\server\share\foo\bar\..\baz`).array == `\\server\share\foo\baz`); + assert(asNormalizedPath(`\\server\share\foo\bar\..\..\baz`).array == `\\server\share\baz`); + assert(asNormalizedPath(`\\server\share\foo\bar\..\.\\baz\..\wee\`).array == `\\server\share\foo\wee`); + + static assert(asNormalizedPath(`\foo\..\..\\bar\baz`).array == `\bar\baz`); + + assert(asNormalizedPath("foo//bar").array == `foo\bar`); + + //Curent dir path + assert(asNormalizedPath(`.\`).array == "."); + assert(asNormalizedPath(`.\.\`).array == "."); + assert(asNormalizedPath(`.\foo\..`).array == "."); + assert(asNormalizedPath(`foo\..`).array == "."); + } + else static assert(0); +} + +@safe unittest +{ + import std.array; + + version (Posix) + { + // Trivial + assert(asNormalizedPath("").empty); + assert(asNormalizedPath("foo/bar").array == "foo/bar"); + + // Correct handling of leading slashes + assert(asNormalizedPath("/").array == "/"); + assert(asNormalizedPath("///").array == "/"); + assert(asNormalizedPath("////").array == "/"); + assert(asNormalizedPath("/foo/bar").array == "/foo/bar"); + assert(asNormalizedPath("//foo/bar").array == "/foo/bar"); + assert(asNormalizedPath("///foo/bar").array == "/foo/bar"); + assert(asNormalizedPath("////foo/bar").array == "/foo/bar"); + + // Correct handling of single-dot symbol (current directory) + assert(asNormalizedPath("/./foo").array == "/foo"); + assert(asNormalizedPath("/foo/./bar").array == "/foo/bar"); + + assert(asNormalizedPath("./foo").array == "foo"); + assert(asNormalizedPath("././foo").array == "foo"); + assert(asNormalizedPath("foo/././bar").array == "foo/bar"); + + // Correct handling of double-dot symbol (previous directory) + assert(asNormalizedPath("/foo/../bar").array == "/bar"); + assert(asNormalizedPath("/foo/../../bar").array == "/bar"); + assert(asNormalizedPath("/../foo").array == "/foo"); + assert(asNormalizedPath("/../../foo").array == "/foo"); + assert(asNormalizedPath("/foo/..").array == "/"); + assert(asNormalizedPath("/foo/../..").array == "/"); + + assert(asNormalizedPath("foo/../bar").array == "bar"); + assert(asNormalizedPath("foo/../../bar").array == "../bar"); + assert(asNormalizedPath("../foo").array == "../foo"); + assert(asNormalizedPath("../../foo").array == "../../foo"); + assert(asNormalizedPath("../foo/../bar").array == "../bar"); + assert(asNormalizedPath(".././../foo").array == "../../foo"); + assert(asNormalizedPath("foo/bar/..").array == "foo"); + assert(asNormalizedPath("/foo/../..").array == "/"); + + // The ultimate path + assert(asNormalizedPath("/foo/../bar//./../...///baz//").array == "/.../baz"); + static assert(asNormalizedPath("/foo/../bar//./../...///baz//").array == "/.../baz"); + } + else version (Windows) + { + // Trivial + assert(asNormalizedPath("").empty); + assert(asNormalizedPath(`foo\bar`).array == `foo\bar`); + assert(asNormalizedPath("foo/bar").array == `foo\bar`); + + // Correct handling of absolute paths + assert(asNormalizedPath("/").array == `\`); + assert(asNormalizedPath(`\`).array == `\`); + assert(asNormalizedPath(`\\\`).array == `\`); + assert(asNormalizedPath(`\\\\`).array == `\`); + assert(asNormalizedPath(`\foo\bar`).array == `\foo\bar`); + assert(asNormalizedPath(`\\foo`).array == `\\foo`); + assert(asNormalizedPath(`\\foo\\`).array == `\\foo`); + assert(asNormalizedPath(`\\foo/bar`).array == `\\foo\bar`); + assert(asNormalizedPath(`\\\foo\bar`).array == `\foo\bar`); + assert(asNormalizedPath(`\\\\foo\bar`).array == `\foo\bar`); + assert(asNormalizedPath(`c:\`).array == `c:\`); + assert(asNormalizedPath(`c:\foo\bar`).array == `c:\foo\bar`); + assert(asNormalizedPath(`c:\\foo\bar`).array == `c:\foo\bar`); + + // Correct handling of single-dot symbol (current directory) + assert(asNormalizedPath(`\./foo`).array == `\foo`); + assert(asNormalizedPath(`\foo/.\bar`).array == `\foo\bar`); + + assert(asNormalizedPath(`.\foo`).array == `foo`); + assert(asNormalizedPath(`./.\foo`).array == `foo`); + assert(asNormalizedPath(`foo\.\./bar`).array == `foo\bar`); + + // Correct handling of double-dot symbol (previous directory) + assert(asNormalizedPath(`\foo\..\bar`).array == `\bar`); + assert(asNormalizedPath(`\foo\../..\bar`).array == `\bar`); + assert(asNormalizedPath(`\..\foo`).array == `\foo`); + assert(asNormalizedPath(`\..\..\foo`).array == `\foo`); + assert(asNormalizedPath(`\foo\..`).array == `\`); + assert(asNormalizedPath(`\foo\../..`).array == `\`); + + assert(asNormalizedPath(`foo\..\bar`).array == `bar`); + assert(asNormalizedPath(`foo\..\../bar`).array == `..\bar`); + + assert(asNormalizedPath(`..\foo`).array == `..\foo`); + assert(asNormalizedPath(`..\..\foo`).array == `..\..\foo`); + assert(asNormalizedPath(`..\foo\..\bar`).array == `..\bar`); + assert(asNormalizedPath(`..\.\..\foo`).array == `..\..\foo`); + assert(asNormalizedPath(`foo\bar\..`).array == `foo`); + assert(asNormalizedPath(`\foo\..\..`).array == `\`); + assert(asNormalizedPath(`c:\foo\..\..`).array == `c:\`); + + // Correct handling of non-root path with drive specifier + assert(asNormalizedPath(`c:foo`).array == `c:foo`); + assert(asNormalizedPath(`c:..\foo\.\..\bar`).array == `c:..\bar`); + + // The ultimate path + assert(asNormalizedPath(`c:\foo\..\bar\\.\..\...\\\baz\\`).array == `c:\...\baz`); + static assert(asNormalizedPath(`c:\foo\..\bar\\.\..\...\\\baz\\`).array == `c:\...\baz`); + } + else static assert(false); +} + +/** Slice up a path into its elements. + + Params: + path = string or slicable random access range + + Returns: + bidirectional range of slices of `path` +*/ +auto pathSplitter(R)(R path) +if ((isRandomAccessRange!R && hasSlicing!R || + isNarrowString!R) && + !isConvertibleToString!R) +{ + static struct PathSplitter + { + @property bool empty() const { return pe == 0; } + + @property R front() + { + assert(!empty); + return _path[fs .. fe]; + } + + void popFront() + { + assert(!empty); + if (ps == pe) + { + if (fs == bs && fe == be) + { + pe = 0; + } + else + { + fs = bs; + fe = be; + } + } + else + { + fs = ps; + fe = fs; + while (fe < pe && !isDirSeparator(_path[fe])) + ++fe; + ps = ltrim(fe, pe); + } + } + + @property R back() + { + assert(!empty); + return _path[bs .. be]; + } + + void popBack() + { + assert(!empty); + if (ps == pe) + { + if (fs == bs && fe == be) + { + pe = 0; + } + else + { + bs = fs; + be = fe; + } + } + else + { + bs = pe; + be = bs; + while (bs > ps && !isDirSeparator(_path[bs - 1])) + --bs; + pe = rtrim(ps, bs); + } + } + @property auto save() { return this; } + + + private: + R _path; + size_t ps, pe; + size_t fs, fe; + size_t bs, be; + + this(R p) + { + if (p.empty) + { + pe = 0; + return; + } + _path = p; + + ps = 0; + pe = _path.length; + + // If path is rooted, first element is special + version (Windows) + { + if (isUNC(_path)) + { + auto i = uncRootLength(_path); + fs = 0; + fe = i; + ps = ltrim(fe, pe); + } + else if (isDriveRoot(_path)) + { + fs = 0; + fe = 3; + ps = ltrim(fe, pe); + } + else if (_path.length >= 1 && isDirSeparator(_path[0])) + { + fs = 0; + fe = 1; + ps = ltrim(fe, pe); + } + else + { + assert(!isRooted(_path)); + popFront(); + } + } + else version (Posix) + { + if (_path.length >= 1 && isDirSeparator(_path[0])) + { + fs = 0; + fe = 1; + ps = ltrim(fe, pe); + } + else + { + popFront(); + } + } + else static assert(0); + + if (ps == pe) + { + bs = fs; + be = fe; + } + else + { + pe = rtrim(ps, pe); + popBack(); + } + } + + size_t ltrim(size_t s, size_t e) + { + while (s < e && isDirSeparator(_path[s])) + ++s; + return s; + } + + size_t rtrim(size_t s, size_t e) + { + while (s < e && isDirSeparator(_path[e - 1])) + --e; + return e; + } + } + + return PathSplitter(path); +} + +/// +@safe unittest +{ + import std.algorithm.comparison : equal; + import std.conv : to; + + assert(equal(pathSplitter("/"), ["/"])); + assert(equal(pathSplitter("/foo/bar"), ["/", "foo", "bar"])); + assert(equal(pathSplitter("foo/../bar//./"), ["foo", "..", "bar", "."])); + + version (Posix) + { + assert(equal(pathSplitter("//foo/bar"), ["/", "foo", "bar"])); + } + + version (Windows) + { + assert(equal(pathSplitter(`foo\..\bar\/.\`), ["foo", "..", "bar", "."])); + assert(equal(pathSplitter("c:"), ["c:"])); + assert(equal(pathSplitter(`c:\foo\bar`), [`c:\`, "foo", "bar"])); + assert(equal(pathSplitter(`c:foo\bar`), ["c:foo", "bar"])); + } +} + +auto pathSplitter(R)(auto ref R path) +if (isConvertibleToString!R) +{ + return pathSplitter!(StringTypeOf!R)(path); +} + +@safe unittest +{ + import std.algorithm.comparison : equal; + assert(testAliasedString!pathSplitter("/")); +} + +@safe unittest +{ + // equal2 verifies that the range is the same both ways, i.e. + // through front/popFront and back/popBack. + import std.algorithm; + import std.range; + bool equal2(R1, R2)(R1 r1, R2 r2) + { + static assert(isBidirectionalRange!R1); + return equal(r1, r2) && equal(retro(r1), retro(r2)); + } + + assert(pathSplitter("").empty); + + // Root directories + assert(equal2(pathSplitter("/"), ["/"])); + assert(equal2(pathSplitter("//"), ["/"])); + assert(equal2(pathSplitter("///"w), ["/"w])); + + // Absolute paths + assert(equal2(pathSplitter("/foo/bar".dup), ["/", "foo", "bar"])); + + // General + assert(equal2(pathSplitter("foo/bar"d.dup), ["foo"d, "bar"d])); + assert(equal2(pathSplitter("foo//bar"), ["foo", "bar"])); + assert(equal2(pathSplitter("foo/bar//"w), ["foo"w, "bar"w])); + assert(equal2(pathSplitter("foo/../bar//./"d), ["foo"d, ".."d, "bar"d, "."d])); + + // save() + auto ps1 = pathSplitter("foo/bar/baz"); + auto ps2 = ps1.save; + ps1.popFront(); + assert(equal2(ps1, ["bar", "baz"])); + assert(equal2(ps2, ["foo", "bar", "baz"])); + + // Platform specific + version (Posix) + { + assert(equal2(pathSplitter("//foo/bar"w.dup), ["/"w, "foo"w, "bar"w])); + } + version (Windows) + { + assert(equal2(pathSplitter(`\`), [`\`])); + assert(equal2(pathSplitter(`foo\..\bar\/.\`), ["foo", "..", "bar", "."])); + assert(equal2(pathSplitter("c:"), ["c:"])); + assert(equal2(pathSplitter(`c:\foo\bar`), [`c:\`, "foo", "bar"])); + assert(equal2(pathSplitter(`c:foo\bar`), ["c:foo", "bar"])); + assert(equal2(pathSplitter(`\\foo\bar`), [`\\foo\bar`])); + assert(equal2(pathSplitter(`\\foo\bar\\`), [`\\foo\bar`])); + assert(equal2(pathSplitter(`\\foo\bar\baz`), [`\\foo\bar`, "baz"])); + } + + import std.exception; + assertCTFEable!( + { + assert(equal(pathSplitter("/foo/bar".dup), ["/", "foo", "bar"])); + }); + + static assert(is(typeof(pathSplitter!(const(char)[])(null).front) == const(char)[])); + + import std.utf : byDchar; + assert(equal2(pathSplitter("foo/bar"d.byDchar), ["foo"d, "bar"d])); +} + + + + +/** Determines whether a path starts at a root directory. + + Params: path = A path name. + Returns: Whether a path starts at a root directory. + + On POSIX, this function returns true if and only if the path starts + with a slash (/). + --- + version (Posix) + { + assert(isRooted("/")); + assert(isRooted("/foo")); + assert(!isRooted("foo")); + assert(!isRooted("../foo")); + } + --- + + On Windows, this function returns true if the path starts at + the root directory of the current drive, of some other drive, + or of a network drive. + --- + version (Windows) + { + assert(isRooted(`\`)); + assert(isRooted(`\foo`)); + assert(isRooted(`d:\foo`)); + assert(isRooted(`\\foo\bar`)); + assert(!isRooted("foo")); + assert(!isRooted("d:foo")); + } + --- +*/ +bool isRooted(R)(R path) +if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || + is(StringTypeOf!R)) +{ + if (path.length >= 1 && isDirSeparator(path[0])) return true; + version (Posix) return false; + else version (Windows) return isAbsolute!(BaseOf!R)(path); +} + + +@safe unittest +{ + assert(isRooted("/")); + assert(isRooted("/foo")); + assert(!isRooted("foo")); + assert(!isRooted("../foo")); + + version (Windows) + { + assert(isRooted(`\`)); + assert(isRooted(`\foo`)); + assert(isRooted(`d:\foo`)); + assert(isRooted(`\\foo\bar`)); + assert(!isRooted("foo")); + assert(!isRooted("d:foo")); + } + + static assert(isRooted("/foo")); + static assert(!isRooted("foo")); + + static struct DirEntry { string s; alias s this; } + assert(!isRooted(DirEntry("foo"))); +} + + + + +/** Determines whether a path is absolute or not. + + Params: path = A path name. + + Returns: Whether a path is absolute or not. + + Example: + On POSIX, an absolute path starts at the root directory. + (In fact, $(D _isAbsolute) is just an alias for $(LREF isRooted).) + --- + version (Posix) + { + assert(isAbsolute("/")); + assert(isAbsolute("/foo")); + assert(!isAbsolute("foo")); + assert(!isAbsolute("../foo")); + } + --- + + On Windows, an absolute path starts at the root directory of + a specific drive. Hence, it must start with $(D `d:\`) or $(D `d:/`), + where $(D d) is the drive letter. Alternatively, it may be a + network path, i.e. a path starting with a double (back)slash. + --- + version (Windows) + { + assert(isAbsolute(`d:\`)); + assert(isAbsolute(`d:\foo`)); + assert(isAbsolute(`\\foo\bar`)); + assert(!isAbsolute(`\`)); + assert(!isAbsolute(`\foo`)); + assert(!isAbsolute("d:foo")); + } + --- +*/ +version (StdDdoc) +{ + bool isAbsolute(R)(R path) pure nothrow @safe + if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || + is(StringTypeOf!R)); +} +else version (Windows) +{ + bool isAbsolute(R)(R path) + if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || + is(StringTypeOf!R)) + { + return isDriveRoot!(BaseOf!R)(path) || isUNC!(BaseOf!R)(path); + } +} +else version (Posix) +{ + alias isAbsolute = isRooted; +} + + +@safe unittest +{ + assert(!isAbsolute("foo")); + assert(!isAbsolute("../foo"w)); + static assert(!isAbsolute("foo")); + + version (Posix) + { + assert(isAbsolute("/"d)); + assert(isAbsolute("/foo".dup)); + static assert(isAbsolute("/foo")); + } + + version (Windows) + { + assert(isAbsolute("d:\\"w)); + assert(isAbsolute("d:\\foo"d)); + assert(isAbsolute("\\\\foo\\bar")); + assert(!isAbsolute("\\"w.dup)); + assert(!isAbsolute("\\foo"d.dup)); + assert(!isAbsolute("d:")); + assert(!isAbsolute("d:foo")); + static assert(isAbsolute(`d:\foo`)); + } + + { + auto r = MockRange!(immutable(char))(`../foo`); + assert(!r.isAbsolute()); + } + + static struct DirEntry { string s; alias s this; } + assert(!isAbsolute(DirEntry("foo"))); +} + + + + +/** Transforms $(D path) into an absolute _path. + + The following algorithm is used: + $(OL + $(LI If $(D path) is empty, return $(D null).) + $(LI If $(D path) is already absolute, return it.) + $(LI Otherwise, append $(D path) to $(D base) and return + the result. If $(D base) is not specified, the current + working directory is used.) + ) + The function allocates memory if and only if it gets to the third stage + of this algorithm. + + Params: + path = the relative path to transform + base = the base directory of the relative path + + Returns: + string of transformed path + + Throws: + $(D Exception) if the specified _base directory is not absolute. + + See_Also: + $(LREF asAbsolutePath) which does not allocate +*/ +string absolutePath(string path, lazy string base = getcwd()) + @safe pure +{ + import std.array : array; + if (path.empty) return null; + if (isAbsolute(path)) return path; + auto baseVar = base; + if (!isAbsolute(baseVar)) throw new Exception("Base directory must be absolute"); + return chainPath(baseVar, path).array; +} + +/// +@safe unittest +{ + version (Posix) + { + assert(absolutePath("some/file", "/foo/bar") == "/foo/bar/some/file"); + assert(absolutePath("../file", "/foo/bar") == "/foo/bar/../file"); + assert(absolutePath("/some/file", "/foo/bar") == "/some/file"); + } + + version (Windows) + { + assert(absolutePath(`some\file`, `c:\foo\bar`) == `c:\foo\bar\some\file`); + assert(absolutePath(`..\file`, `c:\foo\bar`) == `c:\foo\bar\..\file`); + assert(absolutePath(`c:\some\file`, `c:\foo\bar`) == `c:\some\file`); + assert(absolutePath(`\`, `c:\`) == `c:\`); + assert(absolutePath(`\some\file`, `c:\foo\bar`) == `c:\some\file`); + } +} + +@safe unittest +{ + version (Posix) + { + static assert(absolutePath("some/file", "/foo/bar") == "/foo/bar/some/file"); + } + + version (Windows) + { + static assert(absolutePath(`some\file`, `c:\foo\bar`) == `c:\foo\bar\some\file`); + } + + import std.exception; + assertThrown(absolutePath("bar", "foo")); +} + +/** Transforms $(D path) into an absolute _path. + + The following algorithm is used: + $(OL + $(LI If $(D path) is empty, return $(D null).) + $(LI If $(D path) is already absolute, return it.) + $(LI Otherwise, append $(D path) to the current working directory, + which allocates memory.) + ) + + Params: + path = the relative path to transform + + Returns: + the transformed path as a lazy range + + See_Also: + $(LREF absolutePath) which returns an allocated string +*/ +auto asAbsolutePath(R)(R path) +if ((isRandomAccessRange!R && isSomeChar!(ElementType!R) || + isNarrowString!R) && + !isConvertibleToString!R) +{ + import std.file : getcwd; + string base = null; + if (!path.empty && !isAbsolute(path)) + base = getcwd(); + return chainPath(base, path); +} + +/// +@system unittest +{ + import std.array; + assert(asAbsolutePath(cast(string) null).array == ""); + version (Posix) + { + assert(asAbsolutePath("/foo").array == "/foo"); + } + version (Windows) + { + assert(asAbsolutePath("c:/foo").array == "c:/foo"); + } + asAbsolutePath("foo"); +} + +auto asAbsolutePath(R)(auto ref R path) +if (isConvertibleToString!R) +{ + return asAbsolutePath!(StringTypeOf!R)(path); +} + +@system unittest +{ + assert(testAliasedString!asAbsolutePath(null)); +} + +/** Translates $(D path) into a relative _path. + + The returned _path is relative to $(D base), which is by default + taken to be the current working directory. If specified, + $(D base) must be an absolute _path, and it is always assumed + to refer to a directory. If $(D path) and $(D base) refer to + the same directory, the function returns $(D `.`). + + The following algorithm is used: + $(OL + $(LI If $(D path) is a relative directory, return it unaltered.) + $(LI Find a common root between $(D path) and $(D base). + If there is no common root, return $(D path) unaltered.) + $(LI Prepare a string with as many $(D `../`) or $(D `..\`) as + necessary to reach the common root from base path.) + $(LI Append the remaining segments of $(D path) to the string + and return.) + ) + + In the second step, path components are compared using $(D filenameCmp!cs), + where $(D cs) is an optional template parameter determining whether + the comparison is case sensitive or not. See the + $(LREF filenameCmp) documentation for details. + + This function allocates memory. + + Params: + cs = Whether matching path name components against the base path should + be case-sensitive or not. + path = A path name. + base = The base path to construct the relative path from. + + Returns: The relative path. + + See_Also: + $(LREF asRelativePath) which does not allocate memory + + Throws: + $(D Exception) if the specified _base directory is not absolute. +*/ +string relativePath(CaseSensitive cs = CaseSensitive.osDefault) + (string path, lazy string base = getcwd()) +{ + if (!isAbsolute(path)) + return path; + auto baseVar = base; + if (!isAbsolute(baseVar)) + throw new Exception("Base directory must be absolute"); + + import std.conv : to; + return asRelativePath!cs(path, baseVar).to!string; +} + +/// +@system unittest +{ + assert(relativePath("foo") == "foo"); + + version (Posix) + { + assert(relativePath("foo", "/bar") == "foo"); + assert(relativePath("/foo/bar", "/foo/bar") == "."); + assert(relativePath("/foo/bar", "/foo/baz") == "../bar"); + assert(relativePath("/foo/bar/baz", "/foo/woo/wee") == "../../bar/baz"); + assert(relativePath("/foo/bar/baz", "/foo/bar") == "baz"); + } + version (Windows) + { + assert(relativePath("foo", `c:\bar`) == "foo"); + assert(relativePath(`c:\foo\bar`, `c:\foo\bar`) == "."); + assert(relativePath(`c:\foo\bar`, `c:\foo\baz`) == `..\bar`); + assert(relativePath(`c:\foo\bar\baz`, `c:\foo\woo\wee`) == `..\..\bar\baz`); + assert(relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz"); + assert(relativePath(`c:\foo\bar`, `d:\foo`) == `c:\foo\bar`); + } +} + +@system unittest +{ + import std.exception; + assert(relativePath("foo") == "foo"); + version (Posix) + { + relativePath("/foo"); + assert(relativePath("/foo/bar", "/foo/baz") == "../bar"); + assertThrown(relativePath("/foo", "bar")); + } + else version (Windows) + { + relativePath(`\foo`); + assert(relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz"); + assertThrown(relativePath(`c:\foo`, "bar")); + } + else static assert(0); +} + +/** Transforms `path` into a _path relative to `base`. + + The returned _path is relative to `base`, which is usually + the current working directory. + `base` must be an absolute _path, and it is always assumed + to refer to a directory. If `path` and `base` refer to + the same directory, the function returns `'.'`. + + The following algorithm is used: + $(OL + $(LI If `path` is a relative directory, return it unaltered.) + $(LI Find a common root between `path` and `base`. + If there is no common root, return `path` unaltered.) + $(LI Prepare a string with as many `../` or `..\` as + necessary to reach the common root from base path.) + $(LI Append the remaining segments of `path` to the string + and return.) + ) + + In the second step, path components are compared using `filenameCmp!cs`, + where `cs` is an optional template parameter determining whether + the comparison is case sensitive or not. See the + $(LREF filenameCmp) documentation for details. + + Params: + path = _path to transform + base = absolute path + cs = whether filespec comparisons are sensitive or not; defaults to + `CaseSensitive.osDefault` + + Returns: + a random access range of the transformed _path + + See_Also: + $(LREF relativePath) +*/ +auto asRelativePath(CaseSensitive cs = CaseSensitive.osDefault, R1, R2) + (R1 path, R2 base) +if ((isNarrowString!R1 || + (isRandomAccessRange!R1 && hasSlicing!R1 && isSomeChar!(ElementType!R1)) && + !isConvertibleToString!R1) && + (isNarrowString!R2 || + (isRandomAccessRange!R2 && hasSlicing!R2 && isSomeChar!(ElementType!R2)) && + !isConvertibleToString!R2)) +{ + bool choosePath = !isAbsolute(path); + + // Find common root with current working directory + + auto basePS = pathSplitter(base); + auto pathPS = pathSplitter(path); + choosePath |= filenameCmp!cs(basePS.front, pathPS.front) != 0; + + basePS.popFront(); + pathPS.popFront(); + + import std.algorithm.comparison : mismatch; + import std.algorithm.iteration : joiner; + import std.array : array; + import std.range.primitives : walkLength; + import std.range : repeat, chain, choose; + import std.utf : byCodeUnit, byChar; + + // Remove matching prefix from basePS and pathPS + auto tup = mismatch!((a, b) => filenameCmp!cs(a, b) == 0)(basePS, pathPS); + basePS = tup[0]; + pathPS = tup[1]; + + string sep; + if (basePS.empty && pathPS.empty) + sep = "."; // if base == path, this is the return + else if (!basePS.empty && !pathPS.empty) + sep = dirSeparator; + + // Append as many "../" as necessary to reach common base from path + auto r1 = ".." + .byChar + .repeat(basePS.walkLength()) + .joiner(dirSeparator.byChar); + + auto r2 = pathPS + .joiner(dirSeparator.byChar) + .byChar; + + // Return (r1 ~ sep ~ r2) + return choose(choosePath, path.byCodeUnit, chain(r1, sep.byChar, r2)); +} + +/// +@system unittest +{ + import std.array; + version (Posix) + { + assert(asRelativePath("foo", "/bar").array == "foo"); + assert(asRelativePath("/foo/bar", "/foo/bar").array == "."); + assert(asRelativePath("/foo/bar", "/foo/baz").array == "../bar"); + assert(asRelativePath("/foo/bar/baz", "/foo/woo/wee").array == "../../bar/baz"); + assert(asRelativePath("/foo/bar/baz", "/foo/bar").array == "baz"); + } + else version (Windows) + { + assert(asRelativePath("foo", `c:\bar`).array == "foo"); + assert(asRelativePath(`c:\foo\bar`, `c:\foo\bar`).array == "."); + assert(asRelativePath(`c:\foo\bar`, `c:\foo\baz`).array == `..\bar`); + assert(asRelativePath(`c:\foo\bar\baz`, `c:\foo\woo\wee`).array == `..\..\bar\baz`); + assert(asRelativePath(`c:/foo/bar/baz`, `c:\foo\woo\wee`).array == `..\..\bar\baz`); + assert(asRelativePath(`c:\foo\bar\baz`, `c:\foo\bar`).array == "baz"); + assert(asRelativePath(`c:\foo\bar`, `d:\foo`).array == `c:\foo\bar`); + assert(asRelativePath(`\\foo\bar`, `c:\foo`).array == `\\foo\bar`); + } + else + static assert(0); +} + +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); +} + +@system unittest +{ + import std.array; + version (Posix) + assert(asRelativePath(TestAliasedString("foo"), TestAliasedString("/bar")).array == "foo"); + else version (Windows) + assert(asRelativePath(TestAliasedString("foo"), TestAliasedString(`c:\bar`)).array == "foo"); + assert(asRelativePath(TestAliasedString("foo"), "bar").array == "foo"); + assert(asRelativePath("foo", TestAliasedString("bar")).array == "foo"); + assert(asRelativePath(TestAliasedString("foo"), TestAliasedString("bar")).array == "foo"); + import std.utf : byDchar; + assert(asRelativePath("foo"d.byDchar, TestAliasedString("bar")).array == "foo"); +} + +@system unittest +{ + import std.array, std.utf : bCU=byCodeUnit; + version (Posix) + { + assert(asRelativePath("/foo/bar/baz".bCU, "/foo/bar".bCU).array == "baz"); + assert(asRelativePath("/foo/bar/baz"w.bCU, "/foo/bar"w.bCU).array == "baz"w); + assert(asRelativePath("/foo/bar/baz"d.bCU, "/foo/bar"d.bCU).array == "baz"d); + } + else version (Windows) + { + assert(asRelativePath(`\\foo\bar`.bCU, `c:\foo`.bCU).array == `\\foo\bar`); + assert(asRelativePath(`\\foo\bar`w.bCU, `c:\foo`w.bCU).array == `\\foo\bar`w); + assert(asRelativePath(`\\foo\bar`d.bCU, `c:\foo`d.bCU).array == `\\foo\bar`d); + } +} + +/** Compares filename characters. + + This function can perform a case-sensitive or a case-insensitive + comparison. This is controlled through the $(D cs) template parameter + which, if not specified, is given by $(LREF CaseSensitive)$(D .osDefault). + + On Windows, the backslash and slash characters ($(D `\`) and $(D `/`)) + are considered equal. + + Params: + cs = Case-sensitivity of the comparison. + a = A filename character. + b = A filename character. + + Returns: + $(D < 0) if $(D a < b), + $(D 0) if $(D a == b), and + $(D > 0) if $(D a > b). +*/ +int filenameCharCmp(CaseSensitive cs = CaseSensitive.osDefault)(dchar a, dchar b) + @safe pure nothrow +{ + if (isDirSeparator(a) && isDirSeparator(b)) return 0; + static if (!cs) + { + import std.uni : toLower; + a = toLower(a); + b = toLower(b); + } + return cast(int)(a - b); +} + +/// +@safe unittest +{ + assert(filenameCharCmp('a', 'a') == 0); + assert(filenameCharCmp('a', 'b') < 0); + assert(filenameCharCmp('b', 'a') > 0); + + version (linux) + { + // Same as calling filenameCharCmp!(CaseSensitive.yes)(a, b) + assert(filenameCharCmp('A', 'a') < 0); + assert(filenameCharCmp('a', 'A') > 0); + } + version (Windows) + { + // Same as calling filenameCharCmp!(CaseSensitive.no)(a, b) + assert(filenameCharCmp('a', 'A') == 0); + assert(filenameCharCmp('a', 'B') < 0); + assert(filenameCharCmp('A', 'b') < 0); + } +} + +@safe unittest +{ + assert(filenameCharCmp!(CaseSensitive.yes)('A', 'a') < 0); + assert(filenameCharCmp!(CaseSensitive.yes)('a', 'A') > 0); + + assert(filenameCharCmp!(CaseSensitive.no)('a', 'a') == 0); + assert(filenameCharCmp!(CaseSensitive.no)('a', 'b') < 0); + assert(filenameCharCmp!(CaseSensitive.no)('b', 'a') > 0); + assert(filenameCharCmp!(CaseSensitive.no)('A', 'a') == 0); + assert(filenameCharCmp!(CaseSensitive.no)('a', 'A') == 0); + assert(filenameCharCmp!(CaseSensitive.no)('a', 'B') < 0); + assert(filenameCharCmp!(CaseSensitive.no)('B', 'a') > 0); + assert(filenameCharCmp!(CaseSensitive.no)('A', 'b') < 0); + assert(filenameCharCmp!(CaseSensitive.no)('b', 'A') > 0); + + version (Posix) assert(filenameCharCmp('\\', '/') != 0); + version (Windows) assert(filenameCharCmp('\\', '/') == 0); +} + + +/** Compares file names and returns + + Individual characters are compared using $(D filenameCharCmp!cs), + where $(D cs) is an optional template parameter determining whether + the comparison is case sensitive or not. + + Treatment of invalid UTF encodings is implementation defined. + + Params: + cs = case sensitivity + filename1 = range for first file name + filename2 = range for second file name + + Returns: + $(D < 0) if $(D filename1 < filename2), + $(D 0) if $(D filename1 == filename2) and + $(D > 0) if $(D filename1 > filename2). + + See_Also: + $(LREF filenameCharCmp) +*/ +int filenameCmp(CaseSensitive cs = CaseSensitive.osDefault, Range1, Range2) + (Range1 filename1, Range2 filename2) +if (isInputRange!Range1 && !isInfinite!Range1 && + isSomeChar!(ElementEncodingType!Range1) && + !isConvertibleToString!Range1 && + isInputRange!Range2 && !isInfinite!Range2 && + isSomeChar!(ElementEncodingType!Range2) && + !isConvertibleToString!Range2) +{ + alias C1 = Unqual!(ElementEncodingType!Range1); + alias C2 = Unqual!(ElementEncodingType!Range2); + + static if (!cs && (C1.sizeof < 4 || C2.sizeof < 4) || + C1.sizeof != C2.sizeof) + { + // Case insensitive - decode so case is checkable + // Different char sizes - decode to bring to common type + import std.utf : byDchar; + return filenameCmp!cs(filename1.byDchar, filename2.byDchar); + } + else static if (isSomeString!Range1 && C1.sizeof < 4 || + isSomeString!Range2 && C2.sizeof < 4) + { + // Avoid autodecoding + import std.utf : byCodeUnit; + return filenameCmp!cs(filename1.byCodeUnit, filename2.byCodeUnit); + } + else + { + for (;;) + { + if (filename1.empty) return -(cast(int) !filename2.empty); + if (filename2.empty) return 1; + const c = filenameCharCmp!cs(filename1.front, filename2.front); + if (c != 0) return c; + filename1.popFront(); + filename2.popFront(); + } + } +} + +/// +@safe unittest +{ + assert(filenameCmp("abc", "abc") == 0); + assert(filenameCmp("abc", "abd") < 0); + assert(filenameCmp("abc", "abb") > 0); + assert(filenameCmp("abc", "abcd") < 0); + assert(filenameCmp("abcd", "abc") > 0); + + version (linux) + { + // Same as calling filenameCmp!(CaseSensitive.yes)(filename1, filename2) + assert(filenameCmp("Abc", "abc") < 0); + assert(filenameCmp("abc", "Abc") > 0); + } + version (Windows) + { + // Same as calling filenameCmp!(CaseSensitive.no)(filename1, filename2) + assert(filenameCmp("Abc", "abc") == 0); + assert(filenameCmp("abc", "Abc") == 0); + assert(filenameCmp("Abc", "abD") < 0); + assert(filenameCmp("abc", "AbB") > 0); + } +} + +int filenameCmp(CaseSensitive cs = CaseSensitive.osDefault, Range1, Range2) + (auto ref Range1 filename1, auto ref Range2 filename2) +if (isConvertibleToString!Range1 || isConvertibleToString!Range2) +{ + import std.meta : staticMap; + alias Types = staticMap!(convertToString, Range1, Range2); + return filenameCmp!(cs, Types)(filename1, filename2); +} + +@safe unittest +{ + assert(filenameCmp!(CaseSensitive.yes)(TestAliasedString("Abc"), "abc") < 0); + assert(filenameCmp!(CaseSensitive.yes)("Abc", TestAliasedString("abc")) < 0); + assert(filenameCmp!(CaseSensitive.yes)(TestAliasedString("Abc"), TestAliasedString("abc")) < 0); +} + +@safe unittest +{ + assert(filenameCmp!(CaseSensitive.yes)("Abc", "abc") < 0); + assert(filenameCmp!(CaseSensitive.yes)("abc", "Abc") > 0); + + assert(filenameCmp!(CaseSensitive.no)("abc", "abc") == 0); + assert(filenameCmp!(CaseSensitive.no)("abc", "abd") < 0); + assert(filenameCmp!(CaseSensitive.no)("abc", "abb") > 0); + assert(filenameCmp!(CaseSensitive.no)("abc", "abcd") < 0); + assert(filenameCmp!(CaseSensitive.no)("abcd", "abc") > 0); + assert(filenameCmp!(CaseSensitive.no)("Abc", "abc") == 0); + assert(filenameCmp!(CaseSensitive.no)("abc", "Abc") == 0); + assert(filenameCmp!(CaseSensitive.no)("Abc", "abD") < 0); + assert(filenameCmp!(CaseSensitive.no)("abc", "AbB") > 0); + + version (Posix) assert(filenameCmp(`abc\def`, `abc/def`) != 0); + version (Windows) assert(filenameCmp(`abc\def`, `abc/def`) == 0); +} + +/** Matches a pattern against a path. + + Some characters of pattern have a special meaning (they are + $(I meta-characters)) and can't be escaped. These are: + + $(BOOKTABLE, + $(TR $(TD $(D *)) + $(TD Matches 0 or more instances of any character.)) + $(TR $(TD $(D ?)) + $(TD Matches exactly one instance of any character.)) + $(TR $(TD $(D [)$(I chars)$(D ])) + $(TD Matches one instance of any character that appears + between the brackets.)) + $(TR $(TD $(D [!)$(I chars)$(D ])) + $(TD Matches one instance of any character that does not + appear between the brackets after the exclamation mark.)) + $(TR $(TD $(D {)$(I string1)$(D ,)$(I string2)$(D ,)…$(D })) + $(TD Matches either of the specified strings.)) + ) + + Individual characters are compared using $(D filenameCharCmp!cs), + where $(D cs) is an optional template parameter determining whether + the comparison is case sensitive or not. See the + $(LREF filenameCharCmp) documentation for details. + + Note that directory + separators and dots don't stop a meta-character from matching + further portions of the path. + + Params: + cs = Whether the matching should be case-sensitive + path = The path to be matched against + pattern = The glob pattern + + Returns: + $(D true) if pattern matches path, $(D false) otherwise. + + See_also: + $(LINK2 http://en.wikipedia.org/wiki/Glob_%28programming%29,Wikipedia: _glob (programming)) + */ +bool globMatch(CaseSensitive cs = CaseSensitive.osDefault, C, Range) + (Range path, const(C)[] pattern) + @safe pure nothrow +if (isForwardRange!Range && !isInfinite!Range && + isSomeChar!(ElementEncodingType!Range) && !isConvertibleToString!Range && + isSomeChar!C && is(Unqual!C == Unqual!(ElementEncodingType!Range))) +in +{ + // Verify that pattern[] is valid + import std.algorithm.searching : balancedParens; + assert(balancedParens(pattern, '[', ']', 0)); + assert(balancedParens(pattern, '{', '}', 0)); +} +body +{ + alias RC = Unqual!(ElementEncodingType!Range); + + static if (RC.sizeof == 1 && isSomeString!Range) + { + import std.utf : byChar; + return globMatch!cs(path.byChar, pattern); + } + else static if (RC.sizeof == 2 && isSomeString!Range) + { + import std.utf : byWchar; + return globMatch!cs(path.byWchar, pattern); + } + else + { + C[] pattmp; + foreach (ref pi; 0 .. pattern.length) + { + const pc = pattern[pi]; + switch (pc) + { + case '*': + if (pi + 1 == pattern.length) + return true; + for (; !path.empty; path.popFront()) + { + auto p = path.save; + if (globMatch!(cs, C)(p, + pattern[pi + 1 .. pattern.length])) + return true; + } + return false; + + case '?': + if (path.empty) + return false; + path.popFront(); + break; + + case '[': + if (path.empty) + return false; + auto nc = path.front; + path.popFront(); + auto not = false; + ++pi; + if (pattern[pi] == '!') + { + not = true; + ++pi; + } + auto anymatch = false; + while (1) + { + const pc2 = pattern[pi]; + if (pc2 == ']') + break; + if (!anymatch && (filenameCharCmp!cs(nc, pc2) == 0)) + anymatch = true; + ++pi; + } + if (anymatch == not) + return false; + break; + + case '{': + // find end of {} section + auto piRemain = pi; + for (; piRemain < pattern.length + && pattern[piRemain] != '}'; ++piRemain) + { } + + if (piRemain < pattern.length) + ++piRemain; + ++pi; + + while (pi < pattern.length) + { + const pi0 = pi; + C pc3 = pattern[pi]; + // find end of current alternative + for (; pi < pattern.length && pc3 != '}' && pc3 != ','; ++pi) + { + pc3 = pattern[pi]; + } + + auto p = path.save; + if (pi0 == pi) + { + if (globMatch!(cs, C)(p, pattern[piRemain..$])) + { + return true; + } + ++pi; + } + else + { + /* Match for: + * pattern[pi0 .. pi-1] ~ pattern[piRemain..$] + */ + if (pattmp is null) + // Allocate this only once per function invocation. + // Should do it with malloc/free, but that would make it impure. + pattmp = new C[pattern.length]; + + const len1 = pi - 1 - pi0; + pattmp[0 .. len1] = pattern[pi0 .. pi - 1]; + + const len2 = pattern.length - piRemain; + pattmp[len1 .. len1 + len2] = pattern[piRemain .. $]; + + if (globMatch!(cs, C)(p, pattmp[0 .. len1 + len2])) + { + return true; + } + } + if (pc3 == '}') + { + break; + } + } + return false; + + default: + if (path.empty) + return false; + if (filenameCharCmp!cs(pc, path.front) != 0) + return false; + path.popFront(); + break; + } + } + return path.empty; + } +} + +/// +@safe unittest +{ + assert(globMatch("foo.bar", "*")); + assert(globMatch("foo.bar", "*.*")); + assert(globMatch(`foo/foo\bar`, "f*b*r")); + assert(globMatch("foo.bar", "f???bar")); + assert(globMatch("foo.bar", "[fg]???bar")); + assert(globMatch("foo.bar", "[!gh]*bar")); + assert(globMatch("bar.fooz", "bar.{foo,bif}z")); + assert(globMatch("bar.bifz", "bar.{foo,bif}z")); + + version (Windows) + { + // Same as calling globMatch!(CaseSensitive.no)(path, pattern) + assert(globMatch("foo", "Foo")); + assert(globMatch("Goo.bar", "[fg]???bar")); + } + version (linux) + { + // Same as calling globMatch!(CaseSensitive.yes)(path, pattern) + assert(!globMatch("foo", "Foo")); + assert(!globMatch("Goo.bar", "[fg]???bar")); + } +} + +bool globMatch(CaseSensitive cs = CaseSensitive.osDefault, C, Range) + (auto ref Range path, const(C)[] pattern) + @safe pure nothrow +if (isConvertibleToString!Range) +{ + return globMatch!(cs, C, StringTypeOf!Range)(path, pattern); +} + +@safe unittest +{ + assert(testAliasedString!globMatch("foo.bar", "*")); +} + +@safe unittest +{ + assert(globMatch!(CaseSensitive.no)("foo", "Foo")); + assert(!globMatch!(CaseSensitive.yes)("foo", "Foo")); + + assert(globMatch("foo", "*")); + assert(globMatch("foo.bar"w, "*"w)); + assert(globMatch("foo.bar"d, "*.*"d)); + assert(globMatch("foo.bar", "foo*")); + assert(globMatch("foo.bar"w, "f*bar"w)); + assert(globMatch("foo.bar"d, "f*b*r"d)); + assert(globMatch("foo.bar", "f???bar")); + assert(globMatch("foo.bar"w, "[fg]???bar"w)); + assert(globMatch("foo.bar"d, "[!gh]*bar"d)); + + assert(!globMatch("foo", "bar")); + assert(!globMatch("foo"w, "*.*"w)); + assert(!globMatch("foo.bar"d, "f*baz"d)); + assert(!globMatch("foo.bar", "f*b*x")); + assert(!globMatch("foo.bar", "[gh]???bar")); + assert(!globMatch("foo.bar"w, "[!fg]*bar"w)); + assert(!globMatch("foo.bar"d, "[fg]???baz"d)); + assert(!globMatch("foo.di", "*.d")); // test issue 6634: triggered bad assertion + + assert(globMatch("foo.bar", "{foo,bif}.bar")); + assert(globMatch("bif.bar"w, "{foo,bif}.bar"w)); + + assert(globMatch("bar.foo"d, "bar.{foo,bif}"d)); + assert(globMatch("bar.bif", "bar.{foo,bif}")); + + assert(globMatch("bar.fooz"w, "bar.{foo,bif}z"w)); + assert(globMatch("bar.bifz"d, "bar.{foo,bif}z"d)); + + assert(globMatch("bar.foo", "bar.{biz,,baz}foo")); + assert(globMatch("bar.foo"w, "bar.{biz,}foo"w)); + assert(globMatch("bar.foo"d, "bar.{,biz}foo"d)); + assert(globMatch("bar.foo", "bar.{}foo")); + + assert(globMatch("bar.foo"w, "bar.{ar,,fo}o"w)); + assert(globMatch("bar.foo"d, "bar.{,ar,fo}o"d)); + assert(globMatch("bar.o", "bar.{,ar,fo}o")); + + assert(!globMatch("foo", "foo?")); + assert(!globMatch("foo", "foo[]")); + assert(!globMatch("foo", "foob")); + assert(!globMatch("foo", "foo{b}")); + + + static assert(globMatch("foo.bar", "[!gh]*bar")); +} + + + + +/** Checks that the given file or directory name is valid. + + The maximum length of $(D filename) is given by the constant + $(D core.stdc.stdio.FILENAME_MAX). (On Windows, this number is + defined as the maximum number of UTF-16 code points, and the + test will therefore only yield strictly correct results when + $(D filename) is a string of $(D wchar)s.) + + On Windows, the following criteria must be satisfied + ($(LINK2 http://msdn.microsoft.com/en-us/library/aa365247(v=vs.85).aspx,source)): + $(UL + $(LI $(D filename) must not contain any characters whose integer + representation is in the range 0-31.) + $(LI $(D filename) must not contain any of the following $(I reserved + characters): <>:"/\|?*) + $(LI $(D filename) may not end with a space ($(D ' ')) or a period + ($(D '.')).) + ) + + On POSIX, $(D filename) may not contain a forward slash ($(D '/')) or + the null character ($(D '\0')). + + Params: + filename = string to check + + Returns: + $(D true) if and only if $(D filename) is not + empty, not too long, and does not contain invalid characters. + +*/ +bool isValidFilename(Range)(Range filename) +if ((isRandomAccessRange!Range && hasLength!Range && hasSlicing!Range && isSomeChar!(ElementEncodingType!Range) || + isNarrowString!Range) && + !isConvertibleToString!Range) +{ + import core.stdc.stdio : FILENAME_MAX; + if (filename.length == 0 || filename.length >= FILENAME_MAX) return false; + foreach (c; filename) + { + version (Windows) + { + switch (c) + { + case 0: + .. + case 31: + case '<': + case '>': + case ':': + case '"': + case '/': + case '\\': + case '|': + case '?': + case '*': + return false; + + default: + break; + } + } + else version (Posix) + { + if (c == 0 || c == '/') return false; + } + else static assert(0); + } + version (Windows) + { + auto last = filename[filename.length - 1]; + if (last == '.' || last == ' ') return false; + } + + // All criteria passed + return true; +} + +/// +@safe pure @nogc nothrow +unittest +{ + import std.utf : byCodeUnit; + + assert(isValidFilename("hello.exe".byCodeUnit)); +} + +bool isValidFilename(Range)(auto ref Range filename) +if (isConvertibleToString!Range) +{ + return isValidFilename!(StringTypeOf!Range)(filename); +} + +@safe unittest +{ + assert(testAliasedString!isValidFilename("hello.exe")); +} + +@safe pure +unittest +{ + import std.conv; + auto valid = ["foo"]; + auto invalid = ["", "foo\0bar", "foo/bar"]; + auto pfdep = [`foo\bar`, "*.txt"]; + version (Windows) invalid ~= pfdep; + else version (Posix) valid ~= pfdep; + else static assert(0); + + import std.meta : AliasSeq; + foreach (T; AliasSeq!(char[], const(char)[], string, wchar[], + const(wchar)[], wstring, dchar[], const(dchar)[], dstring)) + { + foreach (fn; valid) + assert(isValidFilename(to!T(fn))); + foreach (fn; invalid) + assert(!isValidFilename(to!T(fn))); + } + + { + auto r = MockRange!(immutable(char))(`dir/file.d`); + assert(!isValidFilename(r)); + } + + static struct DirEntry { string s; alias s this; } + assert(isValidFilename(DirEntry("file.ext"))); + + version (Windows) + { + immutable string cases = "<>:\"/\\|?*"; + foreach (i; 0 .. 31 + cases.length) + { + char[3] buf; + buf[0] = 'a'; + buf[1] = i <= 31 ? cast(char) i : cases[i - 32]; + buf[2] = 'b'; + assert(!isValidFilename(buf[])); + } + } +} + + + +/** Checks whether $(D path) is a valid _path. + + Generally, this function checks that $(D path) is not empty, and that + each component of the path either satisfies $(LREF isValidFilename) + or is equal to $(D ".") or $(D ".."). + + $(B It does $(I not) check whether the _path points to an existing file + or directory; use $(REF exists, std,file) for this purpose.) + + On Windows, some special rules apply: + $(UL + $(LI If the second character of $(D path) is a colon ($(D ':')), + the first character is interpreted as a drive letter, and + must be in the range A-Z (case insensitive).) + $(LI If $(D path) is on the form $(D `\\$(I server)\$(I share)\...`) + (UNC path), $(LREF isValidFilename) is applied to $(I server) + and $(I share) as well.) + $(LI If $(D path) starts with $(D `\\?\`) (long UNC path), the + only requirement for the rest of the string is that it does + not contain the null character.) + $(LI If $(D path) starts with $(D `\\.\`) (Win32 device namespace) + this function returns $(D false); such paths are beyond the scope + of this module.) + ) + + Params: + path = string or Range of characters to check + + Returns: + true if $(D path) is a valid _path. +*/ +bool isValidPath(Range)(Range path) +if ((isRandomAccessRange!Range && hasLength!Range && hasSlicing!Range && isSomeChar!(ElementEncodingType!Range) || + isNarrowString!Range) && + !isConvertibleToString!Range) +{ + alias C = Unqual!(ElementEncodingType!Range); + + if (path.empty) return false; + + // Check whether component is "." or "..", or whether it satisfies + // isValidFilename. + bool isValidComponent(Range component) + { + assert(component.length > 0); + if (component[0] == '.') + { + if (component.length == 1) return true; + else if (component.length == 2 && component[1] == '.') return true; + } + return isValidFilename(component); + } + + if (path.length == 1) + return isDirSeparator(path[0]) || isValidComponent(path); + + Range remainder; + version (Windows) + { + if (isDirSeparator(path[0]) && isDirSeparator(path[1])) + { + // Some kind of UNC path + if (path.length < 5) + { + // All valid UNC paths must have at least 5 characters + return false; + } + else if (path[2] == '?') + { + // Long UNC path + if (!isDirSeparator(path[3])) return false; + foreach (c; path[4 .. $]) + { + if (c == '\0') return false; + } + return true; + } + else if (path[2] == '.') + { + // Win32 device namespace not supported + return false; + } + else + { + // Normal UNC path, i.e. \\server\share\... + size_t i = 2; + while (i < path.length && !isDirSeparator(path[i])) ++i; + if (i == path.length || !isValidFilename(path[2 .. i])) + return false; + ++i; // Skip a single dir separator + size_t j = i; + while (j < path.length && !isDirSeparator(path[j])) ++j; + if (!isValidFilename(path[i .. j])) return false; + remainder = path[j .. $]; + } + } + else if (isDriveSeparator(path[1])) + { + import std.ascii : isAlpha; + if (!isAlpha(path[0])) return false; + remainder = path[2 .. $]; + } + else + { + remainder = path; + } + } + else version (Posix) + { + remainder = path; + } + else static assert(0); + remainder = ltrimDirSeparators(remainder); + + // Check that each component satisfies isValidComponent. + while (!remainder.empty) + { + size_t i = 0; + while (i < remainder.length && !isDirSeparator(remainder[i])) ++i; + assert(i > 0); + if (!isValidComponent(remainder[0 .. i])) return false; + remainder = ltrimDirSeparators(remainder[i .. $]); + } + + // All criteria passed + return true; +} + +/// +@safe pure @nogc nothrow +unittest +{ + assert(isValidPath("/foo/bar")); + assert(!isValidPath("/foo\0/bar")); + assert(isValidPath("/")); + assert(isValidPath("a")); + + version (Windows) + { + assert(isValidPath(`c:\`)); + assert(isValidPath(`c:\foo`)); + assert(isValidPath(`c:\foo\.\bar\\\..\`)); + assert(!isValidPath(`!:\foo`)); + assert(!isValidPath(`c::\foo`)); + assert(!isValidPath(`c:\foo?`)); + assert(!isValidPath(`c:\foo.`)); + + assert(isValidPath(`\\server\share`)); + assert(isValidPath(`\\server\share\foo`)); + assert(isValidPath(`\\server\share\\foo`)); + assert(!isValidPath(`\\\server\share\foo`)); + assert(!isValidPath(`\\server\\share\foo`)); + assert(!isValidPath(`\\ser*er\share\foo`)); + assert(!isValidPath(`\\server\sha?e\foo`)); + assert(!isValidPath(`\\server\share\|oo`)); + + assert(isValidPath(`\\?\<>:"?*|/\..\.`)); + assert(!isValidPath("\\\\?\\foo\0bar")); + + assert(!isValidPath(`\\.\PhysicalDisk1`)); + assert(!isValidPath(`\\`)); + } + + import std.utf : byCodeUnit; + assert(isValidPath("/foo/bar".byCodeUnit)); +} + +bool isValidPath(Range)(auto ref Range path) +if (isConvertibleToString!Range) +{ + return isValidPath!(StringTypeOf!Range)(path); +} + +@safe unittest +{ + assert(testAliasedString!isValidPath("/foo/bar")); +} + +/** Performs tilde expansion in paths on POSIX systems. + On Windows, this function does nothing. + + There are two ways of using tilde expansion in a path. One + involves using the tilde alone or followed by a path separator. In + this case, the tilde will be expanded with the value of the + environment variable $(D HOME). The second way is putting + a username after the tilde (i.e. $(D ~john/Mail)). Here, + the username will be searched for in the user database + (i.e. $(D /etc/passwd) on Unix systems) and will expand to + whatever path is stored there. The username is considered the + string after the tilde ending at the first instance of a path + separator. + + Note that using the $(D ~user) syntax may give different + values from just $(D ~) if the environment variable doesn't + match the value stored in the user database. + + When the environment variable version is used, the path won't + be modified if the environment variable doesn't exist or it + is empty. When the database version is used, the path won't be + modified if the user doesn't exist in the database or there is + not enough memory to perform the query. + + This function performs several memory allocations. + + Params: + inputPath = The path name to expand. + + Returns: + $(D inputPath) with the tilde expanded, or just $(D inputPath) + if it could not be expanded. + For Windows, $(D expandTilde) merely returns its argument $(D inputPath). + + Example: + ----- + void processFile(string path) + { + // Allow calling this function with paths such as ~/foo + auto fullPath = expandTilde(path); + ... + } + ----- +*/ +string expandTilde(string inputPath) nothrow +{ + version (Posix) + { + import core.exception : onOutOfMemoryError; + import core.stdc.errno : errno, ERANGE; + import core.stdc.stdlib : malloc, free, realloc; + + /* Joins a path from a C string to the remainder of path. + + The last path separator from c_path is discarded. The result + is joined to path[char_pos .. length] if char_pos is smaller + than length, otherwise path is not appended to c_path. + */ + static string combineCPathWithDPath(char* c_path, string path, size_t char_pos) nothrow + { + import core.stdc.string : strlen; + + assert(c_path != null); + assert(path.length > 0); + assert(char_pos >= 0); + + // Search end of C string + size_t end = strlen(c_path); + + // Remove trailing path separator, if any + if (end && isDirSeparator(c_path[end - 1])) + end--; + + // (this is the only GC allocation done in expandTilde()) + string cp; + if (char_pos < path.length) + // Append something from path + cp = cast(string)(c_path[0 .. end] ~ path[char_pos .. $]); + else + // Create our own copy, as lifetime of c_path is undocumented + cp = c_path[0 .. end].idup; + + return cp; + } + + // Replaces the tilde from path with the environment variable HOME. + static string expandFromEnvironment(string path) nothrow + { + import core.stdc.stdlib : getenv; + + assert(path.length >= 1); + assert(path[0] == '~'); + + // Get HOME and use that to replace the tilde. + auto home = getenv("HOME"); + if (home == null) + return path; + + return combineCPathWithDPath(home, path, 1); + } + + // Replaces the tilde from path with the path from the user database. + static string expandFromDatabase(string path) nothrow + { + // bionic doesn't really support this, as getpwnam_r + // isn't provided and getpwnam is basically just a stub + version (CRuntime_Bionic) + { + return path; + } + else + { + import core.sys.posix.pwd : passwd, getpwnam_r; + import std.string : indexOf; + + assert(path.length > 2 || (path.length == 2 && !isDirSeparator(path[1]))); + assert(path[0] == '~'); + + // Extract username, searching for path separator. + auto last_char = indexOf(path, dirSeparator[0]); + + size_t username_len = (last_char == -1) ? path.length : last_char; + char* username = cast(char*) malloc(username_len * char.sizeof); + if (!username) + onOutOfMemoryError(); + scope(exit) free(username); + + if (last_char == -1) + { + username[0 .. username_len - 1] = path[1 .. $]; + last_char = path.length + 1; + } + else + { + username[0 .. username_len - 1] = path[1 .. last_char]; + } + username[username_len - 1] = 0; + + assert(last_char > 1); + + // Reserve C memory for the getpwnam_r() function. + version (unittest) + uint extra_memory_size = 2; + else + uint extra_memory_size = 5 * 1024; + char* extra_memory; + scope(exit) free(extra_memory); + + passwd result; + while (1) + { + extra_memory = cast(char*) realloc(extra_memory, extra_memory_size * char.sizeof); + if (extra_memory == null) + onOutOfMemoryError(); + + // Obtain info from database. + passwd *verify; + errno = 0; + if (getpwnam_r(username, &result, extra_memory, extra_memory_size, + &verify) == 0) + { + // Succeeded if verify points at result + if (verify == &result) + // username is found + path = combineCPathWithDPath(result.pw_dir, path, last_char); + break; + } + + if (errno != ERANGE && + // On FreeBSD and OSX, errno can be left at 0 instead of set to ERANGE + errno != 0) + onOutOfMemoryError(); + + // extra_memory isn't large enough + import core.checkedint : mulu; + bool overflow; + extra_memory_size = mulu(extra_memory_size, 2, overflow); + if (overflow) assert(0); + } + return path; + } + } + + // Return early if there is no tilde in path. + if (inputPath.length < 1 || inputPath[0] != '~') + return inputPath; + + if (inputPath.length == 1 || isDirSeparator(inputPath[1])) + return expandFromEnvironment(inputPath); + else + return expandFromDatabase(inputPath); + } + else version (Windows) + { + // Put here real windows implementation. + return inputPath; + } + else + { + static assert(0); // Guard. Implement on other platforms. + } +} + + +version (unittest) import std.process : environment; +@system unittest +{ + version (Posix) + { + // Retrieve the current home variable. + auto oldHome = environment.get("HOME"); + + // Testing when there is no environment variable. + environment.remove("HOME"); + assert(expandTilde("~/") == "~/"); + assert(expandTilde("~") == "~"); + + // Testing when an environment variable is set. + environment["HOME"] = "dmd/test"; + assert(expandTilde("~/") == "dmd/test/"); + assert(expandTilde("~") == "dmd/test"); + + // The same, but with a variable ending in a slash. + environment["HOME"] = "dmd/test/"; + assert(expandTilde("~/") == "dmd/test/"); + assert(expandTilde("~") == "dmd/test"); + + // Recover original HOME variable before continuing. + if (oldHome !is null) environment["HOME"] = oldHome; + else environment.remove("HOME"); + + // Test user expansion for root, no /root on Android + version (OSX) + { + assert(expandTilde("~root") == "/var/root", expandTilde("~root")); + assert(expandTilde("~root/") == "/var/root/", expandTilde("~root/")); + } + else version (Android) + { + } + else + { + assert(expandTilde("~root") == "/root", expandTilde("~root")); + assert(expandTilde("~root/") == "/root/", expandTilde("~root/")); + } + assert(expandTilde("~Idontexist/hey") == "~Idontexist/hey"); + } +} + +version (unittest) +{ + /* Define a mock RandomAccessRange to use for unittesting. + */ + + struct MockRange(C) + { + this(C[] array) { this.array = array; } + const + { + @property size_t length() { return array.length; } + @property bool empty() { return array.length == 0; } + @property C front() { return array[0]; } + @property C back() { return array[$ - 1]; } + @property size_t opDollar() { return length; } + C opIndex(size_t i) { return array[i]; } + } + void popFront() { array = array[1 .. $]; } + void popBack() { array = array[0 .. $-1]; } + MockRange!C opSlice( size_t lwr, size_t upr) const + { + return MockRange!C(array[lwr .. upr]); + } + @property MockRange save() { return this; } + private: + C[] array; + } + + static assert( isRandomAccessRange!(MockRange!(const(char))) ); +} + +version (unittest) +{ + /* Define a mock BidirectionalRange to use for unittesting. + */ + + struct MockBiRange(C) + { + this(const(C)[] array) { this.array = array; } + const + { + @property bool empty() { return array.length == 0; } + @property C front() { return array[0]; } + @property C back() { return array[$ - 1]; } + @property size_t opDollar() { return array.length; } + } + void popFront() { array = array[1 .. $]; } + void popBack() { array = array[0 .. $-1]; } + @property MockBiRange save() { return this; } + private: + const(C)[] array; + } + + static assert( isBidirectionalRange!(MockBiRange!(const(char))) ); +} + +private template BaseOf(R) +{ + static if (isRandomAccessRange!R && isSomeChar!(ElementType!R)) + alias BaseOf = R; + else + alias BaseOf = StringTypeOf!R; +} |