%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2000-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
-module(prim_file).
-moduledoc false.

-export([on_load/0]).

-export([open/2, close/1,
         sync/1, datasync/1, truncate/1, advise/4, allocate/3,
         read_line/1, read/2, write/2, position/2,
         pread/2, pread/3, pwrite/2, pwrite/3]).

%% OTP internal.

-export([file_desc_to_ref/2]).

-export([ipread_s32bu_p32bu/3, sendfile/8, internal_get_nif_resource/1,
         altname/1, get_handle/1]).

-export([read_file/1, write_file/2]).

-export([read_link/1, read_link_all/1,
         read_link_info/1, read_link_info/2,
         read_file_info/1, read_file_info/2,
         read_handle_info/1, read_handle_info/2,
         write_file_info/2, write_file_info/3]).

-export([list_dir/1, list_dir_all/1]).

-export([get_cwd/0, get_cwd/1, set_cwd/1,
         delete/1, rename/2,
         make_dir/1, del_dir/1,
         make_link/2, make_symlink/2]).

-define(MIN_READLINE_SIZE, 256).
-define(LARGEFILESIZE, (1 bsl 63)).

-export([copy/3, start/0]).

-include("file_int.hrl").

-type prim_file_ref() :: term().

%%% BIFs

-export([internal_name2native/1,
         internal_native2name/1,
         internal_normalize_utf8/1,
         is_translatable/1]).

-nifs([open_nif/2, close_nif/1, read_nif/2, write_nif/2, pread_nif/3,
       pwrite_nif/3, seek_nif/3, sync_nif/2, truncate_nif/1, allocate_nif/3,
       advise_nif/4, read_handle_info_nif/1,
       make_hard_link_nif/2, make_soft_link_nif/2, rename_nif/2,
       read_info_nif/2, set_permissions_nif/2, set_owner_nif/3, set_time_nif/4,
       read_link_nif/1, list_dir_nif/1, make_dir_nif/1, del_file_nif/1,
       del_dir_nif/1, get_device_cwd_nif/1, set_cwd_nif/1, get_cwd_nif/0,
       ipread_s32bu_p32bu_nif/3, read_file_nif/1,
       get_handle_nif/1, delayed_close_nif/1, altname_nif/1,
       file_desc_to_ref_nif/1]).

-type prim_file_name() :: string() | unicode:unicode_binary().
-type prim_file_name_error() :: 'error' | 'ignore' | 'warning'.

-spec internal_name2native(prim_file_name()) -> binary().

internal_name2native(_) ->
    erlang:nif_error(undefined).

-spec internal_native2name(binary()) ->
        prim_file_name() | {'error',prim_file_name_error()}.

internal_native2name(_) ->
    erlang:nif_error(undefined).

-spec internal_normalize_utf8(unicode:unicode_binary()) -> string().

internal_normalize_utf8(_) ->
    erlang:nif_error(undefined).

-spec is_translatable(prim_file_name()) -> boolean().

is_translatable(_) ->
    erlang:nif_error(undefined).

%% This is a janitor process used to close files whose controlling process has
%% died. The emulator will be torn down if this is killed.
start() ->
    helper_loop().

helper_loop() ->
    receive
        {close, FRef} when is_reference(FRef) -> delayed_close_nif(FRef);
        _ -> ok
    end,
    helper_loop().

on_load() ->
    %% This is spawned as a system process to prevent init:restart/0 from
    %% killing it.
    Pid = erts_internal:spawn_system_process(?MODULE, start, []),
    ok = erlang:load_nif(atom_to_list(?MODULE), Pid).

%% Returns {error, Reason} | {ok, BytesCopied}
copy(#file_descriptor{module = ?MODULE} = Source,
     #file_descriptor{module = ?MODULE} = Dest,
     Length)
  when is_integer(Length), Length >= 0;
       is_atom(Length) ->
    %% XXX Should be moved down to the driver for optimization.
    file:copy_opened(Source, Dest, Length).

open(Name, Modes) ->
    %% The try/catch pattern seen here is used throughout the file to adhere to
    %% the public file interface, which has leaked through for ages because of
    %% "raw files."
    try open_nif(encode_path(Name), Modes) of
        {ok, Ref} -> {ok, make_fd(Modes, Ref)};
        {error, Reason} -> {error, Reason}
    catch
        error:badarg -> {error, badarg}
    end.

