Skip to content

Commit f97b0c0

Browse files
authored
file permissions functions (#349)
1 parent 495e00e commit f97b0c0

File tree

7 files changed

+208
-22
lines changed

7 files changed

+208
-22
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/expect
2+
3+
# uncomment line below for debugging
4+
# exp_internal 1
5+
6+
set timeout 7
7+
8+
source ./ci/expect_scripts/shared-code.exp
9+
10+
spawn $env(EXAMPLES_DIR)file-permissions
11+
12+
expect "LICENSE file permissions:\r\n Executable: Bool.false\r\n Readable: Bool.true\r\n Writable: Bool.true\r\n" {
13+
expect eof {
14+
check_exit_and_segfault
15+
}
16+
}
17+
18+
puts stderr "\nError: output was different from expected value."
19+
exit 1

crates/roc_file/src/lib.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use std::path::Path;
1010
use std::sync::OnceLock;
1111
use std::{env, io};
1212

13+
#[cfg(unix)]
14+
use std::os::unix::fs::PermissionsExt; // used for is_executable, is_readable, is_writable
15+
1316
pub fn heap() -> &'static ThreadSafeRefcountedResourceHeap<BufReader<File>> {
1417
static FILE_HEAP: OnceLock<ThreadSafeRefcountedResourceHeap<BufReader<File>>> = OnceLock::new();
1518
FILE_HEAP.get_or_init(|| {
@@ -181,6 +184,87 @@ pub fn file_size_in_bytes(roc_path: &RocList<u8>) -> RocResult<u64, IOErr> {
181184
}
182185
}
183186

187+
pub fn file_is_executable(roc_path: &RocList<u8>) -> RocResult<bool, IOErr> {
188+
let rust_path = path_from_roc_path(roc_path);
189+
190+
#[cfg(unix)]
191+
{
192+
let metadata_res = std::fs::metadata(rust_path);
193+
194+
match metadata_res {
195+
Ok(metadata) => {
196+
let permissions = metadata.permissions();
197+
RocResult::ok(permissions.mode() & 0o111 != 0)
198+
}
199+
Err(err) => {
200+
RocResult::err(err.into())
201+
}
202+
}
203+
}
204+
205+
#[cfg(windows)]
206+
{
207+
RocResult::err(IOErr{
208+
msg: "Not yet implemented on windows.".into(),
209+
tag: IOErrTag::Unsupported,
210+
})
211+
}
212+
}
213+
214+
pub fn file_is_readable(roc_path: &RocList<u8>) -> RocResult<bool, IOErr> {
215+
let rust_path = path_from_roc_path(roc_path);
216+
217+
#[cfg(unix)]
218+
{
219+
let metadata_res = std::fs::metadata(rust_path);
220+
221+
match metadata_res {
222+
Ok(metadata) => {
223+
let permissions = metadata.permissions();
224+
RocResult::ok(permissions.mode() & 0o400 != 0)
225+
}
226+
Err(err) => {
227+
RocResult::err(err.into())
228+
}
229+
}
230+
}
231+
232+
#[cfg(windows)]
233+
{
234+
RocResult::err(IOErr{
235+
msg: "Not yet implemented on windows.".into(),
236+
tag: IOErrTag::Unsupported,
237+
})
238+
}
239+
}
240+
241+
pub fn file_is_writable(roc_path: &RocList<u8>) -> RocResult<bool, IOErr> {
242+
let rust_path = path_from_roc_path(roc_path);
243+
244+
#[cfg(unix)]
245+
{
246+
let metadata_res = std::fs::metadata(rust_path);
247+
248+
match metadata_res {
249+
Ok(metadata) => {
250+
let permissions = metadata.permissions();
251+
RocResult::ok(permissions.mode() & 0o200 != 0)
252+
}
253+
Err(err) => {
254+
RocResult::err(err.into())
255+
}
256+
}
257+
}
258+
259+
#[cfg(windows)]
260+
{
261+
RocResult::err(IOErr{
262+
msg: "Not yet implemented on windows.".into(),
263+
tag: IOErrTag::Unsupported,
264+
})
265+
}
266+
}
267+
184268
pub fn dir_list(roc_path: &RocList<u8>) -> RocResult<RocList<RocList<u8>>, IOErr> {
185269
let path = path_from_roc_path(roc_path);
186270

crates/roc_host/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ pub fn init() {
315315
roc_fx_file_read_line as _,
316316
roc_fx_file_delete as _,
317317
roc_fx_file_size_in_bytes as _,
318+
roc_fx_file_is_executable as _,
319+
roc_fx_file_is_readable as _,
320+
roc_fx_file_is_writable as _,
318321
roc_fx_cwd as _,
319322
roc_fx_posix_time as _,
320323
roc_fx_sleep_millis as _,
@@ -516,6 +519,28 @@ pub extern "C" fn roc_fx_file_size_in_bytes(
516519
roc_file::file_size_in_bytes(roc_path)
517520
}
518521

522+
#[no_mangle]
523+
pub extern "C" fn roc_fx_file_is_executable(
524+
roc_path: &RocList<u8>,
525+
) -> RocResult<bool, roc_io_error::IOErr> {
526+
roc_file::file_is_executable(roc_path)
527+
}
528+
529+
#[no_mangle]
530+
pub extern "C" fn roc_fx_file_is_readable(
531+
roc_path: &RocList<u8>,
532+
) -> RocResult<bool, roc_io_error::IOErr> {
533+
roc_file::file_is_readable(roc_path)
534+
}
535+
536+
#[no_mangle]
537+
pub extern "C" fn roc_fx_file_is_writable(
538+
roc_path: &RocList<u8>,
539+
) -> RocResult<bool, roc_io_error::IOErr> {
540+
roc_file::file_is_writable(roc_path)
541+
}
542+
543+
519544
#[no_mangle]
520545
pub extern "C" fn roc_fx_cwd() -> RocResult<RocList<u8>, ()> {
521546
roc_env::cwd()

examples/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ file-read
1111
file-read-buffered
1212
file-read-memory-map
1313
file-size
14+
file-permissions
1415
form
1516
hello-world
1617
http-get

examples/file-permissions.roc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
app [main!] { pf: platform "../platform/main.roc" }
2+
3+
# To run this example: check the README.md in this folder
4+
5+
import pf.Stdout
6+
import pf.File
7+
8+
main! = |_args|
9+
file = "LICENSE"
10+
11+
is_executable = File.is_executable!(file)?
12+
13+
is_readable = File.is_readable!(file)?
14+
15+
is_writable = File.is_writable!(file)?
16+
17+
Stdout.line!(
18+
"""
19+
${file} file permissions:
20+
Executable: ${Inspect.to_str(is_executable)}
21+
Readable: ${Inspect.to_str(is_readable)}
22+
Writable: ${Inspect.to_str(is_writable)}
23+
"""
24+
)

platform/File.roc

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ module [
1010
is_dir!,
1111
is_file!,
1212
is_sym_link!,
13+
is_executable!,
14+
is_readable!,
15+
is_writable!,
1316
type!,
1417
open_reader!,
1518
open_reader_with_capacity!,
@@ -67,8 +70,8 @@ IOErr : InternalIOErr.IOErr
6770
## >
6871
## > [Path.write!] does the same thing, except it takes a [Path] instead of a [Str].
6972
write! : val, Str, fmt => Result {} [FileWriteErr Path IOErr] where val implements Encoding, fmt implements EncoderFormatting
70-
write! = |val, path, fmt|
71-
Path.write!(val, Path.from_str(path), fmt)
73+
write! = |val, path_str, fmt|
74+
Path.write!(val, Path.from_str(path_str), fmt)
7275

7376
## Writes bytes to a file.
7477
##
@@ -83,8 +86,8 @@ write! = |val, path, fmt|
8386
## >
8487
## > [Path.write_bytes!] does the same thing, except it takes a [Path] instead of a [Str].
8588
write_bytes! : List U8, Str => Result {} [FileWriteErr Path IOErr]
86-
write_bytes! = |bytes, path|
87-
Path.write_bytes!(bytes, Path.from_str(path))
89+
write_bytes! = |bytes, path_str|
90+
Path.write_bytes!(bytes, Path.from_str(path_str))
8891

8992
## Writes a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8).
9093
##
@@ -99,8 +102,8 @@ write_bytes! = |bytes, path|
99102
## >
100103
## > [Path.write_utf8!] does the same thing, except it takes a [Path] instead of a [Str].
101104
write_utf8! : Str, Str => Result {} [FileWriteErr Path IOErr]
102-
write_utf8! = |str, path|
103-
Path.write_utf8!(str, Path.from_str(path))
105+
write_utf8! = |str, path_str|
106+
Path.write_utf8!(str, Path.from_str(path_str))
104107

105108
## Deletes a file from the filesystem.
106109
##
@@ -123,8 +126,8 @@ write_utf8! = |str, path|
123126
## >
124127
## > [Path.delete!] does the same thing, except it takes a [Path] instead of a [Str].
125128
delete! : Str => Result {} [FileWriteErr Path IOErr]
126-
delete! = |path|
127-
Path.delete!(Path.from_str(path))
129+
delete! = |path_str|
130+
Path.delete!(Path.from_str(path_str))
128131

129132
## Reads all the bytes in a file.
130133
##
@@ -139,8 +142,8 @@ delete! = |path|
139142
## >
140143
## > [Path.read_bytes!] does the same thing, except it takes a [Path] instead of a [Str].
141144
read_bytes! : Str => Result (List U8) [FileReadErr Path IOErr]
142-
read_bytes! = |path|
143-
Path.read_bytes!(Path.from_str(path))
145+
read_bytes! = |path_str|
146+
Path.read_bytes!(Path.from_str(path_str))
144147

145148
## Reads a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text.
146149
##
@@ -156,8 +159,8 @@ read_bytes! = |path|
156159
##
157160
## > [Path.read_utf8!] does the same thing, except it takes a [Path] instead of a [Str].
158161
read_utf8! : Str => Result Str [FileReadErr Path IOErr, FileReadUtf8Err Path _]
159-
read_utf8! = |path|
160-
Path.read_utf8!(Path.from_str(path))
162+
read_utf8! = |path_str|
163+
Path.read_utf8!(Path.from_str(path_str))
161164

162165
# read : Str, fmt => Result contents [FileReadErr Path ReadErr, FileReadDecodingFailed] where contents implements Decoding, fmt implements DecoderFormatting
163166
# read = |path, fmt|
@@ -172,8 +175,8 @@ read_utf8! = |path|
172175
##
173176
## > [Path.hard_link!] does the same thing, except it takes a [Path] instead of a [Str].
174177
hard_link! : Str => Result {} [LinkErr IOErr]
175-
hard_link! = |path|
176-
Path.hard_link!(Path.from_str(path))
178+
hard_link! = |path_str|
179+
Path.hard_link!(Path.from_str(path_str))
177180

178181
## Returns True if the path exists on disk and is pointing at a directory.
179182
## Returns False if the path exists and it is not a directory. If the path does not exist,
@@ -183,8 +186,8 @@ hard_link! = |path|
183186
##
184187
## > [Path.is_dir!] does the same thing, except it takes a [Path] instead of a [Str].
185188
is_dir! : Str => Result Bool [PathErr IOErr]
186-
is_dir! = |path|
187-
Path.is_dir!(Path.from_str(path))
189+
is_dir! = |path_str|
190+
Path.is_dir!(Path.from_str(path_str))
188191

189192
## Returns True if the path exists on disk and is pointing at a regular file.
190193
## Returns False if the path exists and it is not a file. If the path does not exist,
@@ -194,8 +197,8 @@ is_dir! = |path|
194197
##
195198
## > [Path.is_file!] does the same thing, except it takes a [Path] instead of a [Str].
196199
is_file! : Str => Result Bool [PathErr IOErr]
197-
is_file! = |path|
198-
Path.is_file!(Path.from_str(path))
200+
is_file! = |path_str|
201+
Path.is_file!(Path.from_str(path_str))
199202

200203
## Returns True if the path exists on disk and is pointing at a symbolic link.
201204
## Returns False if the path exists and it is not a symbolic link. If the path does not exist,
@@ -205,16 +208,40 @@ is_file! = |path|
205208
##
206209
## > [Path.is_sym_link!] does the same thing, except it takes a [Path] instead of a [Str].
207210
is_sym_link! : Str => Result Bool [PathErr IOErr]
208-
is_sym_link! = |path|
209-
Path.is_sym_link!(Path.from_str(path))
211+
is_sym_link! = |path_str|
212+
Path.is_sym_link!(Path.from_str(path_str))
213+
214+
## Checks if the file has the execute permission for the current process.
215+
##
216+
## This uses rust [std::fs::Metadata](https://doc.rust-lang.org/std/fs/struct.Metadata.html)
217+
is_executable! : Str => Result Bool [PathErr IOErr]
218+
is_executable! = |path_str|
219+
Host.file_is_executable!(InternalPath.to_bytes(Path.from_str(path_str)))
220+
|> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err)))
221+
222+
## Checks if the file has the readable permission for the current process.
223+
##
224+
## This uses rust [std::fs::Metadata](https://doc.rust-lang.org/std/fs/struct.Metadata.html)
225+
is_readable! : Str => Result Bool [PathErr IOErr]
226+
is_readable! = |path_str|
227+
Host.file_is_readable!(InternalPath.to_bytes(Path.from_str(path_str)))
228+
|> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err)))
229+
230+
## Checks if the file has the writeable permission for the current process.
231+
##
232+
## This uses rust [std::fs::Metadata](https://doc.rust-lang.org/std/fs/struct.Metadata.html)
233+
is_writable! : Str => Result Bool [PathErr IOErr]
234+
is_writable! = |path_str|
235+
Host.file_is_writable!(InternalPath.to_bytes(Path.from_str(path_str)))
236+
|> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err)))
210237

