diff --git a/platform/Host.roc b/platform/Host.roc index 286b8b99..11779003 100644 --- a/platform/Host.roc +++ b/platform/Host.roc @@ -27,11 +27,7 @@ Host :: [].{ FileReader :: Box(U64) - PathType : { - is_dir : Bool, - is_file : Bool, - is_sym_link : Bool, - } + PathType : [File, Dir, SymLink, Other] SqliteStmt :: Box(U64) diff --git a/platform/Path.roc b/platform/Path.roc index 9fa0d4f3..b144de41 100644 --- a/platform/Path.roc +++ b/platform/Path.roc @@ -2,6 +2,15 @@ import IOErr exposing [IOErr] import Host import OsStr exposing [OsStr] +path_type_from_host : Host.PathType -> [IsFile, IsDir, IsSymLink, IsOther] +path_type_from_host = |path_type| + match path_type { + File => IsFile + Dir => IsDir + SymLink => IsSymLink + Other => IsOther + } + ## Construct and operate on byte-preserving paths. Native Unix ## bytes and Windows UTF-16 units are preserved across host effects; use ## `display` only when a lossy human-readable representation is appropriate. @@ -21,8 +30,9 @@ Path := [ ## Returns `Bool.True` if the path exists on disk and is pointing at a regular file. ## - ## This function will traverse symbolic links to query information about the - ## destination file. In case of broken symbolic links this will return `Bool.False`. + ## This function does not traverse symbolic links; symbolic links (including + ## broken ones) return `Bool.False`. + ## Sockets, FIFOs, devices, and other special filesystem objects also return `Bool.False`. is_file! : Path => Try(Bool, [PathErr(IOErr), ..]) is_file! = |path| match type!(path) { @@ -34,8 +44,9 @@ Path := [ ## Returns `Bool.True` if the path exists on disk and is pointing at a directory. ## - ## This function will traverse symbolic links to query information about the - ## destination file. In case of broken symbolic links this will return `Bool.False`. + ## This function does not traverse symbolic links; symbolic links (including + ## broken ones) return `Bool.False`. + ## Sockets, FIFOs, devices, and other special filesystem objects also return `Bool.False`. is_dir! : Path => Try(Bool, [PathErr(IOErr), ..]) is_dir! = |path| match type!(path) { @@ -49,6 +60,7 @@ Path := [ ## ## This function will not traverse symbolic links - it checks whether the path ## itself is a symlink. + ## Sockets, FIFOs, devices, and other special filesystem objects return `Bool.False`. is_sym_link! : Path => Try(Bool, [PathErr(IOErr), ..]) is_sym_link! = |path| match type!(path) { @@ -69,21 +81,14 @@ Path := [ ## Return the type of the path if the path exists on disk. ## - type! : Path => Try([IsFile, IsDir, IsSymLink], [PathErr(IOErr), ..]) + ## `IsOther` represents sockets, FIFOs, block and character devices, and any + ## platform-specific object that is not a regular file, directory, or symbolic + ## link. On Windows this includes unrecognized reparse-point types. + type! : Path => Try([IsFile, IsDir, IsSymLink, IsOther], [PathErr(IOErr), ..]) type! = |path| { Host.path_type!(to_raw(path)) .map_err(|err| PathErr(err)) - .map_ok( - |path_type| { - if path_type.is_sym_link { - IsSymLink - } else if path_type.is_dir { - IsDir - } else { - IsFile - } - }, - ) + .map_ok(path_type_from_host) } ## Read all bytes from a file at this path. @@ -666,6 +671,15 @@ expect Path.ext(Path.windows("foo\\bar..")) == Err(EndsInDots) expect Path.ext(Path.utf8("foo/bar/")) == Err(IsDirPath) expect Path.ext(Path.utf8("foo/bar..")) == Err(EndsInDots) +## Host path types map exhaustively to the public API. +expect path_type_from_host(File) == IsFile + +expect path_type_from_host(Dir) == IsDir + +expect path_type_from_host(SymLink) == IsSymLink + +expect path_type_from_host(Other) == IsOther + ## `join` appends a representation-specific separator and text component. expect Path.join(Path.unix("foo"), "bar") == Path.unix("foo/bar") expect Path.join(Path.windows("foo"), "bar") == Path.windows("foo\\bar") diff --git a/src/lib.rs b/src/lib.rs index 7aaa40ae..4654ea7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -659,7 +659,7 @@ fn try_locale_get_ok(value: RocStr) -> HostLocaleGetResult { } } -fn try_path_type_ok(value: HostPathTypeOk) -> HostPathTypeResult { +fn try_path_type_ok(value: PathType) -> HostPathTypeResult { HostPathTypeResult { payload: HostPathTypeResultPayload { ok: ManuallyDrop::new(value), @@ -677,6 +677,36 @@ fn try_path_type_err(error: IOErr) -> HostPathTypeResult { } } +type PathType = HostPathTypeOk; + +fn path_type_from_metadata(metadata: &fs::Metadata) -> PathType { + let file_type = metadata.file_type(); + + if file_type.is_symlink() { + return PathType::SymLink; + } + + // A Windows reparse point that Rust does not identify as a symbolic link + // is not necessarily a regular file or directory (for example, a junction). + #[cfg(windows)] + { + use std::os::windows::fs::MetadataExt; + + const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400; + if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return PathType::Other; + } + } + + if file_type.is_dir() { + PathType::Dir + } else if file_type.is_file() { + PathType::File + } else { + PathType::Other + } +} + fn try_random_u64_ok(value: u64) -> RandomU64Result { RandomU64Result { payload: RandomU64ResultPayload { @@ -1818,14 +1848,7 @@ pub extern "C" fn hosted_path_type(path: UnixBytesOrUtf8OrWindowsU16s) -> HostPa }; match path.symlink_metadata() { - Ok(metadata) => { - let file_type = metadata.file_type(); - try_path_type_ok(HostPathTypeOk { - is_dir: metadata.is_dir(), - is_file: metadata.is_file(), - is_sym_link: file_type.is_symlink(), - }) - } + Ok(metadata) => try_path_type_ok(path_type_from_metadata(&metadata)), Err(error) => try_path_type_err(path_io_err_from_io(&error, roc_host)), } } @@ -2123,3 +2146,91 @@ pub fn rust_main(argc: i32, argv: *const *const c_char) -> i32 { set_roc_host(core::ptr::null_mut()); exit_code } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::{Path, PathBuf}; + use std::sync::atomic::{AtomicUsize, Ordering}; + + static NEXT_TEST_DIR: AtomicUsize = AtomicUsize::new(0); + + struct TestDir(PathBuf); + + impl TestDir { + fn new() -> Self { + let sequence = NEXT_TEST_DIR.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "basic-cli-path-type-{}-{sequence}", + std::process::id() + )); + fs::create_dir(&path).unwrap(); + Self(path) + } + + fn join(&self, path: impl AsRef) -> PathBuf { + self.0.join(path) + } + } + + impl Drop for TestDir { + fn drop(&mut self) { + fs::remove_dir_all(&self.0).unwrap(); + } + } + + fn classify(path: &Path) -> PathType { + path_type_from_metadata(&path.symlink_metadata().unwrap()) + } + + #[test] + fn classifies_regular_files_and_directories() { + let directory = TestDir::new(); + let file = directory.join("file"); + fs::write(&file, b"contents").unwrap(); + + assert_eq!(classify(&directory.0), PathType::Dir); + assert_eq!(classify(&file), PathType::File); + } + + #[cfg(unix)] + #[test] + fn classifies_symbolic_links() { + use std::os::unix::fs::symlink; + + let directory = TestDir::new(); + let target = directory.join("target"); + let link = directory.join("link"); + fs::write(&target, b"contents").unwrap(); + symlink(&target, &link).unwrap(); + + assert_eq!(classify(&link), PathType::SymLink); + } + + #[cfg(unix)] + #[test] + fn classifies_fifo_as_other() { + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt; + + let directory = TestDir::new(); + let fifo = directory.join("fifo"); + let fifo_c_string = CString::new(fifo.as_os_str().as_bytes()).unwrap(); + let result = unsafe { libc::mkfifo(fifo_c_string.as_ptr(), 0o600) }; + assert_eq!(result, 0, "mkfifo failed: {}", io::Error::last_os_error()); + + assert_eq!(classify(&fifo), PathType::Other); + } + + #[cfg(unix)] + #[test] + fn classifies_unix_domain_socket_as_other() { + use std::os::unix::net::UnixListener; + + let directory = TestDir::new(); + let socket = directory.join("socket"); + let _listener = UnixListener::bind(&socket).unwrap(); + + assert_eq!(classify(&socket), PathType::Other); + } +} diff --git a/src/roc_platform_abi.rs b/src/roc_platform_abi.rs index a91d26ed..d7043b21 100644 --- a/src/roc_platform_abi.rs +++ b/src/roc_platform_abi.rs @@ -989,35 +989,6 @@ const _: () = assert!(core::mem::size_of::() == 64, #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct8bbc5017d7a8cb36 alignment mismatch"); -/// Element type for __AnonStruct_8dfa7f17f2083a52 -#[cfg(target_pointer_width = "32")] -#[repr(C)] -#[derive(Clone, Copy)] -pub struct AnonStruct8dfa7f17f2083a52 { - pub is_dir: bool, - pub is_file: bool, - pub is_sym_link: bool, -} - -/// Element type for __AnonStruct_8dfa7f17f2083a52 -#[cfg(not(target_pointer_width = "32"))] -#[repr(C)] -#[derive(Clone, Copy)] -pub struct AnonStruct8dfa7f17f2083a52 { - pub is_dir: bool, - pub is_file: bool, - pub is_sym_link: bool, -} - -#[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 3, "AnonStruct8dfa7f17f2083a52 size mismatch"); -#[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 1, "AnonStruct8dfa7f17f2083a52 alignment mismatch"); -#[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 3, "AnonStruct8dfa7f17f2083a52 size mismatch"); -#[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 1, "AnonStruct8dfa7f17f2083a52 alignment mismatch"); - /// Element type for __AnonStruct_22cf486058afc711 #[cfg(target_pointer_width = "32")] #[repr(C)] @@ -2756,7 +2727,7 @@ pub enum HostPathTypeResultTag { #[derive(Clone, Copy)] pub union HostPathTypeResultPayload { pub err: core::mem::ManuallyDrop, - pub ok: core::mem::ManuallyDrop, + pub ok: core::mem::ManuallyDrop, } #[cfg(target_pointer_width = "32")] @@ -2795,12 +2766,12 @@ impl HostPathTypeResult { } #[cfg(target_pointer_width = "32")] - pub fn payload_ok(&self) -> AnonStruct8dfa7f17f2083a52 { - unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStruct8dfa7f17f2083a52) } + pub fn payload_ok(&self) -> DirOrFileOrOtherOrSymLink { + unsafe { core::ptr::read(self.payload.as_ptr() as *const DirOrFileOrOtherOrSymLink) } } #[cfg(not(target_pointer_width = "32"))] - pub fn payload_ok(&self) -> AnonStruct8dfa7f17f2083a52 { + pub fn payload_ok(&self) -> DirOrFileOrOtherOrSymLink { unsafe { core::mem::ManuallyDrop::into_inner(self.payload.ok) } } @@ -2819,6 +2790,25 @@ const _: () = assert!(core::mem::align_of::() == 4, "HostPat #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::offset_of!(HostPathTypeResult, tag) == 16, "HostPathTypeResult tag offset mismatch"); +/// Tag union: DirOrFileOrOtherOrSymLink +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DirOrFileOrOtherOrSymLink { + Dir = 0, + File = 1, + Other = 2, + SymLink = 3, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 1, "DirOrFileOrOtherOrSymLink size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 1, "DirOrFileOrOtherOrSymLink alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 1, "DirOrFileOrOtherOrSymLink size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 1, "DirOrFileOrOtherOrSymLink alignment mismatch"); + /// Tag discriminant for Try. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -4355,14 +4345,14 @@ const _: () = assert!(core::mem::offset_of!(OsStr, tag) == 12, "OsStr tag offset /// Tag discriminant for Try. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TryType205Tag { +pub enum TryType204Tag { Err = 0, Ok = 1, } #[repr(C)] #[derive(Clone, Copy)] -pub union TryType205Payload { +pub union TryType204Payload { pub err: core::mem::ManuallyDrop, pub ok: [u8; 0], } @@ -4370,28 +4360,28 @@ pub union TryType205Payload { #[cfg(target_pointer_width = "32")] #[repr(align(4))] #[derive(Clone, Copy)] -pub struct TryType205PayloadAlignment; +pub struct TryType204PayloadAlignment; /// Tag union: Try #[cfg(target_pointer_width = "32")] #[repr(C)] #[derive(Clone, Copy)] -pub struct TryType205 { - pub _payload_alignment: [TryType205PayloadAlignment; 0], +pub struct TryType204 { + pub _payload_alignment: [TryType204PayloadAlignment; 0], pub payload: [u8; 4], - pub tag: TryType205Tag, + pub tag: TryType204Tag, } /// Tag union: Try #[cfg(not(target_pointer_width = "32"))] #[repr(C)] #[derive(Clone, Copy)] -pub struct TryType205 { - pub payload: TryType205Payload, - pub tag: TryType205Tag, +pub struct TryType204 { + pub payload: TryType204Payload, + pub tag: TryType204Tag, } -impl TryType205 { +impl TryType204 { #[cfg(target_pointer_width = "32")] pub fn payload_err(&self) -> i32 { unsafe { core::ptr::read(self.payload.as_ptr() as *const i32) } @@ -4405,17 +4395,17 @@ impl TryType205 { } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 8, "TryType205 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 8, "TryType204 size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 4, "TryType205 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 4, "TryType204 alignment mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::offset_of!(TryType205, tag) == 4, "TryType205 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(TryType204, tag) == 4, "TryType204 tag offset mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 8, "TryType205 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 8, "TryType204 size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 4, "TryType205 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 4, "TryType204 alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::offset_of!(TryType205, tag) == 4, "TryType205 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(TryType204, tag) == 4, "TryType204 tag offset mismatch"); /// Return type record for Host.env_platform! /// Fields ordered by compiler-emitted ABI offsets. @@ -4768,7 +4758,7 @@ const _: () = assert!(core::mem::size_of::() == 64, "Ho const _: () = assert!(core::mem::align_of::() == 8, "HostHttpSendRequestArgs alignment mismatch"); /// Arguments for Host.path_type! -/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({ is_dir : Bool, is_file : Bool, is_sym_link : Bool }, IOErr) +/// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try([Dir, File, Other, SymLink], IOErr) /// Refcounted fields are owned by the hosted function. #[repr(C)] #[derive(Clone, Copy)] @@ -5034,7 +5024,7 @@ pub type HostHttpSendRequestErrPayload = BadBodyOrNetworkErrorOrOtherOrTimeoutPa pub type HostHttpSendRequestErrTag = BadBodyOrNetworkErrorOrOtherOrTimeoutTag; pub type HostHttpSendRequestOk = AnonStructBe6bcbc15f8a1360; pub type HostHttpSendRequestOkHeaders = AnonStruct77eaba63dfee299d; -pub type HostPathTypeOk = AnonStruct8dfa7f17f2083a52; +pub type HostPathTypeOk = DirOrFileOrOtherOrSymLink; pub type HostSqliteBindArg1 = AnonStruct2782504baf739389; pub type HostSqliteBindErr = AnonStruct22cf486058afc711; pub type HostSqliteColumnValueErr = AnonStruct22cf486058afc711; @@ -6181,25 +6171,23 @@ impl HostPathTypeResult { } } -impl AnonStruct8dfa7f17f2083a52 { - /// Recursively decrement Roc-owned fields. +impl DirOrFileOrOtherOrSymLink { + /// Recursively decrement Roc-owned payloads. /// /// # Safety - /// `self` must own one live Roc reference for each refcounted field. + /// `self` must own one live Roc reference for each refcounted payload. pub unsafe fn decref(self, roc_host: &RocHost) { - let value = self; - let _ = value; + let _ = self; let _ = roc_host; } - /// Increment Roc-owned fields. + /// Increment Roc-owned payloads. /// /// # Safety /// `self` must point at live Roc allocations. The retained references must /// be balanced by later decrefs. pub unsafe fn incref(self, amount: isize) { - let value = self; - let _ = value; + let _ = self; let _ = amount; } } @@ -7101,7 +7089,7 @@ impl OsStr { } } -impl TryType205 { +impl TryType204 { /// Recursively decrement Roc-owned payloads. /// /// # Safety @@ -7110,8 +7098,8 @@ impl TryType205 { let value = self; let _ = roc_host; match value.tag { - TryType205Tag::Err => {}, - TryType205Tag::Ok => {}, + TryType204Tag::Err => {}, + TryType204Tag::Ok => {}, } } @@ -7124,8 +7112,8 @@ impl TryType205 { let value = self; let _ = amount; match value.tag { - TryType205Tag::Err => {}, - TryType205Tag::Ok => {}, + TryType204Tag::Err => {}, + TryType204Tag::Ok => {}, } } } @@ -7285,7 +7273,7 @@ unsafe extern "C" { pub fn hosted_locale_get() -> HostLocaleGetResult; /// Hosted symbol for Host.path_type! - /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try({ is_dir : Bool, is_file : Bool, is_sym_link : Bool }, IOErr) + /// Roc signature: [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] => Try([Dir, File, Other, SymLink], IOErr) pub fn hosted_path_type(arg0: UnixBytesOrUtf8OrWindowsU16s) -> HostPathTypeResult; /// Hosted symbol for Host.random_seed_u32!