file_desc_to_ref(FileDescriptorId, Modes) ->
    try file_desc_to_ref_nif(FileDescriptorId) of
        {ok, Ref} -> {ok, make_fd(Modes, Ref)};
        {error, Reason} -> {error, Reason}
    catch
        error:badarg -> {error, badarg}
    end.

make_fd(Modes, FRef) ->
    #file_descriptor{module = ?MODULE, data = build_fd_data(Modes, FRef) }.

close(Fd) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        close_nif(FRef)
    catch
        error:badarg -> {error, badarg}
    end.

read(Fd, Size) ->
    try
        #{ handle := FRef,
           r_ahead_size := RASz,
           r_buffer := RBuf } = get_fd_data_for_read(Fd),
        read_1(FRef, RBuf, prim_buffer:size(RBuf), RASz, Size)
    catch
        throw:Err -> Err;
        error:badarg -> {error, badarg}
    end.

-spec read_1(FRef, RBuf, RBufSz, RASz, RSz) -> Result when
      FRef :: prim_file_ref(),
      RBuf :: term(),
      RBufSz :: non_neg_integer(),
      RASz :: non_neg_integer(),
      RSz :: non_neg_integer(),
      Result :: eof | {ok, binary()} | {error, Reason :: atom()}.
read_1(_FRef, RBuf, RBufSz, _RASz, RSz) when RBufSz >= RSz ->
    {ok, prim_buffer:read(RBuf, RSz)};
read_1(FRef, RBuf, RBufSz, RASz, RSz) when RBufSz > 0 ->
    Buffered = prim_buffer:read(RBuf, RBufSz),
    case read_1(FRef, RBuf, 0, RASz, RSz - RBufSz) of
        {ok, Data} ->
            {ok, <<Buffered/binary, Data/binary>>};
        eof ->
            {ok, Buffered};
        {error, Reason} ->
            {error, Reason}
    end;
read_1(FRef, RBuf, RBufSz, RASz, RSz) when RBufSz =:= 0 ->
    case read_nif(FRef, RASz + RSz) of
        {ok, Data} when byte_size(Data) > RSz ->
            {First, Rest} = split_binary(Data, RSz),
            prim_buffer:write(RBuf, [Rest]),
            {ok, First};
        {ok, Data} when byte_size(Data) =< RSz ->
            {ok, Data};
        eof ->
            eof;
        {error, Reason} ->
            {error, Reason}
    end.

read_line(Fd) ->
    try
        #{ handle := FRef,
           r_ahead_size := RASz,
           r_buffer := RBuf } = get_fd_data_for_read(Fd),
        SearchResult = prim_buffer:find_byte_index(RBuf, $\n),
        LineSize = max(?MIN_READLINE_SIZE, RASz),
        read_line_1(FRef, RBuf, SearchResult, LineSize)
    catch
        throw:Err -> Err;
        error:badarg -> {error, badarg}
    end.

-spec read_line_1(FRef, RBuf, SearchResult, LineSize) -> Result when
      FRef :: prim_file_ref(),
      RBuf :: term(),
      SearchResult :: not_found | {ok, non_neg_integer()},
      LineSize :: non_neg_integer(),
      Result :: eof | {ok, binary()} | {error, Reason :: atom()}.
read_line_1(FRef, RBuf, not_found, LineSize) ->
    case read_nif(FRef, LineSize) of
        {ok, Data} ->
            prim_buffer:write(RBuf, [Data]),
            SearchResult = prim_buffer:find_byte_index(RBuf, $\n),
            read_line_1(FRef, RBuf, SearchResult, LineSize);
        eof ->
            case prim_buffer:size(RBuf) of
                Size when Size > 0 -> {ok, prim_buffer:read(RBuf, Size)};
                Size when Size =:= 0 -> eof
            end;
        {error, Reason} ->
            {error, Reason}
    end;
