Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions platform/Host.roc
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
46 changes: 30 additions & 16 deletions platform/Path.roc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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.
Expand Down Expand Up @@ -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")
Expand Down
129 changes: 120 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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 {
Expand Down Expand Up @@ -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)),
}
}
Expand Down Expand Up @@ -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<Path>) -> 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);
}
}
Loading
Loading