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: 3 additions & 3 deletions lib/just_bash/commands/mkdir.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ defmodule JustBash.Commands.Mkdir do
{:error, %VFS.Error{kind: :eexist}} ->
{err_acc <> "mkdir: cannot create directory '#{path}': File exists\n", 1, fs_acc}

{:error, %VFS.Error{kind: :enoent}} ->
{err_acc <> "mkdir: cannot create directory '#{path}': No such file or directory\n",
1, fs_acc}
{:error, %VFS.Error{} = error} ->
{err_acc <> "mkdir: cannot create directory '#{path}': #{FS.strerror(error)}\n", 1,
fs_acc}
end
end)

Expand Down
4 changes: 2 additions & 2 deletions lib/just_bash/commands/rm.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ defmodule JustBash.Commands.Rm do
{:error, %VFS.Error{kind: :enoent}} ->
{err_acc <> "rm: cannot remove '#{path}': No such file or directory\n", 1, fs_acc}

{:error, %VFS.Error{kind: :enotempty}} ->
{err_acc <> "rm: cannot remove '#{path}': Directory not empty\n", 1, fs_acc}
{:error, %VFS.Error{} = error} ->
{err_acc <> "rm: cannot remove '#{path}': #{FS.strerror(error)}\n", 1, fs_acc}
end
end)

Expand Down
32 changes: 32 additions & 0 deletions test/mount_integration_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
defmodule JustBash.MountIntegrationTest do
use ExUnit.Case, async: true

alias JustBash.Test.NoEmptyDirsBackend

describe "mutations on restricted mounts" do
test "mkdir on a backend without directory support fails with exit 1, not a crash" do
bash = JustBash.new() |> JustBash.mount("/mnt", %NoEmptyDirsBackend{})

{result, _bash} = JustBash.exec(bash, "mkdir /mnt/newdir")
assert result.exit_code == 1
assert result.stderr =~ "mkdir: cannot create directory '/mnt/newdir'"
assert result.stderr =~ "Operation not supported"
end

test "mkdir -p on a backend without directory support fails with exit 1, not a crash" do
bash = JustBash.new() |> JustBash.mount("/mnt", %NoEmptyDirsBackend{})

{result, _bash} = JustBash.exec(bash, "mkdir -p /mnt/a/b")
assert result.exit_code == 1
assert result.stderr =~ "Operation not supported"
end

test "rm on a read-only backend fails with exit 1, not a crash" do
bash =
JustBash.new()
|> JustBash.mount("/mnt", %NoEmptyDirsBackend{files: %{"/f.txt" => "x"}, read_only: true})

{result, _bash} = JustBash.exec(bash, "rm /mnt/f.txt")
assert result.exit_code == 1
assert result.stderr =~ "rm: cannot remove '/mnt/f.txt'"
assert result.stderr =~ "Read-only file system"
end
end

describe "JustBash.mount/3" do
test "bash commands read a mounted VFS.Memory backend" do
bash =
Expand Down
56 changes: 56 additions & 0 deletions test/support/no_empty_dirs_backend.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# A backend where files are writable but empty directories cannot exist —
# the shape of any git-tree-backed mount (e.g. an exgit workspace). With
# `read_only: true`, mutations fail with :erofs instead.
defmodule JustBash.Test.NoEmptyDirsBackend do
@moduledoc false
defstruct files: %{}, read_only: false
end

defimpl VFS.Mountable, for: JustBash.Test.NoEmptyDirsBackend do
use VFS.Skeleton

alias VFS.{Error, Stat}

@mtime ~U[1970-01-01 00:00:00Z]

def exists?(b, path), do: {path == "/" or Map.has_key?(b.files, path), b}

def stat(b, "/"), do: {:ok, Stat.directory(@mtime), b}

def stat(b, path) do
case Map.fetch(b.files, path) do
{:ok, content} -> {:ok, Stat.regular(byte_size(content), @mtime), b}
:error -> {:error, Error.new(:enoent, path: path)}
end
end

def readdir(b, "/"),
do: {:ok, b.files |> Map.keys() |> Enum.map(&String.trim_leading(&1, "/")), b}

def readdir(_b, path), do: {:error, Error.new(:enoent, path: path)}

def stream_read(b, path, opts) do
with {:ok, content} <- Map.fetch(b.files, path),
{:ok, stream} <- VFS.StreamOptions.apply(content, opts) do
{:ok, stream, b}
else
:error -> {:error, Error.new(:enoent, path: path)}
{:error, kind} -> {:error, Error.new(kind, path: path)}
end
end

def write_file(%{read_only: true}, path, _content, _opts),
do: {:error, Error.new(:erofs, path: path)}

def write_file(b, path, content, _opts),
do: {:ok, %{b | files: Map.put(b.files, path, content)}}

def mkdir(_b, path, _opts) do
{:error, Error.new(:enotsup, path: path, message: "backend cannot store empty directories")}
end

def rm(%{read_only: true}, path, _opts), do: {:error, Error.new(:erofs, path: path)}
def rm(b, path, _opts), do: {:ok, %{b | files: Map.delete(b.files, path)}}

def capabilities(_), do: MapSet.new([:read, :write])
end
Loading