read_line_1(_FRef, RBuf, {ok, LFIndex}, _LineSize) ->
    %% Translate CRLF into just LF, completely ignoring which encoding is used.
    CRIndex = (LFIndex - 1),
    case prim_buffer:read(RBuf, LFIndex + 1) of
        <<Line:CRIndex/binary, "\r\n">> -> {ok, <<Line/binary, "\n">>};
        Line -> {ok, Line}
    end.

write(Fd, IOData) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        reset_write_position(Fd),
        write_1(FRef, erlang:iolist_to_iovec(IOData))
    catch
        error:badarg -> {error, badarg}
    end.
write_1(FRef, IOVec) ->
    case write_nif(FRef, IOVec) of
        {continue, Remainder} ->
            write_1(FRef, Remainder);
        ok ->
            ok;
        {error, Reason} ->
            {error, Reason}
    end.

truncate(Fd) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        reset_write_position(Fd),
        truncate_nif(FRef)
    catch
        error:badarg -> {error, badarg}
    end.

advise(Fd, Offset, Length, Advise) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        advise_nif(FRef, Offset, Length, Advise)
    catch
        error:badarg -> {error, badarg}
    end.

allocate(Fd, Offset, Length) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        allocate_nif(FRef, Offset, Length)
    catch
        error:badarg -> {error, badarg}
    end.

sync(Fd) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        sync_nif(FRef, 0)
    catch
        error:badarg -> {error, badarg}
    end.

datasync(Fd) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        sync_nif(FRef, 1)
    catch
        error:badarg -> {error, badarg}
    end.

position(Fd, {cur, Offset}) ->
    try
        %% Adjust our current position according to how much we've read ahead.
        case get_fd_data(Fd) of
            #{ r_buffer := RBuf } -> position_1(Fd, cur, Offset - prim_buffer:size(RBuf));
            #{} -> position_1(Fd, cur, Offset)
        end
    catch
        error:badarg -> {error, badarg}
    end;
position(Fd, {Mark, Offset}) ->
    try
        position_1(Fd, Mark, Offset)
    catch
        error:badarg -> {error, badarg}
    end;
position(Fd, cur) -> position(Fd, {cur, 0});
position(Fd, bof) -> position(Fd, {bof, 0});
position(Fd, eof) -> position(Fd, {eof, 0});
position(Fd, Offset) -> position(Fd, {bof, Offset}).

position_1(Fd, Mark, Offset) ->
    case get_fd_data(Fd) of
        #{ handle := FRef, r_buffer := RBuf } ->
            prim_buffer:wipe(RBuf),
            seek_nif(FRef, Mark, Offset);
        #{ handle := FRef } ->
            seek_nif(FRef, Mark, Offset)
    end.

pread(Fd, Offset, Size) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        pread_nif(FRef, Offset, Size)
    catch
        error:badarg -> {error, badarg}
    end.

pread(Fd, LocNums) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        pread_list(FRef, LocNums, [])
    catch
        error:badarg -> {error, badarg}
    end.

-spec pread_list(FRef, LocNums, ResultList) -> Result when
      FRef :: prim_file_ref(),
      LocNums :: list({Offset :: non_neg_integer(),
                       Size :: non_neg_integer()}),
      ResultList :: list(eof | binary()),
      Result :: {ok, ResultList} | {error, Reason :: atom()}.
pread_list(_FRef, [], ResultList) ->
    {ok, reverse_list(ResultList)};
pread_list(FRef, [{Offset, Size} | Rest], ResultList) ->
    case pread_nif(FRef, Offset, Size) of
        {ok, Data} ->
            pread_list(FRef, Rest, [Data | ResultList]);
        eof ->
            pread_list(FRef, Rest, [eof | ResultList]);
        {error, Reason} ->
            {error, Reason}
    end.

pwrite(Fd, Offset, IOData) ->
    try
        IOVec = erlang:iolist_to_iovec(IOData),
        case get_fd_data(Fd) of
            #{ handle := FRef, r_buffer := RBuf } ->
                prim_buffer:wipe(RBuf),
                pwrite_plain(FRef, Offset, IOVec);
            #{ handle := FRef } ->
                pwrite_plain(FRef, Offset, IOVec)
        end
    catch
        error:badarg -> {error, badarg}
    end.
