eio/tests/test_fs.md
Thomas Leonard dddc0735eb Add Eio_unix.FD
This allows creating FDs with Eio and then using them in legacy code.
2022-02-02 14:18:07 +00:00

8.1 KiB

Setting up the environment

# #require "eio_main";;
# ignore @@ Unix.umask 0o022;;
- : unit = ()
let () =
  Printexc.register_printer (function
    | Eio.Dir.Permission_denied (path, _) -> Some (Fmt.str "Eio.Dir.Permission_denied (%S, _)" path)
    | Eio.Dir.Already_exists (path, _)    -> Some (Fmt.str "Eio.Dir.Already_exists (%S, _)" path)
    | Eio.Dir.Not_found (path, _)         -> Some (Fmt.str "Eio.Dir.Not_found (%S, _)" path)
    | _ -> None
  )

open Eio.Std

let run (fn : Eio.Stdenv.t -> unit) =
  Eio_main.run @@ fun env ->
  fn env

let try_write_file ~create ?append dir path content =
  match Eio.Dir.save ~create ?append dir path content 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

let chdir path =
  traceln "chdir %S" path;
  Unix.chdir path

Basic test cases

Creating a file and reading it back:

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  Eio.Dir.save ~create:(`Exclusive 0o666) cwd "test-file" "my-data";
  traceln "Got %S" @@ Eio.Dir.load cwd "test-file";;
+Got "my-data"
- : unit = ()

Check the file got the correct permissions (subject to the umask set above):

# Printf.printf "Perm = %o\n" ((Unix.stat "test-file").st_perm);;
Perm = 644
- : unit = ()

Sandboxing

Trying to use cwd to access a file outside of that subtree fails:

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  Eio.Dir.save ~create:(`Exclusive 0o666) cwd "../test-file" "my-data";
  failwith "Should have failed";;
Exception: Eio.Dir.Permission_denied ("../test-file", _)