211238
## Return the type of the path if the path exists on disk.
212239
## This uses [rust's std::path::is_symlink](https://doc.rust-lang.org/std/path/struct.Path.html#method.is_symlink).
213240
##
214241
## > [Path.type!] does the same thing, except it takes a [Path] instead of a [Str].
215242
type! : Str => Result [IsFile, IsDir, IsSymLink] [PathErr IOErr]
216-
type! = |path|
217-
Path.type!(Path.from_str(path))
243+
type! = |path_str|
244+
Path.type!(Path.from_str(path_str))
218245

219246
Reader := { reader : Host.FileReader, path : Path }
220247

platform/Host.roc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ hosted [
2020
file_size_in_bytes!,
2121
file_write_bytes!,
2222
file_write_utf8!,
23+
file_is_executable!,
24+
file_is_readable!,
25+
file_is_writable!,
2326
get_locale!,
2427
get_locales!,
2528
hard_link!,
@@ -69,6 +72,9 @@ file_write_utf8! : List U8, Str => Result {} InternalIOErr.IOErrFromHost
6972
file_delete! : List U8 => Result {} InternalIOErr.IOErrFromHost
7073
file_read_bytes! : List U8 => Result (List U8) InternalIOErr.IOErrFromHost
7174
file_size_in_bytes! : List U8 => Result U64 InternalIOErr.IOErrFromHost
75+
file_is_executable! : List U8 => Result Bool InternalIOErr.IOErrFromHost
76+
file_is_readable! : List U8 => Result Bool InternalIOErr.IOErrFromHost
77+
file_is_writable! : List U8 => Result Bool InternalIOErr.IOErrFromHost
7278

7379
FileReader := Box {}
7480
file_reader! : List U8, U64 => Result FileReader InternalIOErr.IOErrFromHost

0 commit comments

Comments
 (0)