-module(fio@recursive). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/fio/recursive.gleam"). -export([list_recursive/1, copy_directory/2]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -file("src/fio/recursive.gleam", 45). -spec do_list_recursive( binary(), binary(), gleam@set:set(binary()), list(binary()) ) -> {ok, list(binary())} | {error, fio@error:fio_error()}. do_list_recursive(Root, Current_rel, Visited, Acc) -> Current_dir = case Current_rel of <<""/utf8>> -> Root; Rel -> fio@path:join(Root, Rel) end, gleam@result:'try'( fio@internal@io:list_directory(Current_dir), fun(Items) -> gleam@list:try_fold( Items, Acc, fun(Inner_acc, Item) -> Item_rel = case Current_rel of <<""/utf8>> -> Item; Rel@1 -> fio@path:join(Rel@1, Item) end, Full_path = fio@path:join(Root, Item_rel), gleam@result:'try'( fio@internal@io:is_directory(Full_path), fun(Is_dir) -> case Is_dir of false -> {ok, [Item_rel | Inner_acc]}; true -> gleam@result:'try'( fio@internal@io:file_info(Full_path), fun(Info) -> Key = case erlang:element(5, Info) of 0 -> Full_path; _ -> <<<<<<"dev:"/utf8, (erlang:integer_to_binary( erlang:element( 8, Info ) ))/binary>>/binary, ";ino:"/utf8>>/binary, (erlang:integer_to_binary( erlang:element( 5, Info ) ))/binary>> end, case gleam@set:contains( Visited, Key ) of true -> {ok, [Item_rel | Inner_acc]}; false -> New_visited = gleam@set:insert( Visited, Key ), do_list_recursive( Root, Item_rel, New_visited, [Item_rel | Inner_acc] ) end end ) end end ) end ) end ). -file("src/fio/recursive.gleam", 17). ?DOC( " Recursively list files and directories (paths relative to `path`).\n" "\n" " Uses a flat string accumulator for O(n) traversal.\n" "\n" " **Symlink loop safety**: before descending into any directory, its real\n" " `(dev, inode)` pair (obtained via `stat`, which follows symlinks) is\n" " checked against the `visited` set. If already seen, the entry is listed\n" " but not descended into — breaking any A→B→A or deeper circular chains.\n" ). -spec list_recursive(binary()) -> {ok, list(binary())} | {error, fio@error:fio_error()}. list_recursive(Path) -> gleam@result:'try'( fio@internal@io:is_directory(Path), fun(Is_dir) -> case Is_dir of true -> gleam@result:'try'( fio@internal@io:file_info(Path), fun(Root_info) -> Root_key = case erlang:element(5, Root_info) of 0 -> Path; _ -> <<<<<<"dev:"/utf8, (erlang:integer_to_binary( erlang:element(8, Root_info) ))/binary>>/binary, ";ino:"/utf8>>/binary, (erlang:integer_to_binary( erlang:element(5, Root_info) ))/binary>> end, Visited = gleam@set:from_list([Root_key]), gleam@result:'try'( do_list_recursive( Path, <<""/utf8>>, Visited, [] ), fun(Acc) -> {ok, lists:reverse(Acc)} end ) end ); false -> {error, enotdir} end end ). -file("src/fio/recursive.gleam", 123). -spec do_copy_directory(binary(), binary(), gleam@set:set(binary())) -> {ok, nil} | {error, fio@error:fio_error()}. do_copy_directory(Src, Dest, Visited) -> gleam@result:'try'( fio@internal@io:create_directory_all(Dest), fun(_) -> gleam@result:'try'( fio@internal@io:list_directory(Src), fun(Items) -> gleam@list:try_each( Items, fun(Item) -> Src_path = fio@path:join(Src, Item), Dest_path = fio@path:join(Dest, Item), gleam@result:'try'( fio@internal@io:is_directory(Src_path), fun(Is_dir_item) -> case Is_dir_item of false -> fio@internal@io:copy_file( Src_path, Dest_path ); true -> gleam@result:'try'( fio@internal@io:file_info( Src_path ), fun(Info) -> Key = case erlang:element( 5, Info ) of 0 -> Src_path; _ -> <<<<<<"dev:"/utf8, (erlang:integer_to_binary( erlang:element( 8, Info ) ))/binary>>/binary, ";ino:"/utf8>>/binary, (erlang:integer_to_binary( erlang:element( 5, Info ) ))/binary>> end, case gleam@set:contains( Visited, Key ) of true -> {ok, nil}; false -> do_copy_directory( Src_path, Dest_path, gleam@set:insert( Visited, Key ) ) end end ) end end ) end ) end ) end ). -file("src/fio/recursive.gleam", 103). ?DOC( " Recursively copy `src` directory into `dest` (creates parents).\n" "\n" " **Symlink loop safety**: directory symlinks whose resolved inode has\n" " already been visited are skipped silently rather than followed forever.\n" ). -spec copy_directory(binary(), binary()) -> {ok, nil} | {error, fio@error:fio_error()}. copy_directory(Src, Dest) -> gleam@result:'try'( fio@internal@io:is_directory(Src), fun(Is_dir) -> case Is_dir of false -> {error, enotdir}; true -> gleam@result:'try'( fio@internal@io:file_info(Src), fun(Root_info) -> Root_key = case erlang:element(5, Root_info) of 0 -> Src; _ -> <<<<<<"dev:"/utf8, (erlang:integer_to_binary( erlang:element(8, Root_info) ))/binary>>/binary, ";ino:"/utf8>>/binary, (erlang:integer_to_binary( erlang:element(5, Root_info) ))/binary>> end, Visited = gleam@set:from_list([Root_key]), do_copy_directory(Src, Dest, Visited) end ) end end ).