Trying to use cwd to access an absolute path fails:

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  Eio.Dir.save ~create:(`Exclusive 0o666) cwd "/tmp/test-file" "my-data";
  failwith "Should have failed";;
Exception: Eio.Dir.Permission_denied ("/tmp/test-file", _)

Creation modes

Exclusive create fails if already exists:

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  Eio.Dir.save ~create:(`Exclusive 0o666) cwd "test-file" "first-write";
  Eio.Dir.save ~create:(`Exclusive 0o666) cwd "test-file" "first-write";
  failwith "Should have failed";;
Exception: Eio.Dir.Already_exists ("test-file", _)

If-missing create succeeds if already exists:

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  Eio.Dir.save ~create:(`If_missing 0o666) cwd "test-file" "1st-write-original";
  Eio.Dir.save ~create:(`If_missing 0o666) cwd "test-file" "2nd-write";
  traceln "Got %S" @@ Eio.Dir.load cwd "test-file";;
+Got "2nd-write-original"
- : unit = ()

Truncate create succeeds if already exists, and truncates:

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  Eio.Dir.save ~create:(`Or_truncate 0o666) cwd "test-file" "1st-write-original";
  Eio.Dir.save ~create:(`Or_truncate 0o666) cwd "test-file" "2nd-write";
  traceln "Got %S" @@ Eio.Dir.load cwd "test-file";;
+Got "2nd-write"
- : unit = ()
# Unix.unlink "test-file";;
- : unit = ()

Error if no create and doesn't exist:

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  Eio.Dir.save ~create:`Never cwd "test-file" "1st-write-original";
  traceln "Got %S" @@ Eio.Dir.load cwd "test-file";;
Exception: Eio.Dir.Not_found ("test-file", _)

Appending to an existing file:

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  Eio.Dir.save ~create:(`Or_truncate 0o666) cwd "test-file" "1st-write-original";
  Eio.Dir.save ~create:`Never ~append:true cwd "test-file" "2nd-write";
  traceln "Got %S" @@ Eio.Dir.load cwd "test-file";;
+Got "1st-write-original2nd-write"
- : unit = ()
# Unix.unlink "test-file";;
- : unit = ()

Mkdir

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  try_mkdir cwd "subdir";
  try_mkdir cwd "subdir/nested";
  Eio.Dir.save ~create:(`Exclusive 0o600) cwd "subdir/nested/test-file" "data";
  ();;
+mkdir "subdir" -> ok
+mkdir "subdir/nested" -> ok
- : unit = ()
# Unix.unlink "subdir/nested/test-file"; Unix.rmdir "subdir/nested"; Unix.rmdir "subdir";;
- : unit = ()

Creating directories with nesting, symlinks, etc:

# Unix.symlink "/" "to-root";;
- : unit = ()
# Unix.symlink "subdir" "to-subdir";;
- : unit = ()
# Unix.symlink "foo" "dangle";;
- : unit = ()
# run @@ fun 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/foo", _)
+mkdir "../foo" -> Eio.Dir.Permission_denied ("../foo", _)
+mkdir "to-subdir" -> Eio.Dir.Already_exists ("to-subdir", _)
+mkdir "dangle/foo" -> Eio.Dir.Not_found ("dangle", _)
- : unit = ()

Limiting to a subdirectory

Create a sandbox, write a file with it, then read it from outside:

# run @@ fun env ->
  Switch.run @@ fun sw ->
  let cwd = Eio.Stdenv.cwd env in
  try_mkdir cwd "sandbox";
  let subdir = Eio.Dir.open_dir ~sw cwd "sandbox" in
  Eio.Dir.save ~create:(`Exclusive 0o600) subdir "test-file" "data";
  try_mkdir subdir "../new-sandbox";
  traceln "Got %S" @@ Eio.Dir.load cwd "sandbox/test-file";;
+mkdir "sandbox" -> ok
+mkdir "../new-sandbox" -> Eio.Dir.Permission_denied ("../new-sandbox", _)
+Got "data"
- : 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:

# run @@ fun 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 ~create:(`Exclusive 0o600) cwd "../test-file" "data";
    try_mkdir fs "../outside-cwd";
    try_write_file ~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 ("../outside-cwd", _)
+write "../test-file" -> Eio.Dir.Permission_denied ("../test-file", _)
+mkdir "../outside-cwd" -> ok
+write "../test-file" -> ok
+chdir ".."
- : unit = ()

Can use fs to access absolute paths:

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  let fs = Eio.Stdenv.fs env in
  let b = Buffer.create 10 in
  Eio.Dir.with_open_in fs Filename.null (fun flow -> Eio.Flow.copy flow (Eio.Flow.buffer_sink b));
  traceln "Read %S and got %S" Filename.null (Buffer.contents b);
  traceln "Trying with cwd instead fails:";
  Eio.Dir.with_open_in cwd Filename.null (fun flow -> Eio.Flow.copy flow (Eio.Flow.buffer_sink b));;;
+Read "/dev/null" and got ""
+Trying with cwd instead fails:
Exception: Eio.Dir.Permission_denied ("/dev/null", _)

Streamling lines

# run @@ fun env ->
  let cwd = Eio.Stdenv.cwd env in
  Eio.Dir.save ~create:(`Exclusive 0o600) cwd "test-data" "one\ntwo\nthree";
  Eio.Dir.with_lines cwd "test-data" (fun lines ->
     Seq.iter (traceln "Line: %s") lines
  );;
+Line: one
+Line: two
+Line: three
- : unit = ()

Unix interop

We can get the Unix FD from the flow and use it directly:

# run @@ fun env ->
  let fs = Eio.Stdenv.fs env in
  Eio.Dir.with_open_in fs Filename.null (fun flow ->
     match Eio_unix.FD.peek flow with
     | None -> failwith "No Unix file descriptor!"
     | Some fd ->
        let got = Unix.read fd (Bytes.create 10) 0 10 in
        traceln "Read %d bytes from null device" got
  );;
+Read 0 bytes from null device
- : unit = ()

We can also remove it from the flow completely and take ownership of it. In that case, with_open_in will no longer close it on exit:

# run @@ fun env ->
  let fs = Eio.Stdenv.fs env in
  let fd = Eio.Dir.with_open_in fs Filename.null (fun flow -> Option.get (Eio_unix.FD.take flow)) in
  let got = Unix.read fd (Bytes.create 10) 0 10 in
  traceln "Read %d bytes from null device" got;
  Unix.close fd;;
+Read 0 bytes from null device
- : unit = ()