pwrite_plain(FRef, Offset, IOVec) ->
    case pwrite_nif(FRef, Offset, IOVec) of
        {continue, BytesWritten, Remainder} ->
            pwrite_plain(FRef, Offset + BytesWritten, Remainder);
        ok ->
            ok;
        {error, Reason} ->
            {error, Reason}
    end.

pwrite(Fd, LocBytes) ->
    try
        case get_fd_data(Fd) of
            #{ handle := FRef, r_buffer := RBuf } ->
                prim_buffer:wipe(RBuf),
                pwrite_list(FRef, LocBytes, 0);
            #{ handle := FRef } ->
                pwrite_list(FRef, LocBytes, 0)
        end
    catch
        error:badarg -> {error, badarg}
    end.

-spec pwrite_list(FRef, LocBytes, Successes) -> Result when
      FRef :: prim_file_ref(),
      LocBytes :: list({Offset :: non_neg_integer(),
                        IOData :: iodata()}),
      Successes :: non_neg_integer(),
      Result :: ok | {error, {Successes, Reason :: atom()}}.
pwrite_list(_FRef, [], _Successes) ->
    ok;
pwrite_list(FRef, [{Offset, IOData} | Rest], Successes) ->
    case pwrite_plain(FRef, Offset, erlang:iolist_to_iovec(IOData)) of
        {error, Reason} -> {error, {Successes, Reason}};
        ok -> pwrite_list(FRef, Rest, Successes + 1)
    end.

sendfile(Fd, Socket, Offset, Bytes, _ChunkSize, [], [], _Flags) ->
    %% There's a very nasty race in here; if we die just prior to duplicating
    %% the handle down in the sendfile call, it might get reused by something
    %% entirely different and we'll leak unknown data to the socket until it
    %% dies soon after.
    %%
    %% This bug was inherited from the old driver, except it was vulnerable to
    %% the bug at any point and not just during setup.
    %%
    %% We'll have to live with this until we have a way to unambiguously
    %% transfer things between drivers or NIFs. Current ideas all fall afoul
    %% of the Two Generals problem.
    try
        advise(Fd, Offset, Bytes, sequential),
        prim_inet:sendfile(Socket, get_handle(Fd), Offset, Bytes)
    catch
        error:badarg -> {error, badarg}
    end;
sendfile(_Fd, _Socket, _Offset, _Bytes, _ChunkSize, _Headers, _Trailers, _Flags) ->
    {error, enotsup}.

internal_get_nif_resource(Fd) ->
    #{ handle := FRef } = get_fd_data(Fd),
    FRef.

%% Undocumented internal function that reads a data block with indirection.
%%
%% This is only used once in DETS and can easily be emulated with pread/2, but
%% it's pretty performance-sensitive so we've implemented it down in the NIF to
%% avoid excessive rescheduling.
-spec ipread_s32bu_p32bu(Fd, Offset, MaxSize) -> Result when
      Fd :: #file_descriptor{},
      Offset :: non_neg_integer(),
      MaxSize :: non_neg_integer() | infinity,
      Result :: {ok, Size :: non_neg_integer(),
                     Pointer :: non_neg_integer(),
                     Data :: iodata() | eof} |
                eof |
                {error, Reason :: atom()}.
ipread_s32bu_p32bu(Fd, Offset, Infinity) when is_atom(Infinity) ->
    ipread_s32bu_p32bu(Fd, Offset, (1 bsl 31) - 1);
ipread_s32bu_p32bu(Fd, Offset, MaxSize)
        when is_integer(Offset), is_integer(MaxSize) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        ipread_s32bu_p32bu_nif(FRef, Offset, MaxSize)
    catch
        error:badarg -> {error, badarg}
    end;
ipread_s32bu_p32bu(_Fd, _Offset, _MaxSize) ->
    {error, badarg}.
ipread_s32bu_p32bu_nif(_FRef, _Offset, _MaxSize) ->
    erlang:nif_error(undef).

%% Returns the binary representation of the underlying handle, for use in
%% tricky operations like sendfile/8.
-spec get_handle(Fd) -> Result when
      Fd :: #file_descriptor{},
      Result :: binary() | {error, Reason :: atom()}.
