mirror of
https://github.com/ocaml-multicore/eio.git
synced 2025-08-29 00:03:47 -04:00
Initial FS abstraction
This commit is contained in:
parent
f30603d962
commit
c18edc3ce4
77
README.md
77
README.md
@ -21,6 +21,7 @@ unreleased repository.
|
||||
* [Performance](#performance)
|
||||
* [Networking](#networking)
|
||||
* [Design note: object capabilities](#design-note-object-capabilities)
|
||||
* [Filesystem access](#filesystem-access)
|
||||
* [Multicore support](#multicore-support)
|
||||
* [Design note: thread-safety](#design-note-thread-safety)
|
||||
* [Design note: determinism](#design-note-determinism)
|
||||
@ -432,6 +433,82 @@ However, it still makes non-malicious code easier to understand and test,
|
||||
and may allow for an ocap extension to the language in the future.
|
||||
See [Emily][] for a previous attempt at this.
|
||||
|
||||
## Filesystem access
|
||||
|
||||
Access to the filesystem is also controlled by capabilities, and `env` provides two:
|
||||
|
||||
- `fs` provides full access (just like OCaml's stdlib).
|
||||
- `cwd` restricts access to files beneath the current working directory.
|
||||
|
||||
For example:
|
||||
|
||||
```ocaml
|
||||
let try_write_file dir path data =
|
||||
match
|
||||
Switch.top @@ fun sw ->
|
||||
let flow = Eio.Dir.open_out dir ~sw path ~create:(`Exclusive 0o600) in
|
||||
Eio.Flow.copy_string data flow
|
||||
with
|
||||
| () -> traceln "write %S -> ok" path
|
||||
| exception ex -> traceln "write %S -> %a" path Fmt.exn ex
|
||||
|
||||
let try_mkdir dir path =
|
||||
match Eio.Dir.mkdir dir path ~perm:0o700 with
|
||||
| () -> traceln "mkdir %S -> ok" path
|
||||
| exception ex -> traceln "mkdir %S -> %a" path Fmt.exn ex
|
||||
```
|
||||
|
||||
```ocaml
|
||||
# Eio_main.run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
try_mkdir cwd "dir1";
|
||||
try_mkdir cwd "../dir2";
|
||||
try_mkdir cwd "/tmp/dir3";
|
||||
mkdir "dir1" -> ok
|
||||
mkdir "../dir2" -> Eio.Dir.Permission_denied("..", _)
|
||||
mkdir "/tmp/dir3" -> Eio.Dir.Permission_denied("/tmp", _)
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
The checks also apply to following symlinks:
|
||||
|
||||
```ocaml
|
||||
# Unix.symlink "dir1" "link-to-dir1"; Unix.symlink "/tmp" "link-to-tmp";;
|
||||
- : unit = ()
|
||||
|
||||
# Eio_main.run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
try_write_file cwd "dir1/file1" "A";
|
||||
try_write_file cwd "link-to-dir1/file2" "B";
|
||||
try_write_file cwd "link-to-tmp/file3" "C"
|
||||
write "dir1/file1" -> ok
|
||||
write "link-to-dir1/file2" -> ok
|
||||
write "link-to-tmp/file3" -> Eio.Dir.Permission_denied("link-to-tmp/file3", _)
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
You can use `open_dir` to create a restricted capability to a sub-directory:
|
||||
|
||||
```ocaml
|
||||
# Eio_main.run @@ fun env ->
|
||||
Switch.top @@ fun sw ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
let dir1 = Eio.Dir.open_dir ~sw cwd "dir1" in
|
||||
try_write_file dir1 "file4" "D";
|
||||
try_write_file dir1 "../file5" "E"
|
||||
write "file4" -> ok
|
||||
write "../file5" -> Eio.Dir.Permission_denied("../file5", _)
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Note that you only need to use `open_dir` if you want to create a new sandboxed environment.
|
||||
You can use a single directory object to access all paths beneath it,
|
||||
and this allows following symlinks within that sub-tree.
|
||||
|
||||
A program that operates on the current directory will probably want to use `cwd`,
|
||||
whereas a program that accepts a path from the user will probably want to use `fs`,
|
||||
perhaps with `open_dir` to constrain all access to be within that directory.
|
||||
|
||||
## Multicore support
|
||||
|
||||
Fibres are scheduled cooperatively within a single domain, but you can also create new domains that run in parallel.
|
||||
|
@ -177,6 +177,34 @@ module Time = struct
|
||||
let sleep ?sw (t : #clock) d = t#sleep ?sw d
|
||||
end
|
||||
|
||||
module Dir = struct
|
||||
type path = string
|
||||
|
||||
exception Permission_denied of path * exn
|
||||
|
||||
type create = [`Never | `If_missing of Unix.file_perm | `Or_truncate of Unix.file_perm | `Exclusive of Unix.file_perm]
|
||||
|
||||
class virtual t = object
|
||||
method virtual open_in : sw:Switch.t -> path -> <Flow.source; Flow.close>
|
||||
method virtual open_out :
|
||||
sw:Switch.t ->
|
||||
append:bool ->
|
||||
create:create ->
|
||||
path -> <Flow.two_way; Flow.close>
|
||||
method virtual mkdir : ?sw:Switch.t -> perm:Unix.file_perm -> path -> unit
|
||||
method virtual open_dir : sw:Switch.t -> path -> t_with_close
|
||||
end
|
||||
and virtual t_with_close = object
|
||||
(* This dummy class avoids an "Error: The type < .. > is not an object type" error from the compiler. *)
|
||||
inherit t
|
||||
method virtual close : unit
|
||||
end
|
||||
|
||||
let open_in ~sw (t:#t) = t#open_in ~sw
|
||||
let open_out ~sw ?(append=false) ~create (t:#t) path = t#open_out ~sw ~append ~create path
|
||||
let open_dir ~sw (t:#t) = t#open_dir ~sw
|
||||
let mkdir ?sw (t:#t) = t#mkdir ?sw
|
||||
end
|
||||
|
||||
module Stdenv = struct
|
||||
type t = <
|
||||
@ -186,6 +214,8 @@ module Stdenv = struct
|
||||
network : Network.t;
|
||||
domain_mgr : Domain_manager.t;
|
||||
clock : Time.clock;
|
||||
fs : Dir.t;
|
||||
cwd : Dir.t;
|
||||
>
|
||||
|
||||
let stdin (t : <stdin : #Flow.source; ..>) = t#stdin
|
||||
@ -194,6 +224,8 @@ module Stdenv = struct
|
||||
let network (t : <network : #Network.t; ..>) = t#network
|
||||
let domain_mgr (t : <domain_mgr : #Domain_manager.t; ..>) = t#domain_mgr
|
||||
let clock (t : <clock : #Time.clock; ..>) = t#clock
|
||||
let fs (t : <fs : #Dir.t; ..>) = t#fs
|
||||
let cwd (t : <cwd : #Dir.t; ..>) = t#cwd
|
||||
end
|
||||
|
||||
module Private = struct
|
||||
|
@ -373,6 +373,57 @@ module Time : sig
|
||||
@param sw The sleep is aborted if the switch is turned off. *)
|
||||
end
|
||||
|
||||
module Dir : sig
|
||||
type path = string
|
||||
|
||||
exception Permission_denied of path * exn
|
||||
|
||||
type create = [`Never | `If_missing of Unix.file_perm | `Or_truncate of Unix.file_perm | `Exclusive of Unix.file_perm]
|
||||
(** When to create a new file:
|
||||
If [`Never] then it's an error if the named file doesn't exist.
|
||||
If [`If_missing] then an existing file is simply opened.
|
||||
If [`Or_truncate] then an existing file truncated to zero length.
|
||||
If [`Exclusive] then it is an error is the file does exist.
|
||||
If a new file is created, the given permissions are used for it. *)
|
||||
|
||||
(** A [Dir.t] represents access to a directory and contents, recursively. *)
|
||||
class virtual t : object
|
||||
method virtual open_in : sw:Switch.t -> path -> <Flow.source; Flow.close>
|
||||
method virtual open_out :
|
||||
sw:Switch.t ->
|
||||
append:bool ->
|
||||
create:create ->
|
||||
path -> <Flow.two_way; Flow.close>
|
||||
method virtual mkdir : ?sw:Switch.t -> perm:Unix.file_perm -> path -> unit
|
||||
method virtual open_dir : sw:Switch.t -> path -> t_with_close
|
||||
end
|
||||
and virtual t_with_close : object
|
||||
inherit t
|
||||
method virtual close : unit
|
||||
end
|
||||
|
||||
val open_in : sw:Switch.t -> #t -> path -> <Flow.source; Flow.close>
|
||||
(** [open_in ~sw t path] opens [t/path] for reading.
|
||||
Note: files are always opened in binary mode. *)
|
||||
|
||||
val open_out :
|
||||
sw:Switch.t ->
|
||||
?append:bool ->
|
||||
create:create ->
|
||||
#t -> path -> <Flow.two_way; Flow.close>
|
||||
(** [open_out ~sw t path] opens [t/path] for reading and writing.
|
||||
Note: files are always opened in binary mode.
|
||||
@param append Open for appending: always write at end of file.
|
||||
@param create Controls whether to create the file, and what permissions to give it if so. *)
|
||||
|
||||
val mkdir : ?sw:Switch.t -> #t -> perm:Unix.file_perm -> path -> unit
|
||||
(** [mkdir t ~perm path] creates a new directory [t/path] with permissions [perm]. *)
|
||||
|
||||
val open_dir : sw:Switch.t -> #t -> path -> <t; Flow.close>
|
||||
(** [open_dir ~sw t path] opens [t/path].
|
||||
This can be passed to functions to grant access only to the subtree [t/path]. *)
|
||||
end
|
||||
|
||||
(** The standard environment of a process. *)
|
||||
module Stdenv : sig
|
||||
type t = <
|
||||
@ -382,6 +433,8 @@ module Stdenv : sig
|
||||
network : Network.t;
|
||||
domain_mgr : Domain_manager.t;
|
||||
clock : Time.clock;
|
||||
fs : Dir.t;
|
||||
cwd : Dir.t;
|
||||
>
|
||||
|
||||
val stdin : <stdin : #Flow.source as 'a; ..> -> 'a
|
||||
@ -391,6 +444,17 @@ module Stdenv : sig
|
||||
val network : <network : #Network.t as 'a; ..> -> 'a
|
||||
val domain_mgr : <domain_mgr : #Domain_manager.t as 'a; ..> -> 'a
|
||||
val clock : <clock : #Time.clock as 'a; ..> -> 'a
|
||||
|
||||
val cwd : <cwd : #Dir.t as 'a; ..> -> 'a
|
||||
(** [cwd t] is the current working directory of the process (this may change
|
||||
over time if the process does a `chdir` operation, which is not recommended). *)
|
||||
|
||||
val fs : <fs : #Dir.t as 'a; ..> -> 'a
|
||||
(** [fs t] is the process's full access to the filesystem.
|
||||
Paths can be absolute or relative (to the current working directory).
|
||||
Using relative paths with this is similar to using them with {!cwd},
|
||||
except that this will follow symlinks to other parts of the filesystem.
|
||||
[fs] is useful for handling paths passed in by the user. *)
|
||||
end
|
||||
|
||||
(** {1 Provider API for OS schedulers} *)
|
||||
|
@ -1,4 +1,7 @@
|
||||
(library
|
||||
(name eio_linux)
|
||||
(public_name eio_linux)
|
||||
(foreign_stubs
|
||||
(language c)
|
||||
(names eio_stubs))
|
||||
(libraries eio eunix unix uring logs fmt bigstringaf ctf))
|
||||
|
@ -96,6 +96,7 @@ type io_job =
|
||||
| Read : rw_req * cancel_hook -> io_job
|
||||
| Poll_add : int Suspended.t * cancel_hook -> io_job
|
||||
| Splice : int Suspended.t * cancel_hook -> io_job
|
||||
| Openat2 : int Suspended.t * cancel_hook -> io_job
|
||||
| Connect : int Suspended.t * cancel_hook -> io_job
|
||||
| Accept : int Suspended.t * cancel_hook -> io_job
|
||||
| Close : int Suspended.t -> io_job
|
||||
@ -244,6 +245,17 @@ let rec enqueue_splice ?sw st action ~src ~dst ~len =
|
||||
if retry then (* wait until an sqe is available *)
|
||||
Queue.push (fun st -> enqueue_splice ?sw st action ~src ~dst ~len) st.io_q
|
||||
|
||||
let rec enqueue_openat2 st action ((sw, access, flags, perm, resolve, dir, path) as args) =
|
||||
Log.debug (fun l -> l "openat2: submitting call");
|
||||
Ctf.label "openat2";
|
||||
let fd = Option.map (FD.get "openat2") dir in
|
||||
let retry = with_cancel_hook ~sw ~action st (fun cancel ->
|
||||
Uring.openat2 st.uring ~access ~flags ~perm ~resolve ?fd path (Openat2 (action, cancel))
|
||||
)
|
||||
in
|
||||
if retry then (* wait until an sqe is available *)
|
||||
Queue.push (fun st -> enqueue_openat2 st action args) st.io_q
|
||||
|
||||
let rec enqueue_connect ?sw st action fd addr =
|
||||
Log.debug (fun l -> l "connect: submitting call");
|
||||
Ctf.label "connect";
|
||||
@ -348,6 +360,10 @@ and handle_complete st ~runnable result =
|
||||
Log.debug (fun l -> l "connect returned");
|
||||
Switch.remove_hook !cancel;
|
||||
Suspended.continue k result
|
||||
| Openat2 (k, cancel) ->
|
||||
Log.debug (fun l -> l "openat2 returned");
|
||||
Switch.remove_hook !cancel;
|
||||
Suspended.continue k result
|
||||
| Accept (k, cancel) ->
|
||||
Log.debug (fun l -> l "accept returned");
|
||||
Switch.remove_hook !cancel;
|
||||
@ -378,7 +394,7 @@ and complete_rw_req st ({len; cur_off; action; _} as req) res =
|
||||
let alloc_buf st k =
|
||||
Log.debug (fun l -> l "alloc: %d" (Uring.Region.avail st.mem));
|
||||
match Uring.Region.alloc st.mem with
|
||||
| buf -> Suspended.continue k buf
|
||||
| buf -> Suspended.continue k buf
|
||||
| exception Uring.Region.No_space ->
|
||||
Queue.push k st.mem_q;
|
||||
schedule st
|
||||
@ -473,9 +489,48 @@ let openfile ~sw path flags mode =
|
||||
let fd = Unix.openfile path flags mode in
|
||||
FD.of_unix ~sw ~seekable:(FD.is_seekable fd) fd
|
||||
|
||||
effect Openat2 : (Switch.t * [`R|`W|`RW] * Uring.Open_flags.t * Unix.file_perm * Uring.Resolve.t * FD.t option * string) -> int
|
||||
let openat2 ~sw ?seekable ~access ~flags ~perm ~resolve ?dir path =
|
||||
let res = perform (Openat2 (sw, access, flags, perm, resolve, dir, path)) in
|
||||
if res < 0 then (
|
||||
Switch.check sw; (* If cancelled, report that instead. *)
|
||||
let ex = Unix.Unix_error (Uring.error_of_errno res, "openat2", "") in
|
||||
if res = -18 then raise (Eio.Dir.Permission_denied (path, ex))
|
||||
else raise ex
|
||||
);
|
||||
let fd : Unix.file_descr = Obj.magic res in
|
||||
let seekable =
|
||||
match seekable with
|
||||
| None -> FD.is_seekable fd
|
||||
| Some x -> x
|
||||
in
|
||||
FD.of_unix ~sw ~seekable fd
|
||||
|
||||
let fstat fd =
|
||||
Unix.fstat (FD.get "fstat" fd)
|
||||
|
||||
external eio_mkdirat : Unix.file_descr -> string -> Unix.file_perm -> unit = "caml_eio_mkdirat"
|
||||
|
||||
(* We ignore [sw] because this isn't a uring operation yet. *)
|
||||
let mkdirat ?sw:_ ~perm dir path =
|
||||
match dir with
|
||||
| None -> Unix.mkdir path perm
|
||||
| Some dir -> eio_mkdirat (FD.get "mkdirat" dir) path perm
|
||||
|
||||
let mkdir_beneath ?sw ~perm ?dir path =
|
||||
let dir_path = Filename.dirname path in
|
||||
let leaf = Filename.basename path in
|
||||
(* [mkdir] is really an operation on [path]'s parent. Get a reference to that first: *)
|
||||
Switch.sub_opt sw (fun sw ->
|
||||
let parent = openat2 ~sw ~seekable:false ?dir dir_path
|
||||
~access:`R
|
||||
~flags:Uring.Open_flags.(cloexec + path + directory)
|
||||
~perm:0
|
||||
~resolve:Uring.Resolve.beneath
|
||||
in
|
||||
mkdirat ~sw ~perm (Some parent) leaf
|
||||
)
|
||||
|
||||
let shutdown socket command =
|
||||
Unix.shutdown (FD.get "shutdown" socket) command
|
||||
|
||||
@ -642,6 +697,8 @@ module Objects = struct
|
||||
network : Eio.Network.t;
|
||||
domain_mgr : Eio.Domain_manager.t;
|
||||
clock : Eio.Time.clock;
|
||||
fs : Eio.Dir.t;
|
||||
cwd : Eio.Dir.t;
|
||||
>
|
||||
|
||||
let domain_mgr = object
|
||||
@ -668,11 +725,69 @@ module Objects = struct
|
||||
method sleep ?sw d = sleep ?sw d
|
||||
end
|
||||
|
||||
class dir fd = object
|
||||
inherit Eio.Dir.t
|
||||
|
||||
val resolve_flags = Uring.Resolve.beneath
|
||||
|
||||
method open_in ~sw path =
|
||||
let fd = openat2 ~sw ?dir:fd path
|
||||
~access:`R
|
||||
~flags:Uring.Open_flags.cloexec
|
||||
~perm:0
|
||||
~resolve:Uring.Resolve.beneath
|
||||
in
|
||||
(flow fd :> <Eio.Flow.source; Eio.Flow.close>)
|
||||
|
||||
method open_out ~sw ~append ~create path =
|
||||
let perm, flags =
|
||||
match create with
|
||||
| `Never -> 0, Uring.Open_flags.empty
|
||||
| `If_missing perm -> perm, Uring.Open_flags.creat
|
||||
| `Or_truncate perm -> perm, Uring.Open_flags.(creat + trunc)
|
||||
| `Exclusive perm -> perm, Uring.Open_flags.(creat + excl)
|
||||
in
|
||||
let flags = if append then Uring.Open_flags.(flags + append) else flags in
|
||||
let fd = openat2 ~sw ?dir:fd path
|
||||
~access:`RW
|
||||
~flags:Uring.Open_flags.(cloexec + flags)
|
||||
~perm
|
||||
~resolve:resolve_flags
|
||||
in
|
||||
(flow fd :> <Eio.Flow.two_way; Eio.Flow.close>)
|
||||
|
||||
method open_dir ~sw path =
|
||||
let fd = openat2 ~sw ~seekable:false ?dir:fd path
|
||||
~access:`R
|
||||
~flags:Uring.Open_flags.(cloexec + path + directory)
|
||||
~perm:0
|
||||
~resolve:resolve_flags
|
||||
in
|
||||
(new dir (Some fd) :> <Eio.Dir.t; Eio.Flow.close>)
|
||||
|
||||
method mkdir ?sw ~perm path =
|
||||
mkdir_beneath ?sw ~perm ?dir:fd path
|
||||
|
||||
method close =
|
||||
FD.close (Option.get fd)
|
||||
end
|
||||
|
||||
(* Full access to the filesystem. *)
|
||||
let fs = object
|
||||
inherit dir None
|
||||
|
||||
val! resolve_flags = Uring.Resolve.empty
|
||||
|
||||
method! mkdir ?sw ~perm path =
|
||||
mkdirat ?sw ~perm None path
|
||||
end
|
||||
|
||||
let stdenv () =
|
||||
let of_unix fd = FD.of_unix_no_hook ~seekable:(FD.is_seekable fd) fd in
|
||||
let stdin = lazy (source (of_unix Unix.stdin)) in
|
||||
let stdout = lazy (sink (of_unix Unix.stdout)) in
|
||||
let stderr = lazy (sink (of_unix Unix.stderr)) in
|
||||
let cwd = new dir None in
|
||||
object (_ : stdenv)
|
||||
method stdin = Lazy.force stdin
|
||||
method stdout = Lazy.force stdout
|
||||
@ -680,6 +795,8 @@ module Objects = struct
|
||||
method network = network
|
||||
method domain_mgr = domain_mgr
|
||||
method clock = clock
|
||||
method fs = (fs :> Eio.Dir.t)
|
||||
method cwd = (cwd :> Eio.Dir.t)
|
||||
end
|
||||
end
|
||||
|
||||
@ -695,7 +812,7 @@ let run ?(queue_depth=64) ?(block_size=4096) main =
|
||||
(* TODO unify this allocation API around baregion/uring *)
|
||||
let fixed_buf_len = block_size * queue_depth in
|
||||
let uring = Uring.create ~fixed_buf_len ~queue_depth () in
|
||||
let buf = Uring.buf uring in
|
||||
let buf = Uring.buf uring in
|
||||
let mem = Uring.Region.init ~block_size buf queue_depth in
|
||||
let run_q = Queue.create () in
|
||||
let sleep_q = Zzz.create () in
|
||||
@ -727,6 +844,10 @@ let run ?(queue_depth=64) ?(block_size=4096) main =
|
||||
let k = { Suspended.k; tid } in
|
||||
enqueue_splice ?sw st k ~src ~dst ~len;
|
||||
schedule st
|
||||
| effect (Openat2 args) k ->
|
||||
let k = { Suspended.k; tid } in
|
||||
enqueue_openat2 st k args;
|
||||
schedule st
|
||||
| effect (Connect (sw, fd, addr)) k ->
|
||||
let k = { Suspended.k; tid } in
|
||||
enqueue_connect ?sw st k fd addr;
|
||||
|
@ -62,6 +62,17 @@ val with_chunk : (Uring.Region.chunk -> 'a) -> 'a
|
||||
val openfile : sw:Switch.t -> string -> Unix.open_flag list -> int -> FD.t
|
||||
(** Like {!Unix.open_file}. *)
|
||||
|
||||
val openat2 :
|
||||
sw:Switch.t ->
|
||||
?seekable:bool ->
|
||||
access:[`R|`W|`RW] ->
|
||||
flags:Uring.Open_flags.t ->
|
||||
perm:Unix.file_perm ->
|
||||
resolve:Uring.Resolve.t ->
|
||||
?dir:FD.t -> string -> FD.t
|
||||
(** [openat2 ~sw ~flags ~perm ~resolve ~dir path] opens [dir/path].
|
||||
See {!Uring.openat2} for details. *)
|
||||
|
||||
val read_upto : ?sw:Switch.t -> ?file_offset:Optint.Int63.t -> FD.t -> Uring.Region.chunk -> int -> int
|
||||
(** [read_upto fd chunk len] reads at most [len] bytes from [fd],
|
||||
returning as soon as some data is available.
|
||||
@ -124,6 +135,8 @@ module Objects : sig
|
||||
network : Eio.Network.t;
|
||||
domain_mgr : Eio.Domain_manager.t;
|
||||
clock : Eio.Time.clock;
|
||||
fs : Eio.Dir.t;
|
||||
cwd : Eio.Dir.t;
|
||||
>
|
||||
|
||||
val get_fd : <has_fd; ..> -> FD.t
|
||||
|
21
lib_eio_linux/eio_stubs.c
Normal file
21
lib_eio_linux/eio_stubs.c
Normal file
@ -0,0 +1,21 @@
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <caml/mlvalues.h>
|
||||
#include <caml/memory.h>
|
||||
#include <caml/signals.h>
|
||||
#include <caml/unixsupport.h>
|
||||
|
||||
CAMLprim value caml_eio_mkdirat(value v_fd, value v_path, value v_perm) {
|
||||
CAMLparam1(v_path);
|
||||
char *path;
|
||||
int ret;
|
||||
caml_unix_check_path(v_path, "mkdirat");
|
||||
path = caml_stat_strdup(String_val(v_path));
|
||||
caml_enter_blocking_section();
|
||||
ret = mkdirat(Int_val(v_fd), path, Int_val(v_perm));
|
||||
caml_leave_blocking_section();
|
||||
caml_stat_free(path);
|
||||
if (ret == -1) uerror("mkdirat", v_path);
|
||||
CAMLreturn(Val_unit);
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit 31b098549f2017852b28f6821eb9e8657672850c
|
||||
Subproject commit f73cc8c73c7bd26ccb3d7a628150be1898c29ad6
|
248
tests/test_fs.md
Normal file
248
tests/test_fs.md
Normal file
@ -0,0 +1,248 @@
|
||||
# Setting up the environment
|
||||
|
||||
```ocaml
|
||||
# #require "eio_main";;
|
||||
# ignore @@ Unix.umask 0o022;;
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
```ocaml
|
||||
open Eio.Std
|
||||
|
||||
let run (fn : sw:Switch.t -> Eio.Stdenv.t -> unit) =
|
||||
try
|
||||
Eio_main.run @@ fun env ->
|
||||
Switch.top @@ fun sw ->
|
||||
fn ~sw env;
|
||||
print_endline "ok"
|
||||
with
|
||||
| Failure msg -> print_endline msg
|
||||
| ex -> print_endline (Printexc.to_string ex)
|
||||
|
||||
let read_all ?sw flow =
|
||||
let b = Buffer.create 100 in
|
||||
Eio.Flow.copy ?sw flow (Eio.Flow.buffer_sink b);
|
||||
Buffer.contents b
|
||||
|
||||
let write_file ~sw ~create ?append dir path content =
|
||||
Switch.sub sw ~on_error:raise (fun sw ->
|
||||
let flow = Eio.Dir.open_out ~sw ~create ?append dir path in
|
||||
Eio.Flow.copy_string content flow
|
||||
)
|
||||
|
||||
let try_write_file ~sw ~create ?append dir path content =
|
||||
match write_file ~sw ~create ?append dir path content with
|
||||
| () -> traceln "write %S -> ok" path
|
||||
| exception ex -> traceln "write %S -> %a" path Fmt.exn ex
|
||||
|
||||
let read_file ~sw dir path =
|
||||
Switch.sub sw ~on_error:raise (fun sw ->
|
||||
let flow = Eio.Dir.open_in ~sw dir path in
|
||||
read_all flow
|
||||
)
|
||||
|
||||
let try_mkdir dir path =
|
||||
match Eio.Dir.mkdir dir path ~perm:0o700 with
|
||||
| () -> traceln "mkdir %S -> ok" path
|
||||
| exception ex -> traceln "mkdir %S -> %a" path Fmt.exn ex
|
||||
|
||||
let chdir path =
|
||||
traceln "chdir %S" path;
|
||||
Unix.chdir path
|
||||
```
|
||||
|
||||
# Basic test cases
|
||||
|
||||
Creating a file and reading it back:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~sw ~create:(`Exclusive 0o666) cwd "test-file" "my-data";
|
||||
traceln "Got %S" @@ read_file ~sw cwd "test-file"
|
||||
Got "my-data"
|
||||
ok
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Check the file got the correct permissions (subject to the umask set above):
|
||||
```ocaml
|
||||
# traceln "Perm = %o" ((Unix.stat "test-file").st_perm);;
|
||||
Perm = 644
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
# Sandboxing
|
||||
|
||||
Trying to use cwd to access a file outside of that subtree fails:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~sw ~create:(`Exclusive 0o666) cwd "../test-file" "my-data";
|
||||
failwith "Should have failed"
|
||||
Eio.Dir.Permission_denied("../test-file", _)
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Trying to use cwd to access an absolute path fails:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~sw ~create:(`Exclusive 0o666) cwd "/tmp/test-file" "my-data";
|
||||
failwith "Should have failed"
|
||||
Eio.Dir.Permission_denied("/tmp/test-file", _)
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
# Creation modes
|
||||
|
||||
Exclusive create fails if already exists:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~sw ~create:(`Exclusive 0o666) cwd "test-file" "first-write";
|
||||
write_file ~sw ~create:(`Exclusive 0o666) cwd "test-file" "first-write";
|
||||
failwith "Should have failed"
|
||||
Unix.Unix_error(Unix.EEXIST, "openat2", "")
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
If-missing create succeeds if already exists:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~sw ~create:(`If_missing 0o666) cwd "test-file" "1st-write-original";
|
||||
write_file ~sw ~create:(`If_missing 0o666) cwd "test-file" "2nd-write";
|
||||
traceln "Got %S" @@ read_file ~sw cwd "test-file"
|
||||
Got "2nd-write-original"
|
||||
ok
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Truncate create succeeds if already exists, and truncates:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~sw ~create:(`Or_truncate 0o666) cwd "test-file" "1st-write-original";
|
||||
write_file ~sw ~create:(`Or_truncate 0o666) cwd "test-file" "2nd-write";
|
||||
traceln "Got %S" @@ read_file ~sw cwd "test-file"
|
||||
Got "2nd-write"
|
||||
ok
|
||||
- : unit = ()
|
||||
# Unix.unlink "test-file";;
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Error if no create and doesn't exist:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~sw ~create:`Never cwd "test-file" "1st-write-original";
|
||||
traceln "Got %S" @@ read_file ~sw cwd "test-file"
|
||||
Unix.Unix_error(Unix.ENOENT, "openat2", "")
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Appending to an existing file:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~sw ~create:(`Or_truncate 0o666) cwd "test-file" "1st-write-original";
|
||||
write_file ~sw ~create:`Never ~append:true cwd "test-file" "2nd-write";
|
||||
traceln "Got %S" @@ read_file ~sw cwd "test-file"
|
||||
Got "1st-write-original2nd-write"
|
||||
ok
|
||||
- : unit = ()
|
||||
# Unix.unlink "test-file";;
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
# Mkdir
|
||||
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
try_mkdir cwd "subdir";
|
||||
try_mkdir cwd "subdir/nested";
|
||||
write_file ~sw ~create:(`Exclusive 0o600) cwd "subdir/nested/test-file" "data";
|
||||
()
|
||||
mkdir "subdir" -> ok
|
||||
mkdir "subdir/nested" -> ok
|
||||
ok
|
||||
- : unit = ()
|
||||
# Unix.unlink "subdir/nested/test-file"; Unix.rmdir "subdir/nested"; Unix.rmdir "subdir";;
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Creating directories with nesting, symlinks, etc:
|
||||
```ocaml
|
||||
# Unix.symlink "/" "to-root";;
|
||||
- : unit = ()
|
||||
# Unix.symlink "subdir" "to-subdir";;
|
||||
- : unit = ()
|
||||
# Unix.symlink "foo" "dangle";;
|
||||
- : unit = ()
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
try_mkdir cwd "subdir";
|
||||
try_mkdir cwd "to-subdir/nested";
|
||||
try_mkdir cwd "to-root/tmp/foo";
|
||||
try_mkdir cwd "../foo";
|
||||
try_mkdir cwd "to-subdir";
|
||||
try_mkdir cwd "dangle/foo";
|
||||
()
|
||||
mkdir "subdir" -> ok
|
||||
mkdir "to-subdir/nested" -> ok
|
||||
mkdir "to-root/tmp/foo" -> Eio.Dir.Permission_denied("to-root/tmp", _)
|
||||
mkdir "../foo" -> Eio.Dir.Permission_denied("..", _)
|
||||
mkdir "to-subdir" -> Unix.Unix_error(Unix.EEXIST, "mkdirat", "to-subdir")
|
||||
mkdir "dangle/foo" -> Unix.Unix_error(Unix.ENOENT, "openat2", "")
|
||||
ok
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
# Limiting to a subdirectory
|
||||
|
||||
Create a sandbox, write a file with it, then read it from outside:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
try_mkdir cwd "sandbox";
|
||||
let subdir = Eio.Dir.open_dir ~sw cwd "sandbox" in
|
||||
write_file ~sw ~create:(`Exclusive 0o600) subdir "test-file" "data";
|
||||
try_mkdir subdir "../new-sandbox";
|
||||
traceln "Got %S" @@ read_file ~sw cwd "sandbox/test-file"
|
||||
mkdir "sandbox" -> ok
|
||||
mkdir "../new-sandbox" -> Eio.Dir.Permission_denied("..", _)
|
||||
Got "data"
|
||||
ok
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
# Unconfined FS access
|
||||
|
||||
We create a directory and chdir into it.
|
||||
Using `cwd` we can't access the parent, but using `fs` we can:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
let fs = Eio.Stdenv.fs env in
|
||||
try_mkdir cwd "fs-test";
|
||||
chdir "fs-test";
|
||||
Fun.protect ~finally:(fun () -> chdir "..") (fun () ->
|
||||
try_mkdir cwd "../outside-cwd";
|
||||
try_write_file ~sw ~create:(`Exclusive 0o600) cwd "../test-file" "data";
|
||||
try_mkdir fs "../outside-cwd";
|
||||
try_write_file ~sw ~create:(`Exclusive 0o600) fs "../test-file" "data";
|
||||
);
|
||||
Unix.unlink "test-file";
|
||||
Unix.rmdir "outside-cwd"
|
||||
mkdir "fs-test" -> ok
|
||||
chdir "fs-test"
|
||||
mkdir "../outside-cwd" -> Eio.Dir.Permission_denied("..", _)
|
||||
write "../test-file" -> Eio.Dir.Permission_denied("../test-file", _)
|
||||
mkdir "../outside-cwd" -> ok
|
||||
write "../test-file" -> ok
|
||||
chdir ".."
|
||||
ok
|
||||
- : unit = ()
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user