From e01261d2a315e92fb9661dc26c457f5b9f4429e7 Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Wed, 27 Mar 2024 19:39:44 +0200 Subject: [PATCH 1/4] Add `split_windows_volume_prefix()` function. This patch adds a new public function for splitting Windows paths into its volume prefix and rest of the path. --- src/filepath.gleam | 151 +++++++++ test/filepath_test.gleam | 687 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 838 insertions(+) diff --git a/src/filepath.gleam b/src/filepath.gleam index 3dae367..2f5d63c 100644 --- a/src/filepath.gleam +++ b/src/filepath.gleam @@ -167,6 +167,157 @@ fn pop_windows_drive_specifier(path: String) -> #(Option(String), String) { } } +/// Splits the Windows volume prefix from a given Windows path, +/// returning a tuple of two Strings with the value of the volume +/// prefix and the rest of the path if the split was successful. +/// +/// Works with paths featuring `/`, `\`, or both, as long as the +/// volume prefix uses the same one consistently. +/// The orientation of the slashes in the volume prefix and the rest +/// of the path is preserved in the resulting tuple elements. +/// +/// The separator between the prefix and the rest of the path (if any) +/// will be included in the first (volume part) of the returned tuple. +/// +/// Implemens the feature that: +/// +/// ```gleam +/// let assert Ok(#(drive, rest)) = split_windows_volume_prefix(original_path) +/// drive <> rest == original_path +/// // -> True +/// ``` +/// +/// Full details on possible volume prefix syntax can be found at: +/// * [Microsoft Guide](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats) +/// * [Google Project Zero Investigation](https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html) +/// +/// ## Examples +/// +/// ```gleam +/// // Normal drive-lettered absolute path with either slashes or backslashes: +/// split_windows_volume_prefix("C:\\Users\\Administrator\\AppData") +/// // -> Ok(#("C:", "Users\\Administrator\\AppData")) +/// ``` +/// +/// ```gleam +/// // DOS Local Device ("//./DEV/..."): +/// split_windows_volume_prefix("//./pipe/testpipe") +/// // -> Ok(#("//./pipe", "testpipe")) +/// ``` +/// +/// ```gleam +/// // DOS Root Local Device ("//?/DEV/./..."): +/// split_windows_volume_prefix("//?/C:/Users/Administrator") +/// // -> Ok(#("//?/C:", "Users/Administrator")) +/// ``` +/// +/// ```gleam +/// // UNC paths will include the IP/hostname and sharename portions: +/// split_windows_volume_prefix("//DESKTOP-123/MyShare/subdir/file.txt") +/// // -> Ok(#("//DESKTOP-123/MyShare", "subdir/file.txt")) +/// ``` +/// +pub fn split_windows_volume_prefix( + path path: String, +) -> Result(#(String, String), Nil) { + case path { + // NOTE: DOS device paths may include ":" too, so we must match + // for them before matching for regular drives: + // DOS device paths: + "//." as start <> rest | "//?" as start <> rest -> { + split_rest_once(start, "/", rest) + } + "\\\\." as start <> rest | "\\\\?" as start <> rest -> { + split_rest_once(start, "\\", rest) + } + + // UNC paths where both the IP/hostname and share/drive name count + // as part of the volume prefix: + "//" as start <> rest -> { + split_rest_twice(start, "/", rest) + } + "\\\\" as start <> rest -> { + split_rest_twice(start, "\\", rest) + } + + // Check for normal absolute paths and drive-relative paths: + _ -> + case string.split_once(path, on: ":") { + Ok(#(precolon, postcolon)) -> { + case precolon { + // The colon is the first character in the string + // so there is no drive to speak of: + "" -> Error(Nil) + + precolon -> + case + #( + string.contains(precolon, "\\"), + string.contains(precolon, "/"), + ) + { + #(False, False) -> + case postcolon { + "/" <> rest -> Ok(#(precolon <> ":/", rest)) + "\\" <> rest -> Ok(#(precolon <> ":\\", rest)) + // Path is a current-drive-relative path. + // E.g: C:Users => C:\$CWD\Users + _ -> Ok(#(precolon <> ":", postcolon)) + } + // Path is an incorrect Windows path, as only the first part of + // of a non-UNC Windows path is ever allowed to have a colon. + _ -> Error(Nil) + } + } + } + // Path has no colon and is likely a relative or drive-absolute path: + Error(_) -> Error(Nil) + } + } +} + +// Helper function to extract one more path element from the `rest` of the +// path and form the final result for `split_windows_volume_prefix`. +fn split_rest_once( + start: String, + sep: String, + rest: String, +) -> Result(#(String, String), Nil) { + case string.split_once(rest, on: sep) { + Ok(#(drive, rest2)) -> { + case drive { + // The `rest` started with multiple redundant separators, + // which is acceptable, and we must recurse: + // eg: //./////pipe/testpipe + "" -> split_rest_once(start <> sep, sep, rest2) + _ -> Ok(#(start <> drive <> sep, rest2)) + } + } + Error(_) -> + case rest { + "" -> Error(Nil) + // NOTE: if the `rest` wasn't initially empty, it counts + // even if it doesn't have any `sep` in it: + _ -> Ok(#(start <> rest, "")) + } + } +} + +// Helper function to extract two more path elements from the `rest` of the +// path and form the final result for `split_windows_volume_prefix`. +fn split_rest_twice( + start: String, + sep: String, + rest: String, +) -> Result(#(String, String), Nil) { + case split_rest_once(start, sep, rest) { + Error(_) -> Error(Nil) + Ok(#(drive1, rest1)) -> { + split_rest_once(drive1, sep, rest1) + } + } +} + /// Get the file extension of a path. /// /// ## Examples diff --git a/test/filepath_test.gleam b/test/filepath_test.gleam index 475c97b..e6c2f65 100644 --- a/test/filepath_test.gleam +++ b/test/filepath_test.gleam @@ -107,6 +107,693 @@ pub fn split_windows_6_test() { |> should.equal(["::", "one", "two"]) } +pub fn split_windows_drive_prefix_0_test() { + filepath.split_windows_volume_prefix("/") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_0_inverted_test() { + filepath.split_windows_volume_prefix("\\") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_1_test() { + filepath.split_windows_volume_prefix("/usr/local/bin") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_1_inverted_test() { + filepath.split_windows_volume_prefix("\\usr\\local\\bin") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_2_test() { + filepath.split_windows_volume_prefix("") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_3_test() { + filepath.split_windows_volume_prefix("/") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_3_inverted_test() { + filepath.split_windows_volume_prefix("\\") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_4_test() { + filepath.split_windows_volume_prefix("file") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_5_test() { + filepath.split_windows_volume_prefix("dir1/dir2/file.txt") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_5_inverted_test() { + filepath.split_windows_volume_prefix("dir1\\dir2\\file.txt") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_6_test() { + filepath.split_windows_volume_prefix(":/one/two") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_6_inverted_test() { + filepath.split_windows_volume_prefix(":\\one\\two") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_7_test() { + filepath.split_windows_volume_prefix("::/one/two") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_7_inverted_test() { + filepath.split_windows_volume_prefix("::\\one\\two") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_8_test() { + filepath.split_windows_volume_prefix("./one:/two") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_8_inverted_test() { + filepath.split_windows_volume_prefix(".\\one:\\two") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_9_test() { + filepath.split_windows_volume_prefix("one/two:") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_9_inverted_test() { + filepath.split_windows_volume_prefix("one\\two:") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_10_test() { + let input = "C:" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_11_test() { + let input = "c:" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("c:", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_12_test() { + let input = "C:/" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:/", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_12_inverted_test() { + let input = "C:\\" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:\\", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_13_test() { + let input = "C:/one/two" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:/", "one/two"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_13_inverted_test() { + let input = "C:\\one\\two" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:\\", "one\\two"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_14_test() { + let input = "c:/one/two" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("c:/", "one/two"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_14_inverted_test() { + let input = "c:\\one\\two" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("c:\\", "one\\two"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_15_test() { + let input = "C:\\one\\two" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:\\", "one\\two"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_15_inverted_test() { + let input = "C:/one/two" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:/", "one/two"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_16_test() { + let input = "c:\\one\\two" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("c:\\", "one\\two"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_16_inverted_test() { + let input = "c:/one/two" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("c:/", "one/two"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_17_test() { + let input = "C:\\one\\two/three" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:\\", "one\\two/three"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_17_inverted_test() { + let input = "C:/one/two\\three" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:/", "one/two\\three"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_18_test() { + let input = "c:/one/two\\three" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("c:/", "one/two\\three"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_18_inverted_test() { + let input = "c:\\one\\two/three" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("c:\\", "one\\two/three"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_19_test() { + filepath.split_windows_volume_prefix("/dir1/dir2/file.txt") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_19_inverted_test() { + filepath.split_windows_volume_prefix("\\dir1\\dir2\\file.txt") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_20_test() { + filepath.split_windows_volume_prefix("/dir1/dir2\\file.txt") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_20_inverted_test() { + filepath.split_windows_volume_prefix("\\dir1\\dir2/file.txt") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_21_test() { + let input = "C:dir1/dir2/file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:", "dir1/dir2/file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_21_inverted_test() { + let input = "C:dir1\\dir2\\file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:", "dir1\\dir2\\file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_22_test() { + let input = "C:dir1/dir2\\file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:", "dir1/dir2\\file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_22_inverted_test() { + let input = "C:dir1\\dir2/file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("C:", "dir1\\dir2/file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_23_test() { + let input = "HKLM:" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("HKLM:", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_24_test() { + let input = "HKLM:/" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("HKLM:/", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_24_inverted_test() { + let input = "HKLM:\\" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("HKLM:\\", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_25_test() { + let input = "//./pipe" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//./pipe", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_25_inverted_test() { + let input = "\\\\.\\pipe" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\.\\pipe", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_26_test() { + let input = "//./pipe/" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//./pipe/", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_26_inverted_test() { + let input = "\\\\.\\pipe\\" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\.\\pipe\\", ""))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_27_test() { + let input = "//./pipe/testpipe" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//./pipe/", "testpipe"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_27_inverted_test() { + let input = "\\\\.\\pipe\\testpipe" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\.\\pipe\\", "testpipe"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_28_test() { + let input = "HKLM:/SOFTWARE/Microsoft/Windows/CurrentVersion" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("HKLM:/", "SOFTWARE/Microsoft/Windows/CurrentVersion"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_28_inverted_test() { + let input = "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion" + filepath.split_windows_volume_prefix(input) + |> should.equal( + Ok(#("HKLM:\\", "SOFTWARE\\Microsoft\\Windows\\CurrentVersion")), + ) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_29_test() { + let input = "//./Volume{b75e2c83-0000-0000-0000-602f00000000}/Test/Foo.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal( + Ok(#("//./Volume{b75e2c83-0000-0000-0000-602f00000000}/", "Test/Foo.txt")), + ) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_29_inverted_test() { + let input = + "\\\\.\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\Test\\Foo.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal( + Ok(#( + "\\\\.\\Volume{b75e2c83-0000-0000-0000-602f00000000}\\", + "Test\\Foo.txt", + )), + ) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_30_test() { + let input = "//LOCALHOST/c$/temp/test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//LOCALHOST/c$/", "temp/test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_30_inverted_test() { + let input = "\\\\LOCALHOST\\c$\\temp\\test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\LOCALHOST\\c$\\", "temp\\test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_31_test() { + let input = "//./c:/temp/test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//./c:/", "temp/test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_31_inverted_test() { + let input = "\\\\.\\c:\\temp\\test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\.\\c:\\", "temp\\test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_32_test() { + let input = "//?/c:/temp/test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//?/c:/", "temp/test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_32_inverted_test() { + let input = "\\\\?\\c:\\temp\\test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\?\\c:\\", "temp\\test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_33_test() { + let input = "//./UNC/LOCALHOST/c$/temp/test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//./UNC/", "LOCALHOST/c$/temp/test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_33_inverted_test() { + let input = "\\\\.\\UNC\\LOCALHOST\\c$\\temp\\test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\.\\UNC\\", "LOCALHOST\\c$\\temp\\test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_34_test() { + let input = "//?/UNC/LOCALHOST/c$/temp/test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//?/UNC/", "LOCALHOST/c$/temp/test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_34_inverted_test() { + let input = "\\\\?\\UNC\\LOCALHOST\\c$\\temp\\test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\?\\UNC\\", "LOCALHOST\\c$\\temp\\test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_35_test() { + let input = "//127.0.0.1/c$/temp/test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//127.0.0.1/c$/", "temp/test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_35_inverted_test() { + let input = "\\\\127.0.0.1\\c$\\temp\\test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\127.0.0.1\\c$\\", "temp\\test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_36_test() { + let input = "//DESKTOP-123/MyShare/subdir/file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//DESKTOP-123/MyShare/", "subdir/file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_36_inverted_test() { + let input = "\\\\DESKTOP-123\\MyShare\\subdir\\file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\DESKTOP-123\\MyShare\\", "subdir\\file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_37_test() { + filepath.split_windows_volume_prefix("//") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_37_inverted_test() { + filepath.split_windows_volume_prefix("\\\\") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_38_test() { + filepath.split_windows_volume_prefix("//.") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_38_inverted_test() { + filepath.split_windows_volume_prefix("\\\\.") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_39_test() { + filepath.split_windows_volume_prefix("//./") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_39_inverted_test() { + filepath.split_windows_volume_prefix("\\\\.\\") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_40_test() { + filepath.split_windows_volume_prefix("//?") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_40_inverted_test() { + filepath.split_windows_volume_prefix("\\\\?") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_41_test() { + filepath.split_windows_volume_prefix("//?/") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_41_inverted_test() { + filepath.split_windows_volume_prefix("\\\\?\\") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_42_test() { + filepath.split_windows_volume_prefix("//.///") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_42_inverted_test() { + filepath.split_windows_volume_prefix("\\\\.\\\\\\") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_43_test() { + filepath.split_windows_volume_prefix("//?///") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_43_inverted_test() { + filepath.split_windows_volume_prefix("\\\\?\\\\\\") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_44_test() { + filepath.split_windows_volume_prefix("//127.0.0.1") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_44_inverted_test() { + filepath.split_windows_volume_prefix("\\\\127.0.0.1") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_45_test() { + filepath.split_windows_volume_prefix("//127.0.0.1/") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_45_inverted_test() { + filepath.split_windows_volume_prefix("\\\\127.0.0.1\\") + |> should.equal(Error(Nil)) +} + +pub fn split_windows_drive_prefix_46_test() { + let input = "//./////pipe///testpipe" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//./////pipe/", "//testpipe"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_46_inverted_test() { + let input = "\\\\.\\\\\\\\\\pipe\\\\\\testpipe" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\.\\\\\\\\\\pipe\\", "\\\\testpipe"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_47_test() { + let input = "//?///////pipe///testpipe" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//?///////pipe/", "//testpipe"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_47_inverted_test() { + let input = "\\\\?\\\\\\\\\\\\\\pipe\\\\\\testpipe" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\?\\\\\\\\\\\\\\pipe\\", "\\\\testpipe"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_48_test() { + let input = "//127.0.0.1/////c$/temp/test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("//127.0.0.1/////c$/", "temp/test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + +pub fn split_windows_drive_prefix_48_inverted_test() { + let input = "\\\\127.0.0.1\\\\\\\\\\c$\\temp\\test-file.txt" + filepath.split_windows_volume_prefix(input) + |> should.equal(Ok(#("\\\\127.0.0.1\\\\\\\\\\c$\\", "temp\\test-file.txt"))) + + let assert Ok(#(drive, rest)) = filepath.split_windows_volume_prefix(input) + should.be_true(drive <> rest == input) +} + pub fn join_0_test() { filepath.join("/one", "two") |> should.equal("/one/two") From 248ed76d7d64c5efef53a6619ca7110a6814e25e Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Mon, 1 Apr 2024 20:13:10 +0300 Subject: [PATCH 2/4] Bump version to 1.1.0 and update CHANGELOG. --- CHANGELOG.md | 4 ++++ gleam.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f57a2..3e85793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.1.0 + +- Add new `split_windows_volume_prefix()`. + ## v1.0.0 - 2024-02-08 - All existing functions now support Windows paths when run on Windows. diff --git a/gleam.toml b/gleam.toml index 0922a7f..f5c419a 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "filepath" -version = "1.0.0" +version = "1.1.0" description = "Work with file paths in Gleam!" licences = ["Apache-2.0"] repository = { type = "github", user = "lpil", repo = "filepath" } From f22fafcd4a58e3423670f69c3501015b6c4896c4 Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Tue, 23 Jul 2024 15:49:31 +0300 Subject: [PATCH 3/4] Apply latest formatting and linter fixes. Signed-off-by: Nashwan Azhari --- src/filepath.gleam | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/filepath.gleam b/src/filepath.gleam index 2f5d63c..bca52b6 100644 --- a/src/filepath.gleam +++ b/src/filepath.gleam @@ -10,11 +10,11 @@ // https://github.com/elixir-lang/elixir/blob/main/lib/elixir/test/elixir/path_test.exs // https://cs.opensource.google/go/go/+/refs/tags/go1.21.4:src/path/match.go -import gleam/list import gleam/bool -import gleam/string -import gleam/result +import gleam/list import gleam/option.{type Option, None, Some} +import gleam/result +import gleam/string @external(erlang, "filepath_ffi", "is_windows") @external(javascript, "./filepath_ffi.mjs", "is_windows") @@ -153,11 +153,16 @@ fn pop_windows_drive_specifier(path: String) -> #(Option(String), String) { let start = string.slice(from: path, at_index: 0, length: 3) let codepoints = string.to_utf_codepoints(start) case list.map(codepoints, string.utf_codepoint_to_int) { - [drive, colon, slash] if { - slash == codepoint_slash || slash == codepoint_backslash - } && colon == codepoint_colon && { - drive >= codepoint_a && drive <= codepoint_z || drive >= codepoint_a_up && drive <= codepoint_z_up - } -> { + [drive, colon, slash] + if { slash == codepoint_slash || slash == codepoint_backslash } + && colon == codepoint_colon + && { + drive >= codepoint_a + && drive <= codepoint_z + || drive >= codepoint_a_up + && drive <= codepoint_z_up + } + -> { let drive_letter = string.slice(from: path, at_index: 0, length: 1) let drive = string.lowercase(drive_letter) <> ":/" let path = string.drop_left(path, 3) @@ -251,12 +256,10 @@ pub fn split_windows_volume_prefix( precolon -> case - #( - string.contains(precolon, "\\"), - string.contains(precolon, "/"), - ) + string.contains(precolon, "\\"), + string.contains(precolon, "/") { - #(False, False) -> + False, False -> case postcolon { "/" <> rest -> Ok(#(precolon <> ":/", rest)) "\\" <> rest -> Ok(#(precolon <> ":\\", rest)) @@ -266,7 +269,7 @@ pub fn split_windows_volume_prefix( } // Path is an incorrect Windows path, as only the first part of // of a non-UNC Windows path is ever allowed to have a colon. - _ -> Error(Nil) + _, _ -> Error(Nil) } } } From f2c135dd8c57bc08d64583e3fcc2009fc7f5bb9a Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Tue, 23 Jul 2024 16:02:13 +0300 Subject: [PATCH 4/4] Bump Gleam version in testing workflow. Signed-off-by: Nashwan Azhari --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 951b2fe..86c2c5a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: - uses: erlef/setup-beam@v1 with: otp-version: "26.0.2" - gleam-version: "0.34.1" + gleam-version: "1.2.0" rebar3-version: "3" # elixir-version: "1.15.4" - run: gleam deps download