get_handle(Fd) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        get_handle_nif(FRef)
    catch
        error:badarg -> {error, badarg}
    end.

%% Resets the write head to the position the user believes we're at, which may
%% not be the same as the real one when read caching is in effect.
reset_write_position(Fd) ->
    case Fd#file_descriptor.data of
        #{ r_buffer := RBuf } ->
            case prim_buffer:size(RBuf) of
                Size when Size > 0 -> position(Fd, cur);
                Size when Size =:= 0 -> ok
            end;
        _ -> ok
    end.

get_fd_data(#file_descriptor{ data = Data }) ->
    #{ owner := Owner } = Data,
    case self() of
        Owner -> Data;
        _ -> error(not_on_controlling_process)
    end.

get_fd_data_for_read(Fd) ->
    case get_fd_data(Fd) of
        #{ r_buffer := _ } = Data -> Data;
        _ -> throw({error, ebadf})
    end.

build_fd_data(Modes, FRef) -> build_fd_data(Modes, FRef, self(), 0, default).

build_fd_data([], FRef, Owner, _RASz, write) ->
    #{ handle => FRef, owner => Owner };
build_fd_data([], FRef, Owner, RASz, Mode) when Mode =:= read; Mode =:= default ->
    #{ handle => FRef, owner => Owner, r_ahead_size => RASz, r_buffer => prim_buffer:new() };
build_fd_data([read_ahead | Modes], FRef, Owner, _, Mode) ->
    build_fd_data(Modes, FRef, Owner, 64 bsl 10, Mode);
build_fd_data([{read_ahead, RASz} | Modes], FRef, Owner, _, Mode) ->
    build_fd_data(Modes, FRef, Owner, RASz, Mode);
build_fd_data([write | Modes], FRef, Owner, RASz, default) ->
    build_fd_data(Modes, FRef, Owner, RASz, write);
build_fd_data([read | Modes], FRef, Owner, RASz, _Mode) ->
    build_fd_data(Modes, FRef, Owner, RASz, read);
build_fd_data([_Ignored | Modes], FRef, Owner, RASz, Mode) ->
    build_fd_data(Modes, FRef, Owner, RASz, Mode).

open_nif(_Name, _Modes) ->
    erlang:nif_error(undef).
file_desc_to_ref_nif(_FD) ->
    erlang:nif_error(undef).
close_nif(_FileRef) ->
    erlang:nif_error(undef).
read_nif(_FileRef, _Size) ->
    erlang:nif_error(undef).
write_nif(_FileRef, _IOVec) ->
    erlang:nif_error(undef).
pread_nif(_FileRef, _Offset, _Size) ->
    erlang:nif_error(undef).
pwrite_nif(_FileRef, _Offset, _IOVec) ->
    erlang:nif_error(undef).
seek_nif(_FileRef, _Mark, _Offset) ->
    erlang:nif_error(undef).
sync_nif(_FileRef, _DataOnly) ->
    erlang:nif_error(undef).
advise_nif(_FileRef, _Offset, _Length, _Advise) ->
    erlang:nif_error(undef).
allocate_nif(_FileRef, _Offset, _Length) ->
    erlang:nif_error(undef).
truncate_nif(_FileRef) ->
    erlang:nif_error(undef).
get_handle_nif(_FileRef) ->
    erlang:nif_error(undef).
delayed_close_nif(_FileRef) ->
    erlang:nif_error(undef).
read_handle_info_nif(_FileRef) ->
    erlang:nif_error(undef).

%%
%% Quality-of-life helpers
%%

read_file(Filename) ->
    %% We're doing this operation in the NIF to avoid excessive rescheduling.
    try
        read_file_nif(encode_path(Filename))
    catch
        error:badarg -> {error, badarg}
    end.
read_file_nif(_Filename) ->
    erlang:nif_error(undef).

write_file(Filename, Bytes) ->
    write_file(Filename, Bytes, []).
write_file(Filename, Bytes, Modes) ->
    case open(Filename, [write, binary | Modes]) of
        {ok, Fd} ->
            Result = write(Fd, Bytes),
            close(Fd),
            Result;
        {error, Reason} ->
            {error, Reason}
    end.

%%
%% Filesystem operations
%%

read_link(Name) -> read_link_1(Name, false).
read_link_all(Name) -> read_link_1(Name, true).

read_link_1(Name, AcceptRawNames) ->
    try read_link_nif(encode_path(Name)) of
        {ok, RawName} -> translate_raw_name(RawName, AcceptRawNames);
        {error, Reason} -> {error, Reason}
    catch
        error:badarg -> {error, badarg}
    end.

translate_raw_name(RawName, SilentFailure) ->
    case decode_path(RawName) of
        Converted when is_list(Converted) -> {ok, Converted};
        {error, _Reason} when SilentFailure =:= false -> {error, einval};
        {error, _Reason} when SilentFailure =:= true -> {ok, RawName}
    end.

list_dir(Name) -> list_dir_1(Name, true).
list_dir_all(Name) -> list_dir_1(Name, false).

list_dir_1(Name, SkipInvalid) ->
    try list_dir_nif(encode_path(Name)) of
        {ok, RawNames} -> list_dir_convert(RawNames, SkipInvalid, []);
        {error, Reason} -> {error, Reason}
    catch
        error:badarg -> {error, badarg}
    end.

list_dir_convert([], _SkipInvalid, Result) ->
    {ok, Result};
list_dir_convert([RawName | Rest], SkipInvalid, Result) ->
    case decode_path(RawName) of
        Converted when is_list(Converted) ->
            list_dir_convert(Rest, SkipInvalid, [Converted | Result]);
        {error, _} when SkipInvalid =:= false ->
            list_dir_convert(Rest, SkipInvalid, [RawName | Result]);

        %% If the filename cannot be converted, return error or ignore with
        %% optional error logger warning depending on +fn{u|a}{i|e|w} emulator
        %% switches.
        {error, ignore} ->
            list_dir_convert(Rest, SkipInvalid, Result);
        {error, warning} ->
            %% This is equal to calling logger:warning/3 which
            %% we don't want to do from code_server during system boot.
            %% We don't want to call logger:timestamp() either.
            catch logger ! {log,warning,"Non-unicode filename ~p ignored\n", [RawName],
                          #{pid=>self(),
                            gl=>group_leader(),
                            time=>os:system_time(microsecond),
                            error_logger=>#{tag=>warning_msg}}},
            list_dir_convert(Rest, SkipInvalid, Result);
        {error, _} ->
            {error, {no_translation, RawName}}
    end.

read_file_info(Filename) ->
    read_info_1(Filename, 1, local).
read_file_info(Filename, Opts) ->
    read_info_1(Filename, 1, proplist_get_value(time, Opts, local)).

read_link_info(Name) ->
    read_info_1(Name, 0, local).
read_link_info(Name, Opts) ->
    read_info_1(Name, 0, proplist_get_value(time, Opts, local)).

read_info_1(Name, FollowLinks, TimeType) ->
    try
        case read_info_nif(encode_path(Name), FollowLinks) of
            {error, Reason} -> {error, Reason};
            FileInfo -> {ok, adjust_times(FileInfo, TimeType)}
        end
    catch
        error:_ -> {error, badarg}
    end.

read_handle_info(Fd) ->
  read_handle_info_1(Fd, local).
read_handle_info(Fd, Opts) ->
  read_handle_info_1(Fd, proplist_get_value(time, Opts, local)).

read_handle_info_1(Fd, TimeType) ->
    try
        #{ handle := FRef } = get_fd_data(Fd),
        case read_handle_info_nif(FRef) of
            {error, Reason} -> {error, Reason};
            FileInfo -> {ok, adjust_times(FileInfo, TimeType)}
        end
    catch
        error:_ -> {error, badarg}
    end.

adjust_times(FileInfo, posix) ->
    FileInfo;
adjust_times(FileInfo, TimeType) ->
    CTime = from_posix_seconds(FileInfo#file_info.ctime, TimeType),
    MTime = from_posix_seconds(FileInfo#file_info.mtime, TimeType),
    ATime = from_posix_seconds(FileInfo#file_info.atime, TimeType),
    FileInfo#file_info{ ctime = CTime,
                        mtime = MTime,
                        atime = ATime }.

write_file_info(Filename, Info) ->
    write_file_info_1(Filename, Info, local).
write_file_info(Filename, Info, Opts) ->
    write_file_info_1(Filename, Info, proplist_get_value(time, Opts, local)).

write_file_info_1(Filename, Info, TimeType) ->
    #file_info{ mode = Modes,
                uid = Uid,
                gid = Gid,
                atime = ATime0,
                mtime = MTime0,
                ctime = CTime0} = Info,
    try
        % ATime and/or MTime might be undefined
        %  - use localtime() for atime, if atime is undefined
        %  - use atime as mtime if mtime is undefined
        %  - use mtime as ctime if ctime is undefined
        ATime = file_info_convert_atime(ATime0, TimeType),
        MTime = file_info_convert_mtime(MTime0, ATime, TimeType),
        CTime = file_info_convert_ctime(CTime0, MTime, TimeType),
        EncodedName = encode_path(Filename),

        %% This is a bit ugly but we need to handle partial failures the same
        %% way the old driver did.
        throw_on_error(set_owner(EncodedName, Uid, Gid)),
        throw_on_error(set_permissions(EncodedName, Modes)),
        throw_on_error(set_time(EncodedName, ATime, MTime, CTime))
    catch
        throw:Reason -> {error, Reason};
        error:_ -> {error, badarg}
    end.

set_owner(EncodedName, Uid, undefined) ->
    set_owner(EncodedName, Uid, -1);
set_owner(EncodedName, undefined, Gid) ->
    set_owner(EncodedName, -1, Gid);
set_owner(EncodedName, Uid, Gid) ->
    set_owner_nif(EncodedName, Uid, Gid).
set_owner_nif(_Path, _Uid, _Gid) ->
    erlang:nif_error(undef).

set_permissions(_EncodedName, undefined) ->
    ok;
set_permissions(EncodedName, Permissions) ->
    set_permissions_nif(EncodedName, Permissions).
set_permissions_nif(_Path, _Permissions) ->
    erlang:nif_error(undef).

set_time(EncodedName, ATime, MTime, CTime) ->
    set_time_nif(EncodedName, ATime, MTime, CTime).
set_time_nif(_Path, _ATime, _MTime, _CTime) ->
    erlang:nif_error(undef).

throw_on_error(ok) -> ok;
throw_on_error({error, enotsup}) -> ok;
throw_on_error({error, Reason}) -> throw(Reason).

file_info_convert_atime(ATime, TimeType) when ATime =/= undefined ->
    to_posix_seconds(ATime, TimeType);
file_info_convert_atime(undefined, local) ->
    to_posix_seconds(erlang:localtime(), local);
file_info_convert_atime(undefined, universal) ->
    to_posix_seconds(erlang:universaltime(), universal);
file_info_convert_atime(undefined, posix) ->
    erlang:universaltime_to_posixtime(erlang:universaltime()).

file_info_convert_mtime(undefined, ATime, _TimeType) ->
    ATime;
file_info_convert_mtime(MTime, _ATime, TimeType) ->
    to_posix_seconds(MTime, TimeType).

file_info_convert_ctime(undefined, MTime, _TimeType) ->
    MTime;
file_info_convert_ctime(CTime, _MTime, TimeType) ->
    to_posix_seconds(CTime, TimeType).

%% This is only relevant on Windows, so we assume that format to simplify the
%% internals.
get_cwd([Letter, $:]) when Letter >= $A, Letter =< $Z ->
    get_dcwd(Letter - $A + 1);
get_cwd([Letter, $:]) when Letter >= $a, Letter =< $z ->
    get_dcwd(Letter - $a + 1);
get_cwd([_|_]) ->
    {error, einval};
get_cwd(_) ->
    {error, badarg}.
get_dcwd(Index) ->
    try get_device_cwd_nif(Index) of
        {ok, RawPath} -> {ok, decode_path(RawPath)};
        {error, Reason} -> {error, Reason}
    catch
        error:badarg -> {error, badarg}
    end.

get_cwd() ->
    try get_cwd_nif() of
        {ok, RawPath} -> {ok, decode_path(RawPath)};
        {error, Reason} -> {error, Reason}
    catch
        error:badarg -> {error, badarg}
    end.
set_cwd(Path) ->
    try
        case is_path_translatable(Path) of
            true -> set_cwd_nif(encode_path(Path));
            false -> {error, no_translation}
        end
    catch
        error:badarg -> {error, badarg}
    end.

delete(Path) ->
    try
        del_file_nif(encode_path(Path))
    catch
        error:badarg -> {error, badarg}
    end.

rename(Source, Destination) ->
    try
        rename_nif(encode_path(Source), encode_path(Destination))
    catch
        error:badarg -> {error, badarg}
    end.
make_dir(Path) ->
    try
        make_dir_nif(encode_path(Path))
    catch
        error:badarg -> {error, badarg}
    end.
del_dir(Path) ->
    try
        del_dir_nif(encode_path(Path))
    catch
        error:badarg -> {error, badarg}
    end.
make_link(Existing, New) ->
    try
        make_hard_link_nif(encode_path(Existing), encode_path(New))
    catch
        error:badarg -> {error, badarg}
    end.
make_symlink(Existing, New) ->
    try
        make_soft_link_nif(encode_path(Existing), encode_path(New))
    catch
        error:badarg -> {error, badarg}
    end.

altname(Path) ->
    try altname_nif(encode_path(Path)) of
        {ok, RawPath} -> {ok, decode_path(RawPath)};
        Other -> Other
    catch
        error:badarg -> {error, badarg}
    end.

list_dir_nif(_Path) ->
    erlang:nif_error(undef).
read_link_nif(_Path) ->
    erlang:nif_error(undef).
read_info_nif(_Path, _FollowLinks) ->
    erlang:nif_error(undef).
make_hard_link_nif(_Existing, _New) ->
    erlang:nif_error(undef).
make_soft_link_nif(_Existing, _New) ->
    erlang:nif_error(undef).
rename_nif(_Source, _Destination) ->
    erlang:nif_error(undef).
make_dir_nif(_Path) ->
    erlang:nif_error(undef).
del_file_nif(_Path) ->
    erlang:nif_error(undef).
del_dir_nif(_Path) ->
    erlang:nif_error(undef).
get_device_cwd_nif(_DevicePath) ->
    erlang:nif_error(undef).
set_cwd_nif(_Path) ->
    erlang:nif_error(undef).
get_cwd_nif() ->
    erlang:nif_error(undef).
altname_nif(_Path) ->
    erlang:nif_error(undef).

%%
%% General helper functions.
%%

%% We know for certain that lists:reverse/2 is a BIF, so it's safe to use it
%% even though this module is preloaded.
reverse_list(List) -> lists:reverse(List, []).

proplist_get_value(_Key, [], Default) ->
    Default;
proplist_get_value(Key, [{Key, Value} | _Rest], _Default) ->
    Value;
proplist_get_value(Key, [Key | _Rest], _Default) ->
    true;
proplist_get_value(Key, [_Other | Rest], Default) ->
    proplist_get_value(Key, Rest, Default).

encode_path(Path) ->
    prim_file:internal_name2native(Path).
decode_path(NativePath) when is_binary(NativePath) ->
    prim_file:internal_native2name(NativePath).

is_path_translatable(Path) when is_list(Path) ->
    true;
is_path_translatable(Path) ->
    prim_file:is_translatable(Path).

%% We don't actually want this here
%%
%% We want to use posix time in all prim but erl_prim_loader makes that tricky
%% It is probably needed to redo the whole erl_prim_loader

from_posix_seconds(Seconds, universal) when is_integer(Seconds) ->
    erlang:posixtime_to_universaltime(Seconds);
from_posix_seconds(Seconds, local) when is_integer(Seconds) ->
    erlang:universaltime_to_localtime(erlang:posixtime_to_universaltime(Seconds)).

to_posix_seconds(Seconds, posix) when is_integer(Seconds) ->
    Seconds;
to_posix_seconds({_,_} = Datetime, universal) ->
    erlang:universaltime_to_posixtime(Datetime);
to_posix_seconds({_,_} = Datetime, local) ->
    erlang:universaltime_to_posixtime(erlang:localtime_to_universaltime(Datetime)).
