From 869e50b6cd3284fe1d33d90963c4c12e1d6c30f8 Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 12 Jun 2026 14:33:48 +0800 Subject: [PATCH 01/10] feat: support symlink follow control --- example/cli_tools/moon.mod.json | 2 +- example/lottie_manifest/moon.mod.json | 2 +- example/morm_query/moon.mod.json | 4 ++-- fs.mbt | 15 ++++++++++++++- internal/raw/fs.mbt | 16 +++++++++++----- internal/raw/pkg.generated.mbti | 8 +++++--- internal/raw/types.mbt | 7 ++++++- moon.mod.json | 2 +- pkg.generated.mbti | 4 ++-- 9 files changed, 43 insertions(+), 17 deletions(-) diff --git a/example/cli_tools/moon.mod.json b/example/cli_tools/moon.mod.json index 6a79c15..11255d3 100644 --- a/example/cli_tools/moon.mod.json +++ b/example/cli_tools/moon.mod.json @@ -2,7 +2,7 @@ "name": "moonbit-community/miniio-example-cli-tools", "version": "0.1.0", "deps": { - "moonbit-community/miniio": "0.1.0" + "moonbit-community/miniio": "0.2.0" }, "preferred-target": "wasm" } diff --git a/example/lottie_manifest/moon.mod.json b/example/lottie_manifest/moon.mod.json index f44529d..db1985c 100644 --- a/example/lottie_manifest/moon.mod.json +++ b/example/lottie_manifest/moon.mod.json @@ -2,7 +2,7 @@ "name": "moonbit-community/miniio-example-lottie-manifest", "version": "0.1.0", "deps": { - "moonbit-community/miniio": "0.1.0", + "moonbit-community/miniio": "0.2.0", "cg-zhou/moon_lottie": "0.3.0" }, "preferred-target": "wasm" diff --git a/example/morm_query/moon.mod.json b/example/morm_query/moon.mod.json index 8da2f1d..057703d 100644 --- a/example/morm_query/moon.mod.json +++ b/example/morm_query/moon.mod.json @@ -2,8 +2,8 @@ "name": "moonbit-community/miniio-example-morm-query", "version": "0.1.0", "deps": { - "moonbit-community/miniio": "0.1.0", - "oboard/morm": "0.3.12" + "moonbit-community/miniio": "0.2.0", + "oboard/morm": "0.3.15" }, "preferred-target": "wasm" } diff --git a/fs.mbt b/fs.mbt index 3b4189b..bb62b9e 100644 --- a/fs.mbt +++ b/fs.mbt @@ -38,14 +38,21 @@ pub fn open( mode? : Mode = ReadOnly, append? : Bool = false, create_mode? : CreateMode = OpenExisting, + follow_symlink? : Bool = true, ) -> File raise Errno { { raw: wrap(() => { + let lookup_flags = if follow_symlink { + @raw.LookupFlags::new().symlink_follow() + } else { + @raw.LookupFlags::new() + } @raw.open( path, mode=mode.to_raw(), append~, create_mode=create_mode.to_raw(), + lookup_flags~, ) }), read_buf: @io.ReaderBuffer::new(), @@ -85,8 +92,14 @@ pub fn readdir( path : StringView, include_hidden? : Bool = true, sort? : Bool = false, + follow_symlink? : Bool = true, ) -> Array[String] raise Errno { - let result = wrap(() => @raw.readdir_names(path)) + let lookup_flags = if follow_symlink { + @raw.LookupFlags::new().symlink_follow() + } else { + @raw.LookupFlags::new() + } + let result = wrap(() => @raw.readdir_names(path, lookup_flags~)) let result = if include_hidden { result } else { diff --git a/internal/raw/fs.mbt b/internal/raw/fs.mbt index 583c5ab..fe10a8c 100644 --- a/internal/raw/fs.mbt +++ b/internal/raw/fs.mbt @@ -225,6 +225,7 @@ pub fn open( mode? : Mode = ReadOnly, append? : Bool = false, create_mode? : CreateMode = OpenExisting, + lookup_flags? : LookupFlags = LookupFlags::new(), ) -> File raise Errno { let (base_fd, relative_path) = resolve_path(path) let oflags = create_mode.to_open_flags() @@ -235,7 +236,7 @@ pub fn open( let needs_resize = mode is WriteOnly || mode is ReadWrite { fd: base_fd.path_open( - LookupFlags::new(), + lookup_flags, relative_path, oflags, file_rights(mode, needs_resize), @@ -265,12 +266,15 @@ pub fn create(path : StringView) -> File raise Errno { } ///| -fn open_dir(path : StringView) -> Fd raise Errno { +fn open_dir( + path : StringView, + lookup_flags? : LookupFlags = LookupFlags::new(), +) -> Fd raise Errno { let (base_fd, relative_path) = resolve_path(path) let rights = Rights::new() |> Rights::set(Fd_read) |> Rights::set(Fd_readdir) let oflags = OpenFlags::new() |> OpenFlags::set(Directory) base_fd.path_open( - LookupFlags::new(), + lookup_flags, relative_path, oflags, rights, @@ -301,11 +305,12 @@ pub fn remove_dir(path : StringView) -> Unit raise Errno { pub fn readdir( path : StringView, buffer_size? : Int = 4096, + lookup_flags? : LookupFlags = LookupFlags::new(), ) -> Array[DirEntry] raise Errno { if buffer_size <= 0 { raise Inval } - let fd = open_dir(path) + let fd = open_dir(path, lookup_flags~) with_closed_fd(fd, fd => { let result : Array[DirEntry] = [] let mut cookie : DirCookie = DirCookie(0) @@ -345,9 +350,10 @@ pub fn readdir( pub fn readdir_names( path : StringView, buffer_size? : Int = 4096, + lookup_flags? : LookupFlags = LookupFlags::new(), ) -> Array[String] raise Errno { let names = [] - for entry in readdir(path, buffer_size~) { + for entry in readdir(path, buffer_size~, lookup_flags~) { names.push(entry.name()) } names diff --git a/internal/raw/pkg.generated.mbti b/internal/raw/pkg.generated.mbti index cd8ddd6..86db317 100644 --- a/internal/raw/pkg.generated.mbti +++ b/internal/raw/pkg.generated.mbti @@ -14,7 +14,7 @@ pub fn envs() -> Array[(String, String)] raise Errno pub fn mkdir(StringView) -> Unit raise Errno -pub fn open(StringView, mode? : Mode, append? : Bool, create_mode? : CreateMode) -> File raise Errno +pub fn open(StringView, mode? : Mode, append? : Bool, create_mode? : CreateMode, lookup_flags? : LookupFlags) -> File raise Errno pub fn open_read(StringView) -> File raise Errno @@ -24,9 +24,9 @@ pub fn preopens() -> Array[String] raise Errno pub fn read_file(StringView) -> Bytes raise Errno -pub fn readdir(StringView, buffer_size? : Int) -> Array[DirEntry] raise Errno +pub fn readdir(StringView, buffer_size? : Int, lookup_flags? : LookupFlags) -> Array[DirEntry] raise Errno -pub fn readdir_names(StringView, buffer_size? : Int) -> Array[String] raise Errno +pub fn readdir_names(StringView, buffer_size? : Int, lookup_flags? : LookupFlags) -> Array[String] raise Errno pub fn remove_dir(StringView) -> Unit raise Errno @@ -177,6 +177,8 @@ type FileType derive(Eq, @debug.Debug) type Inode derive(Eq, @debug.Debug) type LookupFlags derive(Eq, @debug.Debug) +pub fn LookupFlags::new() -> Self +pub fn LookupFlags::symlink_follow(Self) -> Self pub(all) enum Mode { ReadOnly diff --git a/internal/raw/types.mbt b/internal/raw/types.mbt index 440858b..4bfaebf 100644 --- a/internal/raw/types.mbt +++ b/internal/raw/types.mbt @@ -391,10 +391,15 @@ fn FdFlags::set(self : FdFlags, flag : FdFlag) -> FdFlags { struct LookupFlags(UInt) derive(Eq, Debug) ///| -fn LookupFlags::new() -> LookupFlags { +pub fn LookupFlags::new() -> LookupFlags { 0 } +///| +pub fn LookupFlags::symlink_follow(self : LookupFlags) -> LookupFlags { + self.0 | 1 +} + ///| struct OpenFlags(UInt) derive(Eq, Debug) diff --git a/moon.mod.json b/moon.mod.json index bc8d357..747741f 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -1,6 +1,6 @@ { "name": "moonbit-community/miniio", - "version": "0.2.0", + "version": "0.2.1", "description": "Small, portable WASIp1 I/O SDK for building MoonBit CLI tools and agent skills.", "keywords": ["wasi", "wasip1", "io", "cli", "agent"], "readme": "README.md", diff --git a/pkg.generated.mbti b/pkg.generated.mbti index 00f2b1c..61d6c84 100644 --- a/pkg.generated.mbti +++ b/pkg.generated.mbti @@ -31,7 +31,7 @@ pub fn kind(StringView) -> FileKind raise Errno pub fn mkdir(StringView) -> Unit raise Errno -pub fn open(StringView, mode? : Mode, append? : Bool, create_mode? : CreateMode) -> File raise Errno +pub fn open(StringView, mode? : Mode, append? : Bool, create_mode? : CreateMode, follow_symlink? : Bool) -> File raise Errno pub fn read_file(StringView) -> &@io.Data raise Errno @@ -39,7 +39,7 @@ pub fn read_json_file(StringView) -> Json raise Errno pub fn read_text_file(StringView) -> String raise Errno -pub fn readdir(StringView, include_hidden? : Bool, sort? : Bool) -> Array[String] raise Errno +pub fn readdir(StringView, include_hidden? : Bool, sort? : Bool, follow_symlink? : Bool) -> Array[String] raise Errno pub fn remove(StringView) -> Unit raise Errno From d6411e3a2a361daecd387dc79e2288e5742bc24a Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 12 Jun 2026 14:37:04 +0800 Subject: [PATCH 02/10] chore: apply moon formatter output --- example/cli_tools/moon.mod | 9 ++++++++ example/cli_tools/moon.mod.json | 8 ------- example/lottie_manifest/moon.mod | 10 ++++++++ example/lottie_manifest/moon.mod.json | 9 -------- example/lottie_manifest/moon.pkg | 2 +- example/morm_query/moon.mod | 10 ++++++++ example/morm_query/moon.mod.json | 9 -------- example/morm_query/moon.pkg | 2 +- fd.mbt | 6 ++--- io/data.mbt | 18 +++++++-------- io/moon.pkg | 4 ++-- io/pkg.generated.mbti | 22 +++++++++--------- io/reader.mbt | 33 +++++++++++++++------------ io/writer.mbt | 10 ++++---- moon.mod | 15 ++++++++++++ moon.mod.json | 10 -------- moon.pkg | 4 ++-- types.mbt | 4 ++-- 18 files changed, 99 insertions(+), 86 deletions(-) create mode 100644 example/cli_tools/moon.mod delete mode 100644 example/cli_tools/moon.mod.json create mode 100644 example/lottie_manifest/moon.mod delete mode 100644 example/lottie_manifest/moon.mod.json create mode 100644 example/morm_query/moon.mod delete mode 100644 example/morm_query/moon.mod.json create mode 100644 moon.mod delete mode 100644 moon.mod.json diff --git a/example/cli_tools/moon.mod b/example/cli_tools/moon.mod new file mode 100644 index 0000000..5fac76c --- /dev/null +++ b/example/cli_tools/moon.mod @@ -0,0 +1,9 @@ +name = "moonbit-community/miniio-example-cli-tools" + +version = "0.1.0" + +import { + "moonbit-community/miniio@0.2.0", +} + +preferred_target = "wasm" \ No newline at end of file diff --git a/example/cli_tools/moon.mod.json b/example/cli_tools/moon.mod.json deleted file mode 100644 index 11255d3..0000000 --- a/example/cli_tools/moon.mod.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "moonbit-community/miniio-example-cli-tools", - "version": "0.1.0", - "deps": { - "moonbit-community/miniio": "0.2.0" - }, - "preferred-target": "wasm" -} diff --git a/example/lottie_manifest/moon.mod b/example/lottie_manifest/moon.mod new file mode 100644 index 0000000..c6747d9 --- /dev/null +++ b/example/lottie_manifest/moon.mod @@ -0,0 +1,10 @@ +name = "moonbit-community/miniio-example-lottie-manifest" + +version = "0.1.0" + +import { + "moonbit-community/miniio@0.2.0", + "cg-zhou/moon_lottie@0.3.0", +} + +preferred_target = "wasm" \ No newline at end of file diff --git a/example/lottie_manifest/moon.mod.json b/example/lottie_manifest/moon.mod.json deleted file mode 100644 index db1985c..0000000 --- a/example/lottie_manifest/moon.mod.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "moonbit-community/miniio-example-lottie-manifest", - "version": "0.1.0", - "deps": { - "moonbit-community/miniio": "0.2.0", - "cg-zhou/moon_lottie": "0.3.0" - }, - "preferred-target": "wasm" -} diff --git a/example/lottie_manifest/moon.pkg b/example/lottie_manifest/moon.pkg index 99dd1c4..3833934 100644 --- a/example/lottie_manifest/moon.pkg +++ b/example/lottie_manifest/moon.pkg @@ -1,6 +1,6 @@ import { "moonbit-community/miniio", - "cg-zhou/moon_lottie/lib/parser" @parser, + "cg-zhou/moon_lottie/lib/parser", "moonbitlang/core/json", } diff --git a/example/morm_query/moon.mod b/example/morm_query/moon.mod new file mode 100644 index 0000000..2fd745c --- /dev/null +++ b/example/morm_query/moon.mod @@ -0,0 +1,10 @@ +name = "moonbit-community/miniio-example-morm-query" + +version = "0.1.0" + +import { + "moonbit-community/miniio@0.2.0", + "oboard/morm@0.3.15", +} + +preferred_target = "wasm" \ No newline at end of file diff --git a/example/morm_query/moon.mod.json b/example/morm_query/moon.mod.json deleted file mode 100644 index 057703d..0000000 --- a/example/morm_query/moon.mod.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "moonbit-community/miniio-example-morm-query", - "version": "0.1.0", - "deps": { - "moonbit-community/miniio": "0.2.0", - "oboard/morm": "0.3.15" - }, - "preferred-target": "wasm" -} diff --git a/example/morm_query/moon.pkg b/example/morm_query/moon.pkg index b77b6b5..8d0b3c3 100644 --- a/example/morm_query/moon.pkg +++ b/example/morm_query/moon.pkg @@ -1,7 +1,7 @@ import { "moonbit-community/miniio", "oboard/morm", - "oboard/morm/engine" @engine, + "oboard/morm/engine", } supported_targets = "wasm" diff --git a/fd.mbt b/fd.mbt index 2a7ee29..900d0e5 100644 --- a/fd.mbt +++ b/fd.mbt @@ -42,16 +42,16 @@ pub fn File::tell(self : File) -> UInt64 raise Errno { } ///| -pub impl @io.Reader for File with _get_internal_buffer(self) { +pub impl @io.Reader for File with fn _get_internal_buffer(self) { self.read_buf } ///| -pub impl @io.Reader for File with _direct_read(self, buf, offset~, max_len~) { +pub impl @io.Reader for File with fn _direct_read(self, buf, offset~, max_len~) { wrap(() => self.raw.read(buf, offset~, max_len~)) } ///| -pub impl @io.Writer for File with write_once(self, data) { +pub impl @io.Writer for File with fn write_once(self, data) { wrap(() => self.raw.write_once(data)) } diff --git a/io/data.mbt b/io/data.mbt index b2b1483..ef8a56e 100644 --- a/io/data.mbt +++ b/io/data.mbt @@ -14,42 +14,42 @@ ///| trait Data { - to_bytesview(Self) -> BytesView = _ - to_bytes(Self) -> Bytes + fn to_bytesview(Self) -> BytesView = _ + fn to_bytes(Self) -> Bytes } ///| -impl Data with to_bytesview(self) { +impl Data with fn to_bytesview(self) { self.to_bytes()[:] } ///| -pub impl Data for Bytes with to_bytes(self) { +pub impl Data for Bytes with fn to_bytes(self) { self } ///| -pub impl Data for BytesView with to_bytes(self) { +pub impl Data for BytesView with fn to_bytes(self) { self.to_owned() } ///| -pub impl Data for BytesView with to_bytesview(self) { +pub impl Data for BytesView with fn to_bytesview(self) { self } ///| -pub impl Data for String with to_bytes(self) { +pub impl Data for String with fn to_bytes(self) { @utf8.encode(self) } ///| -pub impl Data for StringView with to_bytes(self) { +pub impl Data for StringView with fn to_bytes(self) { @utf8.encode(self) } ///| -pub impl Data for Json with to_bytes(self) { +pub impl Data for Json with fn to_bytes(self) { @utf8.encode(self.stringify()) } diff --git a/io/moon.pkg b/io/moon.pkg index 85c172f..b76f51e 100644 --- a/io/moon.pkg +++ b/io/moon.pkg @@ -1,7 +1,7 @@ import { - "moonbit-community/miniio/internal/io_buffer" @io_buffer, + "moonbit-community/miniio/internal/io_buffer", "moonbitlang/core/cmp", - "moonbitlang/core/encoding/utf8" @utf8, + "moonbitlang/core/encoding/utf8", "moonbitlang/core/json", } diff --git a/io/pkg.generated.mbti b/io/pkg.generated.mbti index dd278c9..f343430 100644 --- a/io/pkg.generated.mbti +++ b/io/pkg.generated.mbti @@ -28,19 +28,19 @@ pub fn &Data::json(Self) -> Json raise pub fn &Data::text(Self) -> String raise pub(open) trait Reader { - _get_internal_buffer(Self) -> ReaderBuffer - _direct_read(Self, FixedArray[Byte], offset~ : Int, max_len~ : Int) -> Int raise - read(Self, FixedArray[Byte], offset? : Int, max_len? : Int) -> Int raise = _ - drop(Self, Int) -> Int raise = _ - read_exactly(Self, Int) -> Bytes raise = _ - read_some(Self, max_len? : Int) -> Bytes? raise = _ - read_all(Self) -> &Data raise = _ - read_until(Self, StringView) -> String? raise = _ + fn _get_internal_buffer(Self) -> ReaderBuffer + fn _direct_read(Self, FixedArray[Byte], offset~ : Int, max_len~ : Int) -> Int raise + fn read(Self, FixedArray[Byte], offset? : Int, max_len? : Int) -> Int raise = _ + fn drop(Self, Int) -> Int raise = _ + fn read_exactly(Self, Int) -> Bytes raise = _ + fn read_some(Self, max_len? : Int) -> Bytes? raise = _ + fn read_all(Self) -> &Data raise = _ + fn read_until(Self, StringView) -> String? raise = _ } pub(open) trait Writer { - write_once(Self, BytesView) -> Int raise - write(Self, &Data) -> Unit raise = _ - write_reader(Self, &Reader) -> Unit raise = _ + fn write_once(Self, BytesView) -> Int raise + fn write(Self, &Data) -> Unit raise = _ + fn write_reader(Self, &Reader) -> Unit raise = _ } diff --git a/io/reader.mbt b/io/reader.mbt index 0e203a0..a1d50f9 100644 --- a/io/reader.mbt +++ b/io/reader.mbt @@ -14,21 +14,26 @@ ///| pub(open) trait Reader { - _get_internal_buffer(Self) -> ReaderBuffer - _direct_read(Self, FixedArray[Byte], offset~ : Int, max_len~ : Int) -> Int raise - read(Self, FixedArray[Byte], offset? : Int, max_len? : Int) -> Int raise = _ - drop(Self, Int) -> Int raise = _ - read_exactly(Self, len : Int) -> Bytes raise = _ - read_some(Self, max_len? : Int) -> Bytes? raise = _ - read_all(Self) -> &Data raise = _ - read_until(Self, StringView) -> String? raise = _ + fn _get_internal_buffer(Self) -> ReaderBuffer + fn _direct_read(Self, FixedArray[Byte], offset~ : Int, max_len~ : Int) -> Int raise + fn read(Self, FixedArray[Byte], offset? : Int, max_len? : Int) -> Int raise = _ + fn drop(Self, Int) -> Int raise = _ + fn read_exactly(Self, len : Int) -> Bytes raise = _ + fn read_some(Self, max_len? : Int) -> Bytes? raise = _ + fn read_all(Self) -> &Data raise = _ + fn read_until(Self, StringView) -> String? raise = _ } ///| pub(all) suberror ReaderClosed derive(Debug, ToJson) ///| -impl Reader with read(self, dst, offset? = 0, max_len? = dst.length() - offset) { +impl Reader with fn read( + self, + dst, + offset? = 0, + max_len? = dst.length() - offset, +) { let buf = self._get_internal_buffer() if buf.len() > 0 { let n = @cmp.minimum(max_len, buf.len()) @@ -41,7 +46,7 @@ impl Reader with read(self, dst, offset? = 0, max_len? = dst.length() - offset) } ///| -impl Reader with drop(self, len) { +impl Reader with fn drop(self, len) { let buf = self._get_internal_buffer() if buf.len() >= len { buf.drop(len) @@ -70,7 +75,7 @@ impl Reader with drop(self, len) { } ///| -impl Reader with read_exactly(self, len) { +impl Reader with fn read_exactly(self, len) { let buf = FixedArray::make(len, b'\x00') for received = 0; received < len; { let new_received = self.read(buf, offset=received, max_len=len - received) @@ -83,7 +88,7 @@ impl Reader with read_exactly(self, len) { } ///| -impl Reader with read_some(self, max_len?) { +impl Reader with fn read_some(self, max_len?) { let buf = self._get_internal_buffer() if buf.len() > 0 { let buf_bytes = buf.buf().unsafe_reinterpret_as_bytes() @@ -115,7 +120,7 @@ impl Reader with read_some(self, max_len?) { } ///| -impl Reader with read_all(self) { +impl Reader with fn read_all(self) { let buffer_list = [] let mut buffer = FixedArray::make(1024, b'\x00') let mut offset = 0 @@ -138,7 +143,7 @@ impl Reader with read_all(self) { } ///| -impl Reader with read_until(self, sep) { +impl Reader with fn read_until(self, sep) { let sep = @utf8.encode(sep) let buf = self._get_internal_buffer() match buf.find_opt(sep, reader=self) { diff --git a/io/writer.mbt b/io/writer.mbt index 20b9b8b..6bf2646 100644 --- a/io/writer.mbt +++ b/io/writer.mbt @@ -14,13 +14,13 @@ ///| pub(open) trait Writer { - write_once(Self, BytesView) -> Int raise - write(Self, &Data) -> Unit raise = _ - write_reader(Self, &Reader) -> Unit raise = _ + fn write_once(Self, BytesView) -> Int raise + fn write(Self, &Data) -> Unit raise = _ + fn write_reader(Self, &Reader) -> Unit raise = _ } ///| -impl Writer with write(self, data) { +impl Writer with fn write(self, data) { let mut view = data.to_bytesview() while view.length() > 0 { let written = self.write_once(view) @@ -29,7 +29,7 @@ impl Writer with write(self, data) { } ///| -impl Writer with write_reader(self, reader) { +impl Writer with fn write_reader(self, reader) { let buf = reader._get_internal_buffer() if buf.len() > 0 { let buf_bytes = buf.buf().unsafe_reinterpret_as_bytes() diff --git a/moon.mod b/moon.mod new file mode 100644 index 0000000..4be378b --- /dev/null +++ b/moon.mod @@ -0,0 +1,15 @@ +name = "moonbit-community/miniio" + +version = "0.2.1" + +description = "Small, portable WASIp1 I/O SDK for building MoonBit CLI tools and agent skills." + +keywords = [ "wasi", "wasip1", "io", "cli", "agent" ] + +readme = "README.md" + +repository = "https://github.com/moonbit-community/miniio" + +license = "Apache-2.0" + +preferred_target = "wasm" \ No newline at end of file diff --git a/moon.mod.json b/moon.mod.json deleted file mode 100644 index 747741f..0000000 --- a/moon.mod.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "moonbit-community/miniio", - "version": "0.2.1", - "description": "Small, portable WASIp1 I/O SDK for building MoonBit CLI tools and agent skills.", - "keywords": ["wasi", "wasip1", "io", "cli", "agent"], - "readme": "README.md", - "repository": "https://github.com/moonbit-community/miniio", - "license": "Apache-2.0", - "preferred-target": "wasm" -} diff --git a/moon.pkg b/moon.pkg index a3205e4..4566bc6 100644 --- a/moon.pkg +++ b/moon.pkg @@ -1,6 +1,6 @@ import { - "moonbit-community/miniio/io" @io, - "moonbit-community/miniio/internal/raw" @raw, + "moonbit-community/miniio/io", + "moonbit-community/miniio/internal/raw", } supported_targets = "wasm" diff --git a/types.mbt b/types.mbt index b5b66df..c3e2f56 100644 --- a/types.mbt +++ b/types.mbt @@ -157,7 +157,7 @@ pub(all) suberror Errno { } derive(Eq, Debug) ///| -pub impl Show for Errno with to_string(self) { +pub impl Show for Errno with fn to_string(self) { match self { Success => "Success" TooBig => "TooBig" @@ -240,7 +240,7 @@ pub impl Show for Errno with to_string(self) { } ///| -pub impl Show for Errno with output(self, logger) { +pub impl Show for Errno with fn output(self, logger) { logger.write_string(self.to_string()) } From 18cb069862215b0b349c6eea6c758807852181c2 Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 12 Jun 2026 14:39:48 +0800 Subject: [PATCH 03/10] chore: remove deprecated try usage --- README.mbt.md | 6 +++--- example/cli_tools/moon.mod | 2 +- example/lottie_manifest/moon.mod | 2 +- example/morm_query/moon.mod | 2 +- fs_test.mbt | 37 ++++++++++++++++++++------------ io_test.mbt | 4 ++-- moon.mod | 2 +- 7 files changed, 32 insertions(+), 23 deletions(-) diff --git a/README.mbt.md b/README.mbt.md index dd25286..dd7b9c3 100644 --- a/README.mbt.md +++ b/README.mbt.md @@ -74,7 +74,7 @@ test "copy text between files" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) @miniio.write_text_file(dir + "/input.txt", "hello\n") @miniio.copy_file(dir + "/input.txt", dir + "/output.txt") @@ -92,7 +92,7 @@ test "append log file" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let path = dir + "/tool.log" @miniio.write_text_file(path, "start\n") @@ -111,7 +111,7 @@ test "read and write json" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let path = dir + "/manifest.json" @miniio.write_json_file(path, { "name": "miniio", "portable": true }) diff --git a/example/cli_tools/moon.mod b/example/cli_tools/moon.mod index 5fac76c..4020363 100644 --- a/example/cli_tools/moon.mod +++ b/example/cli_tools/moon.mod @@ -6,4 +6,4 @@ import { "moonbit-community/miniio@0.2.0", } -preferred_target = "wasm" \ No newline at end of file +preferred_target = "wasm" diff --git a/example/lottie_manifest/moon.mod b/example/lottie_manifest/moon.mod index c6747d9..ebf3837 100644 --- a/example/lottie_manifest/moon.mod +++ b/example/lottie_manifest/moon.mod @@ -7,4 +7,4 @@ import { "cg-zhou/moon_lottie@0.3.0", } -preferred_target = "wasm" \ No newline at end of file +preferred_target = "wasm" diff --git a/example/morm_query/moon.mod b/example/morm_query/moon.mod index 2fd745c..48cb209 100644 --- a/example/morm_query/moon.mod +++ b/example/morm_query/moon.mod @@ -7,4 +7,4 @@ import { "oboard/morm@0.3.15", } -preferred_target = "wasm" \ No newline at end of file +preferred_target = "wasm" diff --git a/fs_test.mbt b/fs_test.mbt index 205fbeb..4b82aec 100644 --- a/fs_test.mbt +++ b/fs_test.mbt @@ -21,7 +21,7 @@ test "write_file supports create append and truncate" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let path = dir + "/sample.txt" @miniio.write_file(path, b"hello") @@ -43,16 +43,17 @@ test "create modes distinguish existing files" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let path = dir + "/sample.txt" @miniio.write_text_file(path, "first", create_mode=CreateNew) inspect(@miniio.read_text_file(path), content="first") - debug_inspect( - try? @miniio.write_text_file(path, "second", create_mode=CreateNew), - content="Err(Exist)", - ) + try @miniio.write_text_file(path, "second", create_mode=CreateNew) catch { + e => debug_inspect(e, content="Exist") + } noraise { + _ => abort("expected CreateNew to reject an existing file") + } @miniio.write_text_file(path, "second", create_mode=CreateOrTruncate) inspect(@miniio.read_text_file(path), content="second") @@ -67,7 +68,7 @@ test "readdir sorts and filters hidden entries" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) @miniio.write_file(dir + "/b.txt", b"b") @miniio.write_file(dir + "/a.txt", b"a") @@ -92,7 +93,7 @@ test "exists and kind classify file and directory" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let path = dir + "/sample.txt" @miniio.write_file(path, b"sample") @@ -119,7 +120,7 @@ test "read_file returns io data" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let path = dir + "/sample.txt" @miniio.write_text_file(path, "hello") @@ -138,7 +139,7 @@ test "copy_file copies through reader and writer" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let src = dir + "/src.txt" let dst = dir + "/dst.txt" @@ -156,7 +157,7 @@ test "json file helpers read and write json" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let path = dir + "/config.json" @miniio.write_json_file(path, { "name": "miniio", "ok": true }) @@ -175,7 +176,7 @@ test "remove and recursive rmdir delete created paths" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let file_path = dir + "/sample.txt" let nested_dir = dir + "/nested" @@ -185,8 +186,16 @@ test "remove and recursive rmdir delete created paths" { @miniio.write_file(nested_dir + "/.hidden", b"hidden") @miniio.remove_file(file_path) - debug_inspect(try? @miniio.remove(file_path), content="Err(Noent)") + try @miniio.remove(file_path) catch { + e => debug_inspect(e, content="Noent") + } noraise { + _ => abort("expected removing a missing file to fail") + } @miniio.rmdir(dir, recursive=true) - debug_inspect(try? @miniio.rmdir(dir), content="Err(Noent)") + try @miniio.rmdir(dir) catch { + e => debug_inspect(e, content="Noent") + } noraise { + _ => abort("expected removing a missing directory to fail") + } } diff --git a/io_test.mbt b/io_test.mbt index 8d723a1..cf7336d 100644 --- a/io_test.mbt +++ b/io_test.mbt @@ -21,7 +21,7 @@ test "file implements io reader and writer" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let path = dir + "/sample.txt" let writer = @miniio.create(path) @@ -47,7 +47,7 @@ test "write_reader copies file content" { e => raise e } @miniio.mkdir(dir) - defer ignore(try? @miniio.rmdir(dir, recursive=true)) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) let src_path = dir + "/src.txt" let dst_path = dir + "/dst.txt" diff --git a/moon.mod b/moon.mod index 4be378b..2fde47d 100644 --- a/moon.mod +++ b/moon.mod @@ -12,4 +12,4 @@ repository = "https://github.com/moonbit-community/miniio" license = "Apache-2.0" -preferred_target = "wasm" \ No newline at end of file +preferred_target = "wasm" From b3a138d55cc950859625fcc30c4dfa09a75975ba Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 12 Jun 2026 14:55:00 +0800 Subject: [PATCH 04/10] feat: expose no-follow symlink kind --- fs.mbt | 37 +++++++++++++++++++++++-- fs_test.mbt | 27 ++++++++++++++++++ pkg.generated.mbti | 2 +- testdata/symlinks/child-link | 1 + testdata/symlinks/rmdir-target-link | 1 + testdata/symlinks/rmdir-target/kept.txt | 1 + testdata/symlinks/sample-link.txt | 1 + testdata/symlinks/sample.txt | 1 + types.mbt | 13 +++++++++ 9 files changed, 81 insertions(+), 3 deletions(-) create mode 120000 testdata/symlinks/child-link create mode 120000 testdata/symlinks/rmdir-target-link create mode 100644 testdata/symlinks/rmdir-target/kept.txt create mode 120000 testdata/symlinks/sample-link.txt create mode 100644 testdata/symlinks/sample.txt diff --git a/fs.mbt b/fs.mbt index bb62b9e..dc5d089 100644 --- a/fs.mbt +++ b/fs.mbt @@ -112,7 +112,34 @@ pub fn readdir( } ///| -pub fn kind(path : StringView) -> FileKind raise Errno { +fn split_parent_name(path : StringView) -> (String, String) { + let path = path.trim_end(chars="/") + match path.rev_split_once("/") { + Some(("", name)) => ("/", name.to_owned()) + Some((parent, name)) => (parent.to_owned(), name.to_owned()) + None => (".", path.to_owned()) + } +} + +///| +fn kind_no_follow(path : StringView) -> FileKind raise Errno { + let (parent, name) = split_parent_name(path) + for entry in wrap(() => @raw.readdir(parent)) { + if entry.name() == name { + return entry.kind().to_file_kind() + } + } + raise Noent +} + +///| +pub fn kind( + path : StringView, + follow_symlink? : Bool = true, +) -> FileKind raise Errno { + if !follow_symlink { + return kind_no_follow(path) + } try { ignore(readdir(path, include_hidden=true, sort=false)) Directory @@ -160,7 +187,13 @@ pub fn is_file(path : StringView) -> Bool raise Errno { ///| pub fn rmdir(path : StringView, recursive? : Bool = false) -> Unit raise Errno { if recursive { - for entry in readdir(path, include_hidden=true, sort=false) { + for + entry in readdir( + path, + include_hidden=true, + sort=false, + follow_symlink=false, + ) { let entry_path = path.to_owned() + "/" + entry remove(entry_path) catch { Notempty => rmdir(entry_path, recursive=true) diff --git a/fs_test.mbt b/fs_test.mbt index 4b82aec..f3cda8d 100644 --- a/fs_test.mbt +++ b/fs_test.mbt @@ -111,6 +111,21 @@ test "exists and kind classify file and directory" { assert_false(@miniio.is_file(dir)) } +///| +test "kind can report symlink itself" { + let dir = "testdata/symlinks" + let file_link = dir + "/sample-link.txt" + debug_inspect(@miniio.kind(file_link), content="Regular") + debug_inspect( + @miniio.kind(file_link, follow_symlink=false), + content="SymLink", + ) + + let dir_link = dir + "/child-link" + debug_inspect(@miniio.kind(dir_link), content="Directory") + debug_inspect(@miniio.kind(dir_link, follow_symlink=false), content="SymLink") +} + ///| test "read_file returns io data" { let dir = "miniio_test_dir_read_file" @@ -199,3 +214,15 @@ test "remove and recursive rmdir delete created paths" { _ => abort("expected removing a missing directory to fail") } } + +///| +test "recursive rmdir does not follow symlinked root" { + let target_dir = "testdata/symlinks/rmdir-target" + let link_path = "testdata/symlinks/rmdir-target-link" + try @miniio.rmdir(link_path, recursive=true) catch { + e => debug_inspect(e, content="Loop") + } noraise { + _ => abort("expected recursive rmdir on a symlink to fail") + } + inspect(@miniio.read_text_file(target_dir + "/kept.txt"), content="keep") +} diff --git a/pkg.generated.mbti b/pkg.generated.mbti index 61d6c84..ee0b90e 100644 --- a/pkg.generated.mbti +++ b/pkg.generated.mbti @@ -27,7 +27,7 @@ pub fn is_dir(StringView) -> Bool raise Errno pub fn is_file(StringView) -> Bool raise Errno -pub fn kind(StringView) -> FileKind raise Errno +pub fn kind(StringView, follow_symlink? : Bool) -> FileKind raise Errno pub fn mkdir(StringView) -> Unit raise Errno diff --git a/testdata/symlinks/child-link b/testdata/symlinks/child-link new file mode 120000 index 0000000..b0c6ee1 --- /dev/null +++ b/testdata/symlinks/child-link @@ -0,0 +1 @@ +child \ No newline at end of file diff --git a/testdata/symlinks/rmdir-target-link b/testdata/symlinks/rmdir-target-link new file mode 120000 index 0000000..16fb99d --- /dev/null +++ b/testdata/symlinks/rmdir-target-link @@ -0,0 +1 @@ +rmdir-target \ No newline at end of file diff --git a/testdata/symlinks/rmdir-target/kept.txt b/testdata/symlinks/rmdir-target/kept.txt new file mode 100644 index 0000000..c693f13 --- /dev/null +++ b/testdata/symlinks/rmdir-target/kept.txt @@ -0,0 +1 @@ +keep \ No newline at end of file diff --git a/testdata/symlinks/sample-link.txt b/testdata/symlinks/sample-link.txt new file mode 120000 index 0000000..bdbc095 --- /dev/null +++ b/testdata/symlinks/sample-link.txt @@ -0,0 +1 @@ +sample.txt \ No newline at end of file diff --git a/testdata/symlinks/sample.txt b/testdata/symlinks/sample.txt new file mode 100644 index 0000000..eed7e79 --- /dev/null +++ b/testdata/symlinks/sample.txt @@ -0,0 +1 @@ +sample \ No newline at end of file diff --git a/types.mbt b/types.mbt index c3e2f56..e0e3c48 100644 --- a/types.mbt +++ b/types.mbt @@ -39,6 +39,19 @@ fn SeekFrom::to_raw(self : SeekFrom) -> @raw.SeekFrom { } } +///| +fn @raw.EntryKind::to_file_kind(self : @raw.EntryKind) -> FileKind { + match self { + @raw.Unknown => Unknown + @raw.Regular => Regular + @raw.Directory => Directory + @raw.SymLink => SymLink + @raw.Socket => Socket + @raw.BlockDevice => BlockDevice + @raw.CharDevice => CharDevice + } +} + ///| pub(all) enum Mode { ReadOnly From 204c6e4ffd8616b607ac9109068198a74ff78813 Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 12 Jun 2026 14:59:11 +0800 Subject: [PATCH 05/10] fix: keep symlink support scoped to lookup flags --- fs.mbt | 29 +------------------------ fs_test.mbt | 27 ----------------------- pkg.generated.mbti | 2 +- testdata/symlinks/child-link | 1 - testdata/symlinks/rmdir-target-link | 1 - testdata/symlinks/rmdir-target/kept.txt | 1 - testdata/symlinks/sample-link.txt | 1 - testdata/symlinks/sample.txt | 1 - types.mbt | 13 ----------- 9 files changed, 2 insertions(+), 74 deletions(-) delete mode 120000 testdata/symlinks/child-link delete mode 120000 testdata/symlinks/rmdir-target-link delete mode 100644 testdata/symlinks/rmdir-target/kept.txt delete mode 120000 testdata/symlinks/sample-link.txt delete mode 100644 testdata/symlinks/sample.txt diff --git a/fs.mbt b/fs.mbt index dc5d089..a39f980 100644 --- a/fs.mbt +++ b/fs.mbt @@ -112,34 +112,7 @@ pub fn readdir( } ///| -fn split_parent_name(path : StringView) -> (String, String) { - let path = path.trim_end(chars="/") - match path.rev_split_once("/") { - Some(("", name)) => ("/", name.to_owned()) - Some((parent, name)) => (parent.to_owned(), name.to_owned()) - None => (".", path.to_owned()) - } -} - -///| -fn kind_no_follow(path : StringView) -> FileKind raise Errno { - let (parent, name) = split_parent_name(path) - for entry in wrap(() => @raw.readdir(parent)) { - if entry.name() == name { - return entry.kind().to_file_kind() - } - } - raise Noent -} - -///| -pub fn kind( - path : StringView, - follow_symlink? : Bool = true, -) -> FileKind raise Errno { - if !follow_symlink { - return kind_no_follow(path) - } +pub fn kind(path : StringView) -> FileKind raise Errno { try { ignore(readdir(path, include_hidden=true, sort=false)) Directory diff --git a/fs_test.mbt b/fs_test.mbt index f3cda8d..4b82aec 100644 --- a/fs_test.mbt +++ b/fs_test.mbt @@ -111,21 +111,6 @@ test "exists and kind classify file and directory" { assert_false(@miniio.is_file(dir)) } -///| -test "kind can report symlink itself" { - let dir = "testdata/symlinks" - let file_link = dir + "/sample-link.txt" - debug_inspect(@miniio.kind(file_link), content="Regular") - debug_inspect( - @miniio.kind(file_link, follow_symlink=false), - content="SymLink", - ) - - let dir_link = dir + "/child-link" - debug_inspect(@miniio.kind(dir_link), content="Directory") - debug_inspect(@miniio.kind(dir_link, follow_symlink=false), content="SymLink") -} - ///| test "read_file returns io data" { let dir = "miniio_test_dir_read_file" @@ -214,15 +199,3 @@ test "remove and recursive rmdir delete created paths" { _ => abort("expected removing a missing directory to fail") } } - -///| -test "recursive rmdir does not follow symlinked root" { - let target_dir = "testdata/symlinks/rmdir-target" - let link_path = "testdata/symlinks/rmdir-target-link" - try @miniio.rmdir(link_path, recursive=true) catch { - e => debug_inspect(e, content="Loop") - } noraise { - _ => abort("expected recursive rmdir on a symlink to fail") - } - inspect(@miniio.read_text_file(target_dir + "/kept.txt"), content="keep") -} diff --git a/pkg.generated.mbti b/pkg.generated.mbti index ee0b90e..61d6c84 100644 --- a/pkg.generated.mbti +++ b/pkg.generated.mbti @@ -27,7 +27,7 @@ pub fn is_dir(StringView) -> Bool raise Errno pub fn is_file(StringView) -> Bool raise Errno -pub fn kind(StringView, follow_symlink? : Bool) -> FileKind raise Errno +pub fn kind(StringView) -> FileKind raise Errno pub fn mkdir(StringView) -> Unit raise Errno diff --git a/testdata/symlinks/child-link b/testdata/symlinks/child-link deleted file mode 120000 index b0c6ee1..0000000 --- a/testdata/symlinks/child-link +++ /dev/null @@ -1 +0,0 @@ -child \ No newline at end of file diff --git a/testdata/symlinks/rmdir-target-link b/testdata/symlinks/rmdir-target-link deleted file mode 120000 index 16fb99d..0000000 --- a/testdata/symlinks/rmdir-target-link +++ /dev/null @@ -1 +0,0 @@ -rmdir-target \ No newline at end of file diff --git a/testdata/symlinks/rmdir-target/kept.txt b/testdata/symlinks/rmdir-target/kept.txt deleted file mode 100644 index c693f13..0000000 --- a/testdata/symlinks/rmdir-target/kept.txt +++ /dev/null @@ -1 +0,0 @@ -keep \ No newline at end of file diff --git a/testdata/symlinks/sample-link.txt b/testdata/symlinks/sample-link.txt deleted file mode 120000 index bdbc095..0000000 --- a/testdata/symlinks/sample-link.txt +++ /dev/null @@ -1 +0,0 @@ -sample.txt \ No newline at end of file diff --git a/testdata/symlinks/sample.txt b/testdata/symlinks/sample.txt deleted file mode 100644 index eed7e79..0000000 --- a/testdata/symlinks/sample.txt +++ /dev/null @@ -1 +0,0 @@ -sample \ No newline at end of file diff --git a/types.mbt b/types.mbt index e0e3c48..c3e2f56 100644 --- a/types.mbt +++ b/types.mbt @@ -39,19 +39,6 @@ fn SeekFrom::to_raw(self : SeekFrom) -> @raw.SeekFrom { } } -///| -fn @raw.EntryKind::to_file_kind(self : @raw.EntryKind) -> FileKind { - match self { - @raw.Unknown => Unknown - @raw.Regular => Regular - @raw.Directory => Directory - @raw.SymLink => SymLink - @raw.Socket => Socket - @raw.BlockDevice => BlockDevice - @raw.CharDevice => CharDevice - } -} - ///| pub(all) enum Mode { ReadOnly From e77bc3922c05a4f6b1f15fc5435eb360a817aaf2 Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 12 Jun 2026 15:07:41 +0800 Subject: [PATCH 06/10] fix: make create follow symlinks by default --- fs.mbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs.mbt b/fs.mbt index a39f980..c9541ff 100644 --- a/fs.mbt +++ b/fs.mbt @@ -61,7 +61,7 @@ pub fn open( ///| pub fn create(path : StringView) -> File raise Errno { - { raw: wrap(() => @raw.create(path)), read_buf: @io.ReaderBuffer::new() } + open(path, mode=WriteOnly, create_mode=CreateOrTruncate) } ///| From ff0b3fa482dccdf5929f376c021da3ee1e189eee Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 12 Jun 2026 15:26:38 +0800 Subject: [PATCH 07/10] test: add symlink regression coverage --- fs_test.mbt | 78 +++++++++++++++++++++++++++++++++++++ testdata/symlinks/dir-link | 1 + testdata/symlinks/file-link | 1 + 3 files changed, 80 insertions(+) create mode 120000 testdata/symlinks/dir-link create mode 120000 testdata/symlinks/file-link diff --git a/fs_test.mbt b/fs_test.mbt index 4b82aec..a8a8755 100644 --- a/fs_test.mbt +++ b/fs_test.mbt @@ -84,6 +84,62 @@ test "readdir sorts and filters hidden entries" { ) } +///| +test "open follows symlink by default and can reject symlink" { + let target = "miniio_test_symlink_target.txt" + let link = "testdata/symlinks/file-link" + @miniio.remove(target) catch { + Noent => () + e => raise e + } + defer (@miniio.remove(target) catch { _ => () }) + + @miniio.write_text_file(target, "target") + inspect(@miniio.read_text_file(link), content="target") + + let file = @miniio.create(link) + file.write_text("created through link") + file.close() + inspect(@miniio.read_text_file(target), content="created through link") + + try { + let file = @miniio.open(link, follow_symlink=false) + file.close() + abort("expected opening a symlink with follow_symlink=false to fail") + } catch { + Loop => () + e => raise e + } +} + +///| +test "readdir follows directory symlink by default and can reject symlink" { + let dir = "miniio_test_symlink_target_dir" + let link = "testdata/symlinks/dir-link" + @miniio.remove(dir) catch { + Noent => () + Notempty => @miniio.rmdir(dir, recursive=true) + e => raise e + } + @miniio.mkdir(dir) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) + + @miniio.write_text_file(dir + "/visible.txt", "visible") + @miniio.write_text_file(dir + "/.hidden", "hidden") + + debug_inspect( + @miniio.readdir(link, include_hidden=false, sort=true), + content="[\"visible.txt\"]", + ) + try { + ignore(@miniio.readdir(link, follow_symlink=false)) + abort("expected reading a symlink with follow_symlink=false to fail") + } catch { + Loop => () + e => raise e + } +} + ///| test "exists and kind classify file and directory" { let dir = "miniio_test_dir_kind" @@ -199,3 +255,25 @@ test "remove and recursive rmdir delete created paths" { _ => abort("expected removing a missing directory to fail") } } + +///| +test "recursive rmdir does not follow symlink root" { + let dir = "miniio_test_symlink_target_dir" + let link = "testdata/symlinks/dir-link" + @miniio.remove(dir) catch { + Noent => () + Notempty => @miniio.rmdir(dir, recursive=true) + e => raise e + } + @miniio.mkdir(dir) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) + + @miniio.write_text_file(dir + "/target.txt", "keep") + try @miniio.rmdir(link, recursive=true) catch { + Loop | Notdir => () + e => raise e + } noraise { + _ => abort("expected recursive rmdir on a symlink root to fail") + } + inspect(@miniio.read_text_file(dir + "/target.txt"), content="keep") +} diff --git a/testdata/symlinks/dir-link b/testdata/symlinks/dir-link new file mode 120000 index 0000000..b0ae6ca --- /dev/null +++ b/testdata/symlinks/dir-link @@ -0,0 +1 @@ +../../miniio_test_symlink_target_dir \ No newline at end of file diff --git a/testdata/symlinks/file-link b/testdata/symlinks/file-link new file mode 120000 index 0000000..c9b2edf --- /dev/null +++ b/testdata/symlinks/file-link @@ -0,0 +1 @@ +../../miniio_test_symlink_target.txt \ No newline at end of file From 7be5a5febfcc77b6db6088082a0a37686479d88f Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 12 Jun 2026 15:34:31 +0800 Subject: [PATCH 08/10] test: tolerate non-symlink checkouts --- fs_test.mbt | 70 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/fs_test.mbt b/fs_test.mbt index a8a8755..2a91883 100644 --- a/fs_test.mbt +++ b/fs_test.mbt @@ -95,20 +95,27 @@ test "open follows symlink by default and can reject symlink" { defer (@miniio.remove(target) catch { _ => () }) @miniio.write_text_file(target, "target") - inspect(@miniio.read_text_file(link), content="target") - - let file = @miniio.create(link) - file.write_text("created through link") - file.close() - inspect(@miniio.read_text_file(target), content="created through link") - - try { - let file = @miniio.open(link, follow_symlink=false) - file.close() - abort("expected opening a symlink with follow_symlink=false to fail") - } catch { - Loop => () + try @miniio.read_text_file(link) catch { + Noent => () e => raise e + } noraise { + content => + // Windows checkouts may materialize symlink fixtures as plain text files. + if content == "target" { + let file = @miniio.create(link) + file.write_text("created through link") + file.close() + inspect(@miniio.read_text_file(target), content="created through link") + + try { + let file = @miniio.open(link, follow_symlink=false) + file.close() + abort("expected opening a symlink with follow_symlink=false to fail") + } catch { + Loop => () + e => raise e + } + } } } @@ -127,16 +134,20 @@ test "readdir follows directory symlink by default and can reject symlink" { @miniio.write_text_file(dir + "/visible.txt", "visible") @miniio.write_text_file(dir + "/.hidden", "hidden") - debug_inspect( - @miniio.readdir(link, include_hidden=false, sort=true), - content="[\"visible.txt\"]", - ) - try { - ignore(@miniio.readdir(link, follow_symlink=false)) - abort("expected reading a symlink with follow_symlink=false to fail") - } catch { - Loop => () + try @miniio.readdir(link, include_hidden=false, sort=true) catch { + Noent | Notdir => () e => raise e + } noraise { + entries => { + debug_inspect(entries, content="[\"visible.txt\"]") + try { + ignore(@miniio.readdir(link, follow_symlink=false)) + abort("expected reading a symlink with follow_symlink=false to fail") + } catch { + Loop => () + e => raise e + } + } } } @@ -269,11 +280,18 @@ test "recursive rmdir does not follow symlink root" { defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) @miniio.write_text_file(dir + "/target.txt", "keep") - try @miniio.rmdir(link, recursive=true) catch { - Loop | Notdir => () + try @miniio.readdir(link, include_hidden=false, sort=true) catch { + Noent | Notdir => () e => raise e } noraise { - _ => abort("expected recursive rmdir on a symlink root to fail") + _ => { + try @miniio.rmdir(link, recursive=true) catch { + Loop | Notdir => () + e => raise e + } noraise { + _ => abort("expected recursive rmdir on a symlink root to fail") + } + inspect(@miniio.read_text_file(dir + "/target.txt"), content="keep") + } } - inspect(@miniio.read_text_file(dir + "/target.txt"), content="keep") } From e0e49c182107fb2c4f764dcd3f79c3fbe68a194d Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 12 Jun 2026 15:44:23 +0800 Subject: [PATCH 09/10] test: avoid symlink test platform assumptions --- fs_test.mbt | 13 +++++-------- testdata/symlinks/rmdir-dir-link | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) create mode 120000 testdata/symlinks/rmdir-dir-link diff --git a/fs_test.mbt b/fs_test.mbt index 2a91883..5d23434 100644 --- a/fs_test.mbt +++ b/fs_test.mbt @@ -112,8 +112,7 @@ test "open follows symlink by default and can reject symlink" { file.close() abort("expected opening a symlink with follow_symlink=false to fail") } catch { - Loop => () - e => raise e + _ => () } } } @@ -144,8 +143,7 @@ test "readdir follows directory symlink by default and can reject symlink" { ignore(@miniio.readdir(link, follow_symlink=false)) abort("expected reading a symlink with follow_symlink=false to fail") } catch { - Loop => () - e => raise e + _ => () } } } @@ -269,8 +267,8 @@ test "remove and recursive rmdir delete created paths" { ///| test "recursive rmdir does not follow symlink root" { - let dir = "miniio_test_symlink_target_dir" - let link = "testdata/symlinks/dir-link" + let dir = "miniio_test_symlink_rmdir_target_dir" + let link = "testdata/symlinks/rmdir-dir-link" @miniio.remove(dir) catch { Noent => () Notempty => @miniio.rmdir(dir, recursive=true) @@ -286,8 +284,7 @@ test "recursive rmdir does not follow symlink root" { } noraise { _ => { try @miniio.rmdir(link, recursive=true) catch { - Loop | Notdir => () - e => raise e + _ => () } noraise { _ => abort("expected recursive rmdir on a symlink root to fail") } diff --git a/testdata/symlinks/rmdir-dir-link b/testdata/symlinks/rmdir-dir-link new file mode 120000 index 0000000..45bffdf --- /dev/null +++ b/testdata/symlinks/rmdir-dir-link @@ -0,0 +1 @@ +../../miniio_test_symlink_rmdir_target_dir \ No newline at end of file From e829c2755071ecf235e6609d3553b6dba4bf2d13 Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 12 Jun 2026 15:51:56 +0800 Subject: [PATCH 10/10] test: skip host symlink tests on Windows CI --- fs_test.mbt | 160 +++++++++++++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 72 deletions(-) diff --git a/fs_test.mbt b/fs_test.mbt index 5d23434..33c8c47 100644 --- a/fs_test.mbt +++ b/fs_test.mbt @@ -12,6 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +///| +fn host_symlink_tests_enabled() -> Bool raise Errno { + @miniio.get_env_var("RUNNER_OS") != Some("Windows") +} + ///| test "write_file supports create append and truncate" { let dir = "miniio_test_dir_write_file" @@ -86,64 +91,73 @@ test "readdir sorts and filters hidden entries" { ///| test "open follows symlink by default and can reject symlink" { - let target = "miniio_test_symlink_target.txt" - let link = "testdata/symlinks/file-link" - @miniio.remove(target) catch { - Noent => () - e => raise e - } - defer (@miniio.remove(target) catch { _ => () }) - - @miniio.write_text_file(target, "target") - try @miniio.read_text_file(link) catch { - Noent => () - e => raise e - } noraise { - content => - // Windows checkouts may materialize symlink fixtures as plain text files. - if content == "target" { - let file = @miniio.create(link) - file.write_text("created through link") - file.close() - inspect(@miniio.read_text_file(target), content="created through link") - - try { - let file = @miniio.open(link, follow_symlink=false) + if host_symlink_tests_enabled() { + let target = "miniio_test_symlink_target.txt" + let link = "testdata/symlinks/file-link" + @miniio.remove(target) catch { + Noent => () + e => raise e + } + defer (@miniio.remove(target) catch { _ => () }) + + @miniio.write_text_file(target, "target") + try @miniio.read_text_file(link) catch { + Noent => () + e => raise e + } noraise { + content => + // Windows checkouts may materialize symlink fixtures as plain text files. + if content == "target" { + let file = @miniio.create(link) + file.write_text("created through link") file.close() - abort("expected opening a symlink with follow_symlink=false to fail") - } catch { - _ => () + inspect( + @miniio.read_text_file(target), + content="created through link", + ) + + try { + let file = @miniio.open(link, follow_symlink=false) + file.close() + abort( + "expected opening a symlink with follow_symlink=false to fail", + ) + } catch { + _ => () + } } - } + } } } ///| test "readdir follows directory symlink by default and can reject symlink" { - let dir = "miniio_test_symlink_target_dir" - let link = "testdata/symlinks/dir-link" - @miniio.remove(dir) catch { - Noent => () - Notempty => @miniio.rmdir(dir, recursive=true) - e => raise e - } - @miniio.mkdir(dir) - defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) - - @miniio.write_text_file(dir + "/visible.txt", "visible") - @miniio.write_text_file(dir + "/.hidden", "hidden") - - try @miniio.readdir(link, include_hidden=false, sort=true) catch { - Noent | Notdir => () - e => raise e - } noraise { - entries => { - debug_inspect(entries, content="[\"visible.txt\"]") - try { - ignore(@miniio.readdir(link, follow_symlink=false)) - abort("expected reading a symlink with follow_symlink=false to fail") - } catch { - _ => () + if host_symlink_tests_enabled() { + let dir = "miniio_test_symlink_target_dir" + let link = "testdata/symlinks/dir-link" + @miniio.remove(dir) catch { + Noent => () + Notempty => @miniio.rmdir(dir, recursive=true) + e => raise e + } + @miniio.mkdir(dir) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) + + @miniio.write_text_file(dir + "/visible.txt", "visible") + @miniio.write_text_file(dir + "/.hidden", "hidden") + + try @miniio.readdir(link, include_hidden=false, sort=true) catch { + Noent | Notdir => () + e => raise e + } noraise { + entries => { + debug_inspect(entries, content="[\"visible.txt\"]") + try { + ignore(@miniio.readdir(link, follow_symlink=false)) + abort("expected reading a symlink with follow_symlink=false to fail") + } catch { + _ => () + } } } } @@ -267,28 +281,30 @@ test "remove and recursive rmdir delete created paths" { ///| test "recursive rmdir does not follow symlink root" { - let dir = "miniio_test_symlink_rmdir_target_dir" - let link = "testdata/symlinks/rmdir-dir-link" - @miniio.remove(dir) catch { - Noent => () - Notempty => @miniio.rmdir(dir, recursive=true) - e => raise e - } - @miniio.mkdir(dir) - defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) - - @miniio.write_text_file(dir + "/target.txt", "keep") - try @miniio.readdir(link, include_hidden=false, sort=true) catch { - Noent | Notdir => () - e => raise e - } noraise { - _ => { - try @miniio.rmdir(link, recursive=true) catch { - _ => () - } noraise { - _ => abort("expected recursive rmdir on a symlink root to fail") + if host_symlink_tests_enabled() { + let dir = "miniio_test_symlink_rmdir_target_dir" + let link = "testdata/symlinks/rmdir-dir-link" + @miniio.remove(dir) catch { + Noent => () + Notempty => @miniio.rmdir(dir, recursive=true) + e => raise e + } + @miniio.mkdir(dir) + defer (@miniio.rmdir(dir, recursive=true) catch { _ => () }) + + @miniio.write_text_file(dir + "/target.txt", "keep") + try @miniio.readdir(link, include_hidden=false, sort=true) catch { + Noent | Notdir => () + e => raise e + } noraise { + _ => { + try @miniio.rmdir(link, recursive=true) catch { + _ => () + } noraise { + _ => abort("expected recursive rmdir on a symlink root to fail") + } + inspect(@miniio.read_text_file(dir + "/target.txt"), content="keep") } - inspect(@miniio.read_text_file(dir + "/target.txt"), content="keep") } } }