%% Vendored from hex_core v0.7.0, do not edit manually
|
|
|
|
%% @private
|
|
%% Copied from https://github.com/erlang/otp/blob/OTP-20.0.1/lib/stdlib/src/erl_tar.erl
|
|
%% with modifications:
|
|
%% - Change module name to `r3_hex_erl_tar`
|
|
%% - Set tar mtimes to 0 and remove dependency on :os.system_time/1
|
|
%% - Preserve modes when building tarball
|
|
%% - Do not crash if failing to write tar
|
|
%% - Allow setting file_info opts on :r3_hex_erl_tar.add
|
|
%% - Add safe_relative_path_links/2 to check directory traversal vulnerability when extracting files,
|
|
%% it differs from OTP's current fix (2020-02-04) in that it checks regular files instead of
|
|
%% symlink targets. This allows creating symlinks with relative path targets such as `../tmp/log`
|
|
%% - Remove ram_file usage (backported from OTP master)
|
|
|
|
%%
|
|
%% %CopyrightBegin%
|
|
%%
|
|
%% Copyright Ericsson AB 1997-2017. 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%
|
|
%%
|
|
%% This module implements extraction/creation of tar archives.
|
|
%% It supports reading most common tar formats, namely V7, STAR,
|
|
%% USTAR, GNU, BSD/libarchive, and PAX. It produces archives in USTAR
|
|
%% format, unless it must use PAX headers, in which case it produces PAX
|
|
%% format.
|
|
%%
|
|
%% The following references where used:
|
|
%% http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5
|
|
%% http://www.gnu.org/software/tar/manual/html_node/Standard.html
|
|
%% http://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html
|
|
-module(r3_hex_erl_tar).
|
|
|
|
-export([init/3,
|
|
create/2, create/3,
|
|
extract/1, extract/2,
|
|
table/1, table/2, t/1, tt/1,
|
|
open/2, close/1,
|
|
add/3, add/4, add/5,
|
|
format_error/1]).
|
|
|
|
-include_lib("kernel/include/file.hrl").
|
|
-include_lib("r3_hex_erl_tar.hrl").
|
|
|
|
%% Converts the short error reason to a descriptive string.
|
|
-spec format_error(term()) -> string().
|
|
format_error(invalid_tar_checksum) ->
|
|
"Checksum failed";
|
|
format_error(bad_header) ->
|
|
"Unrecognized tar header format";
|
|
format_error({bad_header, Reason}) ->
|
|
lists:flatten(io_lib:format("Unrecognized tar header format: ~p", [Reason]));
|
|
format_error({invalid_header, negative_size}) ->
|
|
"Invalid header: negative size";
|
|
format_error(invalid_sparse_header_size) ->
|
|
"Invalid sparse header: negative size";
|
|
format_error(invalid_sparse_map_entry) ->
|
|
"Invalid sparse map entry";
|
|
format_error({invalid_sparse_map_entry, Reason}) ->
|
|
lists:flatten(io_lib:format("Invalid sparse map entry: ~p", [Reason]));
|
|
format_error(invalid_end_of_archive) ->
|
|
"Invalid end of archive";
|
|
format_error(eof) ->
|
|
"Unexpected end of file";
|
|
format_error(integer_overflow) ->
|
|
"Failed to parse numeric: integer overflow";
|
|
format_error({misaligned_read, Pos}) ->
|
|
lists:flatten(io_lib:format("Read a block which was misaligned: block_size=~p pos=~p",
|
|
[?BLOCK_SIZE, Pos]));
|
|
format_error(invalid_gnu_1_0_sparsemap) ->
|
|
"Invalid GNU sparse map (version 1.0)";
|
|
format_error({invalid_gnu_0_1_sparsemap, Format}) ->
|
|
lists:flatten(io_lib:format("Invalid GNU sparse map (version ~s)", [Format]));
|
|
format_error(unsafe_path) ->
|
|
"The path points above the current working directory";
|
|
format_error({Name,Reason}) ->
|
|
lists:flatten(io_lib:format("~ts: ~ts", [Name,format_error(Reason)]));
|
|
format_error(Atom) when is_atom(Atom) ->
|
|
file:format_error(Atom);
|
|
format_error(Term) ->
|
|
lists:flatten(io_lib:format("~tp", [Term])).
|
|
|
|
%% Initializes a new reader given a custom file handle and I/O wrappers
|
|
-spec init(handle(), write | read, file_op()) -> {ok, reader()} | {error, badarg}.
|
|
init(Handle, AccessMode, Fun) when is_function(Fun, 2) ->
|
|
Reader = #reader{handle=Handle,access=AccessMode,func=Fun},
|
|
{ok, Pos, Reader2} = do_position(Reader, {cur, 0}),
|
|
{ok, Reader2#reader{pos=Pos}};
|
|
init(_Handle, _AccessMode, _Fun) ->
|
|
{error, badarg}.
|
|
|
|
%%%================================================================
|
|
%% Extracts all files from the tar file Name.
|
|
-spec extract(open_handle()) -> ok | {error, term()}.
|
|
extract(Name) ->
|
|
extract(Name, []).
|
|
|
|
%% Extracts (all) files from the tar file Name.
|
|
%% Options accepted:
|
|
%% - cooked: Opens the tar file without mode `raw`
|
|
%% - compressed: Uncompresses the tar file when reading
|
|
%% - memory: Returns the tar contents as a list of tuples {Name, Bin}
|
|
%% - keep_old_files: Extracted files will not overwrite the destination
|
|
%% - {files, ListOfFilesToExtract}: Only extract ListOfFilesToExtract
|
|
%% - verbose: Prints verbose information about the extraction,
|
|
%% - {cwd, AbsoluteDir}: Sets the current working directory for the extraction
|
|
-spec extract(open_handle(), [extract_opt()]) ->
|
|
ok
|
|
| {ok, [{string(), binary()}]}
|
|
| {error, term()}.
|
|
extract({binary, Bin}, Opts) when is_list(Opts) ->
|
|
do_extract({binary, Bin}, Opts);
|
|
extract({file, Fd}, Opts) when is_list(Opts) ->
|
|
do_extract({file, Fd}, Opts);
|
|
extract(#reader{}=Reader, Opts) when is_list(Opts) ->
|
|
do_extract(Reader, Opts);
|
|
extract(Name, Opts) when is_list(Name); is_binary(Name), is_list(Opts) ->
|
|
do_extract(Name, Opts).
|
|
|
|
do_extract(Handle, Opts) when is_list(Opts) ->
|
|
Opts2 = extract_opts(Opts),
|
|
Acc = if Opts2#read_opts.output =:= memory -> []; true -> ok end,
|
|
foldl_read(Handle, fun extract1/4, Acc, Opts2).
|
|
|
|
extract1(eof, Reader, _, Acc) when is_list(Acc) ->
|
|
{ok, {ok, lists:reverse(Acc)}, Reader};
|
|
extract1(eof, Reader, _, leading_slash) ->
|
|
error_logger:info_msg("erl_tar: removed leading '/' from member names\n"),
|
|
{ok, ok, Reader};
|
|
extract1(eof, Reader, _, Acc) ->
|
|
{ok, Acc, Reader};
|
|
extract1(#tar_header{name=Name,size=Size}=Header, Reader0, Opts, Acc0) ->
|
|
case check_extract(Name, Opts) of
|
|
true ->
|
|
case do_read(Reader0, Size) of
|
|
{ok, Bin, Reader1} ->
|
|
Acc = extract2(Header, Bin, Opts, Acc0),
|
|
{ok, Acc, Reader1};
|
|
{error, _} = Err ->
|
|
throw(Err)
|
|
end;
|
|
false ->
|
|
{ok, Acc0, skip_file(Reader0)}
|
|
end.
|
|
|
|
extract2(Header, Bin, Opts, Acc) ->
|
|
case write_extracted_element(Header, Bin, Opts) of
|
|
ok ->
|
|
case Header of
|
|
#tar_header{name="/"++_} ->
|
|
leading_slash;
|
|
#tar_header{} ->
|
|
Acc
|
|
end;
|
|
{ok, NameBin} when is_list(Acc) ->
|
|
[NameBin | Acc];
|
|
{error, _} = Err ->
|
|
throw(Err)
|
|
end.
|
|
|
|
%% Checks if the file Name should be extracted.
|
|
check_extract(_, #read_opts{files=all}) ->
|
|
true;
|
|
check_extract(Name, #read_opts{files=Files}) ->
|
|
ordsets:is_element(Name, Files).
|
|
|
|
%%%================================================================
|
|
%% The following table functions produce a list of information about
|
|
%% the files contained in the archive.
|
|
-type filename() :: string().
|
|
-type typeflag() :: regular | link | symlink |
|
|
char | block | directory |
|
|
fifo | reserved | unknown.
|
|
-type mode() :: non_neg_integer().
|
|
-type uid() :: non_neg_integer().
|
|
-type gid() :: non_neg_integer().
|
|
|
|
-type tar_entry() :: {filename(),
|
|
typeflag(),
|
|
non_neg_integer(),
|
|
tar_time(),
|
|
mode(),
|
|
uid(),
|
|
gid()}.
|
|
|
|
%% Returns a list of names of the files in the tar file Name.
|
|
-spec table(open_handle()) -> {ok, [string()]} | {error, term()}.
|
|
table(Name) ->
|
|
table(Name, []).
|
|
|
|
%% Returns a list of names of the files in the tar file Name.
|
|
%% Options accepted: compressed, verbose, cooked.
|
|
-spec table(open_handle(), [compressed | verbose | cooked]) ->
|
|
{ok, [tar_entry()]} | {error, term()}.
|
|
table(Name, Opts) when is_list(Opts) ->
|
|
foldl_read(Name, fun table1/4, [], table_opts(Opts)).
|
|
|
|
table1(eof, Reader, _, Result) ->
|
|
{ok, {ok, lists:reverse(Result)}, Reader};
|
|
table1(#tar_header{}=Header, Reader, #read_opts{verbose=Verbose}, Result) ->
|
|
Attrs = table1_attrs(Header, Verbose),
|
|
Reader2 = skip_file(Reader),
|
|
{ok, [Attrs|Result], Reader2}.
|
|
|
|
%% Extracts attributes relevant to table1's output
|
|
table1_attrs(#tar_header{typeflag=Typeflag,mode=Mode}=Header, true) ->
|
|
Type = typeflag(Typeflag),
|
|
Name = Header#tar_header.name,
|
|
Mtime = Header#tar_header.mtime,
|
|
Uid = Header#tar_header.uid,
|
|
Gid = Header#tar_header.gid,
|
|
Size = Header#tar_header.size,
|
|
{Name, Type, Size, Mtime, Mode, Uid, Gid};
|
|
table1_attrs(#tar_header{name=Name}, _Verbose) ->
|
|
Name.
|
|
|
|
typeflag(?TYPE_REGULAR) -> regular;
|
|
typeflag(?TYPE_REGULAR_A) -> regular;
|
|
typeflag(?TYPE_GNU_SPARSE) -> regular;
|
|
typeflag(?TYPE_CONT) -> regular;
|
|
typeflag(?TYPE_LINK) -> link;
|
|
typeflag(?TYPE_SYMLINK) -> symlink;
|
|
typeflag(?TYPE_CHAR) -> char;
|
|
typeflag(?TYPE_BLOCK) -> block;
|
|
typeflag(?TYPE_DIR) -> directory;
|
|
typeflag(?TYPE_FIFO) -> fifo;
|
|
typeflag(_) -> unknown.
|
|
|
|
%%%================================================================
|
|
%% Comments for printing the contents of a tape archive,
|
|
%% meant to be invoked from the shell.
|
|
|
|
%% Prints each filename in the archive
|
|
-spec t(file:filename()) -> ok | {error, term()}.
|
|
t(Name) when is_list(Name); is_binary(Name) ->
|
|
case table(Name) of
|
|
{ok, List} ->
|
|
lists:foreach(fun(N) -> ok = io:format("~ts\n", [N]) end, List);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
%% Prints verbose information about each file in the archive
|
|
-spec tt(open_handle()) -> ok | {error, term()}.
|
|
tt(Name) ->
|
|
case table(Name, [verbose]) of
|
|
{ok, List} ->
|
|
lists:foreach(fun print_header/1, List);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
%% Used by tt/1 to print a tar_entry tuple
|
|
-spec print_header(tar_entry()) -> ok.
|
|
print_header({Name, Type, Size, Mtime, Mode, Uid, Gid}) ->
|
|
io:format("~s~s ~4w/~-4w ~7w ~s ~s\n",
|
|
[type_to_string(Type), mode_to_string(Mode),
|
|
Uid, Gid, Size, time_to_string(Mtime), Name]).
|
|
|
|
type_to_string(regular) -> "-";
|
|
type_to_string(directory) -> "d";
|
|
type_to_string(link) -> "l";
|
|
type_to_string(symlink) -> "s";
|
|
type_to_string(char) -> "c";
|
|
type_to_string(block) -> "b";
|
|
type_to_string(fifo) -> "f";
|
|
type_to_string(unknown) -> "?".
|
|
|
|
%% Converts a numeric mode to its human-readable representation
|
|
mode_to_string(Mode) ->
|
|
mode_to_string(Mode, "xwrxwrxwr", []).
|
|
mode_to_string(Mode, [C|T], Acc) when Mode band 1 =:= 1 ->
|
|
mode_to_string(Mode bsr 1, T, [C|Acc]);
|
|
mode_to_string(Mode, [_|T], Acc) ->
|
|
mode_to_string(Mode bsr 1, T, [$-|Acc]);
|
|
mode_to_string(_, [], Acc) ->
|
|
Acc.
|
|
|
|
%% Converts a tar_time() (POSIX time) to a readable string
|
|
time_to_string(Secs0) ->
|
|
Epoch = calendar:datetime_to_gregorian_seconds(?EPOCH),
|
|
Secs = Epoch + Secs0,
|
|
DateTime0 = calendar:gregorian_seconds_to_datetime(Secs),
|
|
DateTime = calendar:universal_time_to_local_time(DateTime0),
|
|
{{Y, Mon, Day}, {H, Min, _}} = DateTime,
|
|
io_lib:format("~s ~2w ~s:~s ~w", [month(Mon), Day, two_d(H), two_d(Min), Y]).
|
|
|
|
two_d(N) ->
|
|
tl(integer_to_list(N + 100)).
|
|
|
|
month(1) -> "Jan";
|
|
month(2) -> "Feb";
|
|
month(3) -> "Mar";
|
|
month(4) -> "Apr";
|
|
month(5) -> "May";
|
|
month(6) -> "Jun";
|
|
month(7) -> "Jul";
|
|
month(8) -> "Aug";
|
|
month(9) -> "Sep";
|
|
month(10) -> "Oct";
|
|
month(11) -> "Nov";
|
|
month(12) -> "Dec".
|
|
|
|
%%%================================================================
|
|
%% The open function with friends is to keep the file and binary api of this module
|
|
-type open_handle() :: file:filename()
|
|
| {binary, binary()}
|
|
| {file, term()}.
|
|
-spec open(open_handle(), [write | compressed | cooked]) ->
|
|
{ok, reader()} | {error, term()}.
|
|
open({binary, Bin}, Mode) when is_binary(Bin) ->
|
|
do_open({binary, Bin}, Mode);
|
|
open({file, Fd}, Mode) ->
|
|
do_open({file, Fd}, Mode);
|
|
open(Name, Mode) when is_list(Name); is_binary(Name) ->
|
|
do_open(Name, Mode).
|
|
|
|
do_open(Name, Mode) when is_list(Mode) ->
|
|
case open_mode(Mode) of
|
|
{ok, Access, Raw, Opts} ->
|
|
open1(Name, Access, Raw, Opts);
|
|
{error, Reason} ->
|
|
{error, {Name, Reason}}
|
|
end.
|
|
|
|
open1({binary,Bin0}, read, _Raw, Opts) when is_binary(Bin0) ->
|
|
Bin = case lists:member(compressed, Opts) of
|
|
true ->
|
|
try
|
|
zlib:gunzip(Bin0)
|
|
catch
|
|
_:_ -> Bin0
|
|
end;
|
|
false ->
|
|
Bin0
|
|
end,
|
|
case file:open(Bin, [ram,binary,read]) of
|
|
{ok,File} ->
|
|
{ok, #reader{handle=File,access=read,func=fun file_op/2}};
|
|
Error ->
|
|
Error
|
|
end;
|
|
open1({file, Fd}, read, _Raw, _Opts) ->
|
|
Reader = #reader{handle=Fd,access=read,func=fun file_op/2},
|
|
case do_position(Reader, {cur, 0}) of
|
|
{ok, Pos, Reader2} ->
|
|
{ok, Reader2#reader{pos=Pos}};
|
|
{error, _} = Err ->
|
|
Err
|
|
end;
|
|
open1(Name, Access, Raw, Opts) when is_list(Name) or is_binary(Name) ->
|
|
case file:open(Name, Raw ++ [binary, Access|Opts]) of
|
|
{ok, File} ->
|
|
{ok, #reader{handle=File,access=Access,func=fun file_op/2}};
|
|
{error, Reason} ->
|
|
{error, {Name, Reason}}
|
|
end.
|
|
|
|
open_mode(Mode) ->
|
|
open_mode(Mode, false, [raw], []).
|
|
|
|
open_mode(read, _, Raw, _) ->
|
|
{ok, read, Raw, []};
|
|
open_mode(write, _, Raw, _) ->
|
|
{ok, write, Raw, []};
|
|
open_mode([read|Rest], false, Raw, Opts) ->
|
|
open_mode(Rest, read, Raw, Opts);
|
|
open_mode([write|Rest], false, Raw, Opts) ->
|
|
open_mode(Rest, write, Raw, Opts);
|
|
open_mode([compressed|Rest], Access, Raw, Opts) ->
|
|
open_mode(Rest, Access, Raw, [compressed|Opts]);
|
|
open_mode([cooked|Rest], Access, _Raw, Opts) ->
|
|
open_mode(Rest, Access, [], Opts);
|
|
open_mode([], Access, Raw, Opts) ->
|
|
{ok, Access, Raw, Opts};
|
|
open_mode(_, _, _, _) ->
|
|
{error, einval}.
|
|
|
|
file_op(write, {Fd, Data}) ->
|
|
file:write(Fd, Data);
|
|
file_op(position, {Fd, Pos}) ->
|
|
file:position(Fd, Pos);
|
|
file_op(read2, {Fd, Size}) ->
|
|
file:read(Fd, Size);
|
|
file_op(close, Fd) ->
|
|
file:close(Fd).
|
|
|
|
%% Closes a tar archive.
|
|
-spec close(reader()) -> ok | {error, term()}.
|
|
close(#reader{access=read}=Reader) ->
|
|
ok = do_close(Reader);
|
|
close(#reader{access=write}=Reader) ->
|
|
{ok, Reader2} = pad_file(Reader),
|
|
ok = do_close(Reader2),
|
|
ok;
|
|
close(_) ->
|
|
{error, einval}.
|
|
|
|
pad_file(#reader{pos=Pos}=Reader) ->
|
|
%% There must be at least two zero blocks at the end.
|
|
PadCurrent = skip_padding(Pos+?BLOCK_SIZE),
|
|
Padding = <<0:PadCurrent/unit:8>>,
|
|
do_write(Reader, [Padding, ?ZERO_BLOCK, ?ZERO_BLOCK]).
|
|
|
|
|
|
%%%================================================================
|
|
%% Creation/modification of tar archives
|
|
|
|
%% Creates a tar file Name containing the given files.
|
|
-spec create(file:filename(), filelist()) -> ok | {error, {string(), term()}}.
|
|
create(Name, FileList) when is_list(Name); is_binary(Name) ->
|
|
create(Name, FileList, []).
|
|
|
|
%% Creates a tar archive Name containing the given files.
|
|
%% Accepted options: verbose, compressed, cooked
|
|
-spec create(file:filename(), filelist(), [create_opt()]) ->
|
|
ok | {error, term()} | {error, {string(), term()}}.
|
|
create(Name, FileList, Options) when is_list(Name); is_binary(Name) ->
|
|
Mode = lists:filter(fun(X) -> (X=:=compressed) or (X=:=cooked)
|
|
end, Options),
|
|
case open(Name, [write|Mode]) of
|
|
{ok, TarFile} ->
|
|
do_create(TarFile, FileList, Options);
|
|
{error, _} = Err ->
|
|
Err
|
|
end.
|
|
|
|
do_create(TarFile, [], _Opts) ->
|
|
close(TarFile);
|
|
do_create(TarFile, [{NameInArchive, NameOrBin}|Rest], Opts) ->
|
|
case add(TarFile, NameOrBin, NameInArchive, Opts) of
|
|
ok ->
|
|
do_create(TarFile, Rest, Opts);
|
|
{error, _} = Err ->
|
|
_ = close(TarFile),
|
|
Err
|
|
end;
|
|
do_create(TarFile, [Name|Rest], Opts) ->
|
|
case add(TarFile, Name, Name, Opts) of
|
|
ok ->
|
|
do_create(TarFile, Rest, Opts);
|
|
{error, _} = Err ->
|
|
_ = close(TarFile),
|
|
Err
|
|
end.
|
|
|
|
%% Adds a file to a tape archive.
|
|
-type add_type() :: string()
|
|
| {string(), string()}
|
|
| {string(), binary()}.
|
|
-spec add(reader(), add_type(), [add_opt()]) -> ok | {error, term()}.
|
|
add(Reader, {NameInArchive, Name}, Opts)
|
|
when is_list(NameInArchive), is_list(Name) ->
|
|
do_add(Reader, Name, NameInArchive, undefined, Opts);
|
|
add(Reader, {NameInArchive, Bin}, Opts)
|
|
when is_list(NameInArchive), is_binary(Bin) ->
|
|
do_add(Reader, Bin, NameInArchive, undefined, Opts);
|
|
add(Reader, {NameInArchive, Bin, Mode}, Opts)
|
|
when is_list(NameInArchive), is_binary(Bin), is_integer(Mode) ->
|
|
do_add(Reader, Bin, NameInArchive, Mode, Opts);
|
|
add(Reader, Name, Opts) when is_list(Name) ->
|
|
do_add(Reader, Name, Name, undefined, Opts).
|
|
|
|
|
|
-spec add(reader(), string() | binary(), string(), [add_opt()]) ->
|
|
ok | {error, term()}.
|
|
add(Reader, NameOrBin, NameInArchive, Options)
|
|
when is_list(NameOrBin); is_binary(NameOrBin),
|
|
is_list(NameInArchive), is_list(Options) ->
|
|
do_add(Reader, NameOrBin, NameInArchive, undefined, Options).
|
|
|
|
-spec add(reader(), string() | binary(), string(), integer(), [add_opt()]) ->
|
|
ok | {error, term()}.
|
|
add(Reader, NameOrBin, NameInArchive, Mode, Options)
|
|
when is_list(NameOrBin); is_binary(NameOrBin),
|
|
is_list(NameInArchive), is_integer(Mode), is_list(Options) ->
|
|
do_add(Reader, NameOrBin, NameInArchive, Mode, Options).
|
|
|
|
do_add(#reader{access=write}=Reader, Name, NameInArchive, Mode, Options)
|
|
when is_list(NameInArchive), is_list(Options) ->
|
|
RF = fun(F) -> apply_file_info_opts(Options, file:read_link_info(F, [{time, posix}])) end,
|
|
Opts = #add_opts{read_info=RF},
|
|
add1(Reader, Name, NameInArchive, Mode, add_opts(Options, Options, Opts));
|
|
do_add(#reader{access=read},_,_,_,_) ->
|
|
{error, eacces};
|
|
do_add(Reader,_,_,_,_) ->
|
|
{error, {badarg, Reader}}.
|
|
|
|
add_opts([dereference|T], AllOptions, Opts) ->
|
|
RF = fun(F) -> apply_file_info_opts(AllOptions, file:read_file_info(F, [{time, posix}])) end,
|
|
add_opts(T, AllOptions, Opts#add_opts{read_info=RF});
|
|
add_opts([verbose|T], AllOptions, Opts) ->
|
|
add_opts(T, AllOptions, Opts#add_opts{verbose=true});
|
|
add_opts([{chunks,N}|T], AllOptions, Opts) ->
|
|
add_opts(T, AllOptions, Opts#add_opts{chunk_size=N});
|
|
add_opts([{atime,Value}|T], AllOptions, Opts) ->
|
|
add_opts(T, AllOptions, Opts#add_opts{atime=Value});
|
|
add_opts([{mtime,Value}|T], AllOptions, Opts) ->
|
|
add_opts(T, AllOptions, Opts#add_opts{mtime=Value});
|
|
add_opts([{ctime,Value}|T], AllOptions, Opts) ->
|
|
add_opts(T, AllOptions, Opts#add_opts{ctime=Value});
|
|
add_opts([{uid,Value}|T], AllOptions, Opts) ->
|
|
add_opts(T, AllOptions, Opts#add_opts{uid=Value});
|
|
add_opts([{gid,Value}|T], AllOptions, Opts) ->
|
|
add_opts(T, AllOptions, Opts#add_opts{gid=Value});
|
|
add_opts([_|T], AllOptions, Opts) ->
|
|
add_opts(T, AllOptions, Opts);
|
|
add_opts([], _AllOptions, Opts) ->
|
|
Opts.
|
|
|
|
apply_file_info_opts(Opts, {ok, FileInfo}) ->
|
|
{ok, do_apply_file_info_opts(Opts, FileInfo)};
|
|
apply_file_info_opts(_Opts, Other) ->
|
|
Other.
|
|
|
|
do_apply_file_info_opts([{atime,Value}|T], FileInfo) ->
|
|
do_apply_file_info_opts(T, FileInfo#file_info{atime=Value});
|
|
do_apply_file_info_opts([{mtime,Value}|T], FileInfo) ->
|
|
do_apply_file_info_opts(T, FileInfo#file_info{mtime=Value});
|
|
do_apply_file_info_opts([{ctime,Value}|T], FileInfo) ->
|
|
do_apply_file_info_opts(T, FileInfo#file_info{ctime=Value});
|
|
do_apply_file_info_opts([{uid,Value}|T], FileInfo) ->
|
|
do_apply_file_info_opts(T, FileInfo#file_info{uid=Value});
|
|
do_apply_file_info_opts([{gid,Value}|T], FileInfo) ->
|
|
do_apply_file_info_opts(T, FileInfo#file_info{gid=Value});
|
|
do_apply_file_info_opts([_|T], FileInfo) ->
|
|
do_apply_file_info_opts(T, FileInfo);
|
|
do_apply_file_info_opts([], FileInfo) ->
|
|
FileInfo.
|
|
|
|
add1(#reader{}=Reader, Name, NameInArchive, undefined, #add_opts{read_info=ReadInfo}=Opts)
|
|
when is_list(Name) ->
|
|
Res = case ReadInfo(Name) of
|
|
{error, Reason0} ->
|
|
{error, {Name, Reason0}};
|
|
{ok, #file_info{type=symlink}=Fi} ->
|
|
add_verbose(Opts, "a ~ts~n", [NameInArchive]),
|
|
{ok, Linkname} = file:read_link(Name),
|
|
Header = fileinfo_to_header(NameInArchive, Fi, Linkname),
|
|
add_header(Reader, Header, Opts);
|
|
{ok, #file_info{type=regular}=Fi} ->
|
|
add_verbose(Opts, "a ~ts~n", [NameInArchive]),
|
|
Header = fileinfo_to_header(NameInArchive, Fi, false),
|
|
{ok, Reader2} = add_header(Reader, Header, Opts),
|
|
FileSize = Header#tar_header.size,
|
|
{ok, FileSize, Reader3} = do_copy(Reader2, Name, Opts),
|
|
Padding = skip_padding(FileSize),
|
|
Pad = <<0:Padding/unit:8>>,
|
|
do_write(Reader3, Pad);
|
|
{ok, #file_info{type=directory}=Fi} ->
|
|
add_directory(Reader, Name, NameInArchive, Fi, Opts);
|
|
{ok, #file_info{}=Fi} ->
|
|
add_verbose(Opts, "a ~ts~n", [NameInArchive]),
|
|
Header = fileinfo_to_header(NameInArchive, Fi, false),
|
|
add_header(Reader, Header, Opts)
|
|
end,
|
|
case Res of
|
|
ok -> ok;
|
|
{ok, _Reader} -> ok;
|
|
{error, _Reason} = Err -> Err
|
|
end;
|
|
add1(Reader, Bin, NameInArchive, Mode, Opts) when is_binary(Bin) ->
|
|
add_verbose(Opts, "a ~ts~n", [NameInArchive]),
|
|
Now = 0,
|
|
Header = #tar_header{
|
|
name = NameInArchive,
|
|
size = byte_size(Bin),
|
|
typeflag = ?TYPE_REGULAR,
|
|
atime = add_opts_time(Opts#add_opts.atime, Now),
|
|
mtime = add_opts_time(Opts#add_opts.mtime, Now),
|
|
ctime = add_opts_time(Opts#add_opts.ctime, Now),
|
|
uid = Opts#add_opts.uid,
|
|
gid = Opts#add_opts.gid,
|
|
mode = default_mode(Mode, 8#100644)},
|
|
{ok, Reader2} = add_header(Reader, Header, Opts),
|
|
Padding = skip_padding(byte_size(Bin)),
|
|
Data = [Bin, <<0:Padding/unit:8>>],
|
|
case do_write(Reader2, Data) of
|
|
{ok, _Reader3} -> ok;
|
|
{error, Reason} -> {error, {NameInArchive, Reason}}
|
|
end.
|
|
|
|
add_opts_time(undefined, _Now) -> 0;
|
|
add_opts_time(Time, _Now) -> Time.
|
|
|
|
default_mode(undefined, Mode) -> Mode;
|
|
default_mode(Mode, _) -> Mode.
|
|
|
|
add_directory(Reader, DirName, NameInArchive, Info, Opts) ->
|
|
case file:list_dir(DirName) of
|
|
{ok, []} ->
|
|
add_verbose(Opts, "a ~ts~n", [NameInArchive]),
|
|
Header = fileinfo_to_header(NameInArchive, Info, false),
|
|
add_header(Reader, Header, Opts);
|
|
{ok, Files} ->
|
|
add_verbose(Opts, "a ~ts~n", [NameInArchive]),
|
|
try add_files(Reader, Files, DirName, NameInArchive, Opts) of
|
|
ok -> ok;
|
|
{error, _} = Err -> Err
|
|
catch
|
|
throw:{error, {_Name, _Reason}} = Err -> Err;
|
|
throw:{error, Reason} -> {error, {DirName, Reason}}
|
|
end;
|
|
{error, Reason} ->
|
|
{error, {DirName, Reason}}
|
|
end.
|
|
|
|
add_files(_Reader, [], _Dir, _DirInArchive, _Opts) ->
|
|
ok;
|
|
add_files(Reader, [Name|Rest], Dir, DirInArchive, #add_opts{read_info=Info}=Opts) ->
|
|
FullName = filename:join(Dir, Name),
|
|
NameInArchive = filename:join(DirInArchive, Name),
|
|
Res = case Info(FullName) of
|
|
{error, Reason} ->
|
|
{error, {FullName, Reason}};
|
|
{ok, #file_info{type=directory}=Fi} ->
|
|
add_directory(Reader, FullName, NameInArchive, Fi, Opts);
|
|
{ok, #file_info{type=symlink}=Fi} ->
|
|
add_verbose(Opts, "a ~ts~n", [NameInArchive]),
|
|
{ok, Linkname} = file:read_link(FullName),
|
|
Header = fileinfo_to_header(NameInArchive, Fi, Linkname),
|
|
add_header(Reader, Header, Opts);
|
|
{ok, #file_info{type=regular}=Fi} ->
|
|
add_verbose(Opts, "a ~ts~n", [NameInArchive]),
|
|
Header = fileinfo_to_header(NameInArchive, Fi, false),
|
|
{ok, Reader2} = add_header(Reader, Header, Opts),
|
|
FileSize = Header#tar_header.size,
|
|
{ok, FileSize, Reader3} = do_copy(Reader2, FullName, Opts),
|
|
Padding = skip_padding(FileSize),
|
|
Pad = <<0:Padding/unit:8>>,
|
|
do_write(Reader3, Pad);
|
|
{ok, #file_info{}=Fi} ->
|
|
add_verbose(Opts, "a ~ts~n", [NameInArchive]),
|
|
Header = fileinfo_to_header(NameInArchive, Fi, false),
|
|
add_header(Reader, Header, Opts)
|
|
end,
|
|
case Res of
|
|
ok -> add_files(Reader, Rest, Dir, DirInArchive, Opts);
|
|
{ok, ReaderNext} -> add_files(ReaderNext, Rest, Dir, DirInArchive, Opts);
|
|
{error, _} = Err -> Err
|
|
end.
|
|
|
|
format_string(String, Size) when length(String) > Size ->
|
|
throw({error, {write_string, field_too_long}});
|
|
format_string(String, Size) ->
|
|
Ascii = to_ascii(String),
|
|
if byte_size(Ascii) < Size ->
|
|
[Ascii, 0];
|
|
true ->
|
|
Ascii
|
|
end.
|
|
|
|
format_octal(Octal) ->
|
|
iolist_to_binary(io_lib:fwrite("~.8B", [Octal])).
|
|
|
|
add_header(#reader{}=Reader, #tar_header{}=Header, Opts) ->
|
|
{ok, Iodata} = build_header(Header, Opts),
|
|
do_write(Reader, Iodata).
|
|
|
|
write_to_block(Block, IoData, Start) when is_list(IoData) ->
|
|
write_to_block(Block, iolist_to_binary(IoData), Start);
|
|
write_to_block(Block, Bin, Start) when is_binary(Bin) ->
|
|
Size = byte_size(Bin),
|
|
<<Head:Start/unit:8, _:Size/unit:8, Rest/binary>> = Block,
|
|
<<Head:Start/unit:8, Bin/binary, Rest/binary>>.
|
|
|
|
build_header(#tar_header{}=Header, Opts) ->
|
|
#tar_header{
|
|
name=Name,
|
|
mode=Mode,
|
|
uid=Uid,
|
|
gid=Gid,
|
|
size=Size,
|
|
typeflag=Type,
|
|
linkname=Linkname,
|
|
uname=Uname,
|
|
gname=Gname,
|
|
devmajor=Devmaj,
|
|
devminor=Devmin
|
|
} = Header,
|
|
Mtime = Header#tar_header.mtime,
|
|
|
|
Block0 = ?ZERO_BLOCK,
|
|
{Block1, Pax0} = write_string(Block0, ?V7_NAME, ?V7_NAME_LEN, Name, ?PAX_PATH, #{}),
|
|
Block2 = write_octal(Block1, ?V7_MODE, ?V7_MODE_LEN, Mode),
|
|
{Block3, Pax1} = write_numeric(Block2, ?V7_UID, ?V7_UID_LEN, Uid, ?PAX_UID, Pax0),
|
|
{Block4, Pax2} = write_numeric(Block3, ?V7_GID, ?V7_GID_LEN, Gid, ?PAX_GID, Pax1),
|
|
{Block5, Pax3} = write_numeric(Block4, ?V7_SIZE, ?V7_SIZE_LEN, Size, ?PAX_SIZE, Pax2),
|
|
{Block6, Pax4} = write_numeric(Block5, ?V7_MTIME, ?V7_MTIME_LEN, Mtime, ?PAX_NONE, Pax3),
|
|
{Block7, Pax5} = write_string(Block6, ?V7_TYPE, ?V7_TYPE_LEN, <<Type>>, ?PAX_NONE, Pax4),
|
|
{Block8, Pax6} = write_string(Block7, ?V7_LINKNAME, ?V7_LINKNAME_LEN,
|
|
Linkname, ?PAX_LINKPATH, Pax5),
|
|
{Block9, Pax7} = write_string(Block8, ?USTAR_UNAME, ?USTAR_UNAME_LEN,
|
|
Uname, ?PAX_UNAME, Pax6),
|
|
{Block10, Pax8} = write_string(Block9, ?USTAR_GNAME, ?USTAR_GNAME_LEN,
|
|
Gname, ?PAX_GNAME, Pax7),
|
|
{Block11, Pax9} = write_numeric(Block10, ?USTAR_DEVMAJ, ?USTAR_DEVMAJ_LEN,
|
|
Devmaj, ?PAX_NONE, Pax8),
|
|
{Block12, Pax10} = write_numeric(Block11, ?USTAR_DEVMIN, ?USTAR_DEVMIN_LEN,
|
|
Devmin, ?PAX_NONE, Pax9),
|
|
{Block13, Pax11} = set_path(Block12, Pax10),
|
|
PaxEntry = case maps:size(Pax11) of
|
|
0 -> [];
|
|
_ -> build_pax_entry(Header, Pax11, Opts)
|
|
end,
|
|
Block14 = set_format(Block13, ?FORMAT_USTAR),
|
|
Block15 = set_checksum(Block14),
|
|
{ok, [PaxEntry, Block15]}.
|
|
|
|
set_path(Block0, Pax) ->
|
|
%% only use ustar header when name is too long
|
|
case maps:get(?PAX_PATH, Pax, nil) of
|
|
nil ->
|
|
{Block0, Pax};
|
|
PaxPath ->
|
|
case split_ustar_path(PaxPath) of
|
|
{ok, UstarName, UstarPrefix} ->
|
|
{Block1, _} = write_string(Block0, ?V7_NAME, ?V7_NAME_LEN,
|
|
UstarName, ?PAX_NONE, #{}),
|
|
{Block2, _} = write_string(Block1, ?USTAR_PREFIX, ?USTAR_PREFIX_LEN,
|
|
UstarPrefix, ?PAX_NONE, #{}),
|
|
{Block2, maps:remove(?PAX_PATH, Pax)};
|
|
false ->
|
|
{Block0, Pax}
|
|
end
|
|
end.
|
|
|
|
set_format(Block0, Format)
|
|
when Format =:= ?FORMAT_USTAR; Format =:= ?FORMAT_PAX ->
|
|
Block1 = write_to_block(Block0, ?MAGIC_USTAR, ?USTAR_MAGIC),
|
|
write_to_block(Block1, ?VERSION_USTAR, ?USTAR_VERSION);
|
|
set_format(_Block, Format) ->
|
|
throw({error, {invalid_format, Format}}).
|
|
|
|
set_checksum(Block) ->
|
|
Checksum = compute_checksum(Block),
|
|
write_octal(Block, ?V7_CHKSUM, ?V7_CHKSUM_LEN, Checksum).
|
|
|
|
build_pax_entry(Header, PaxAttrs, Opts) ->
|
|
Path = Header#tar_header.name,
|
|
Filename = filename:basename(Path),
|
|
Dir = filename:dirname(Path),
|
|
Path2 = filename:join([Dir, "PaxHeaders.0", Filename]),
|
|
AsciiPath = to_ascii(Path2),
|
|
Path3 = if byte_size(AsciiPath) > ?V7_NAME_LEN ->
|
|
binary_part(AsciiPath, 0, ?V7_NAME_LEN - 1);
|
|
true ->
|
|
AsciiPath
|
|
end,
|
|
Keys = maps:keys(PaxAttrs),
|
|
SortedKeys = lists:sort(Keys),
|
|
PaxFile = build_pax_file(SortedKeys, PaxAttrs),
|
|
Size = byte_size(PaxFile),
|
|
Padding = (?BLOCK_SIZE -
|
|
(byte_size(PaxFile) rem ?BLOCK_SIZE)) rem ?BLOCK_SIZE,
|
|
Pad = <<0:Padding/unit:8>>,
|
|
PaxHeader = #tar_header{
|
|
name=unicode:characters_to_list(Path3),
|
|
size=Size,
|
|
mtime=Header#tar_header.mtime,
|
|
atime=Header#tar_header.atime,
|
|
ctime=Header#tar_header.ctime,
|
|
typeflag=?TYPE_X_HEADER
|
|
},
|
|
{ok, PaxHeaderData} = build_header(PaxHeader, Opts),
|
|
[PaxHeaderData, PaxFile, Pad].
|
|
|
|
build_pax_file(Keys, PaxAttrs) ->
|
|
build_pax_file(Keys, PaxAttrs, []).
|
|
build_pax_file([], _, Acc) ->
|
|
unicode:characters_to_binary(Acc);
|
|
build_pax_file([K|Rest], Attrs, Acc) ->
|
|
V = maps:get(K, Attrs),
|
|
Size = sizeof(K) + sizeof(V) + 3,
|
|
Size2 = sizeof(Size) + Size,
|
|
Key = to_string(K),
|
|
Value = to_string(V),
|
|
Record = unicode:characters_to_binary(io_lib:format("~B ~ts=~ts\n", [Size2, Key, Value])),
|
|
if byte_size(Record) =/= Size2 ->
|
|
Size3 = byte_size(Record),
|
|
Record2 = io_lib:format("~B ~ts=~ts\n", [Size3, Key, Value]),
|
|
build_pax_file(Rest, Attrs, [Acc, Record2]);
|
|
true ->
|
|
build_pax_file(Rest, Attrs, [Acc, Record])
|
|
end.
|
|
|
|
sizeof(Bin) when is_binary(Bin) ->
|
|
byte_size(Bin);
|
|
sizeof(List) when is_list(List) ->
|
|
length(List);
|
|
sizeof(N) when is_integer(N) ->
|
|
byte_size(integer_to_binary(N));
|
|
sizeof(N) when is_float(N) ->
|
|
byte_size(float_to_binary(N)).
|
|
|
|
to_string(Bin) when is_binary(Bin) ->
|
|
unicode:characters_to_list(Bin);
|
|
to_string(List) when is_list(List) ->
|
|
List;
|
|
to_string(N) when is_integer(N) ->
|
|
integer_to_list(N);
|
|
to_string(N) when is_float(N) ->
|
|
float_to_list(N).
|
|
|
|
split_ustar_path(Path) ->
|
|
Len = length(Path),
|
|
NotAscii = not is_ascii(Path),
|
|
if Len =< ?V7_NAME_LEN; NotAscii ->
|
|
false;
|
|
true ->
|
|
PathBin = binary:list_to_bin(Path),
|
|
case binary:split(PathBin, [<<$/>>], [global, trim_all]) of
|
|
[Part] when byte_size(Part) >= ?V7_NAME_LEN ->
|
|
false;
|
|
Parts ->
|
|
case lists:last(Parts) of
|
|
Name when byte_size(Name) >= ?V7_NAME_LEN ->
|
|
false;
|
|
Name ->
|
|
Parts2 = lists:sublist(Parts, length(Parts) - 1),
|
|
join_split_ustar_path(Parts2, {ok, Name, nil})
|
|
end
|
|
end
|
|
end.
|
|
|
|
join_split_ustar_path([], Acc) ->
|
|
Acc;
|
|
join_split_ustar_path([Part|_], {ok, _, nil})
|
|
when byte_size(Part) > ?USTAR_PREFIX_LEN ->
|
|
false;
|
|
join_split_ustar_path([Part|_], {ok, _Name, Acc})
|
|
when (byte_size(Part)+byte_size(Acc)) > ?USTAR_PREFIX_LEN ->
|
|
false;
|
|
join_split_ustar_path([Part|Rest], {ok, Name, nil}) ->
|
|
join_split_ustar_path(Rest, {ok, Name, Part});
|
|
join_split_ustar_path([Part|Rest], {ok, Name, Acc}) ->
|
|
join_split_ustar_path(Rest, {ok, Name, <<Acc/binary,$/,Part/binary>>}).
|
|
|
|
write_octal(Block, Pos, Size, X) ->
|
|
Octal = zero_pad(format_octal(X), Size-1),
|
|
if byte_size(Octal) < Size ->
|
|
write_to_block(Block, Octal, Pos);
|
|
true ->
|
|
throw({error, {write_failed, octal_field_too_long}})
|
|
end.
|
|
|
|
write_string(Block, Pos, Size, Str, PaxAttr, Pax0) ->
|
|
NotAscii = not is_ascii(Str),
|
|
if PaxAttr =/= ?PAX_NONE andalso (length(Str) > Size orelse NotAscii) ->
|
|
Pax1 = maps:put(PaxAttr, Str, Pax0),
|
|
{Block, Pax1};
|
|
true ->
|
|
Formatted = format_string(Str, Size),
|
|
{write_to_block(Block, Formatted, Pos), Pax0}
|
|
end.
|
|
write_numeric(Block, Pos, Size, X, PaxAttr, Pax0) ->
|
|
%% attempt octal
|
|
Octal = zero_pad(format_octal(X), Size-1),
|
|
if byte_size(Octal) < Size ->
|
|
{write_to_block(Block, [Octal, 0], Pos), Pax0};
|
|
PaxAttr =/= ?PAX_NONE ->
|
|
Pax1 = maps:put(PaxAttr, X, Pax0),
|
|
{Block, Pax1};
|
|
true ->
|
|
throw({error, {write_failed, numeric_field_too_long}})
|
|
end.
|
|
|
|
zero_pad(Str, Size) when byte_size(Str) >= Size ->
|
|
Str;
|
|
zero_pad(Str, Size) ->
|
|
Padding = Size - byte_size(Str),
|
|
Pad = binary:copy(<<$0>>, Padding),
|
|
<<Pad/binary, Str/binary>>.
|
|
|
|
|
|
%%%================================================================
|
|
%% Functions for creating or modifying tar archives
|
|
|
|
read_block(Reader) ->
|
|
case do_read(Reader, ?BLOCK_SIZE) of
|
|
eof ->
|
|
throw({error, eof});
|
|
%% Two zero blocks mark the end of the archive
|
|
{ok, ?ZERO_BLOCK, Reader1} ->
|
|
case do_read(Reader1, ?BLOCK_SIZE) of
|
|
eof ->
|
|
% This is technically a malformed end-of-archive marker,
|
|
% as two ZERO_BLOCKs are expected as the marker,
|
|
% but if we've already made it this far, we should just ignore it
|
|
eof;
|
|
{ok, ?ZERO_BLOCK, _Reader2} ->
|
|
eof;
|
|
{ok, _Block, _Reader2} ->
|
|
throw({error, invalid_end_of_archive});
|
|
{error,_} = Err ->
|
|
throw(Err)
|
|
end;
|
|
{ok, Block, Reader1} when is_binary(Block) ->
|
|
{ok, Block, Reader1};
|
|
{error, _} = Err ->
|
|
throw(Err)
|
|
end.
|
|
|
|
get_header(#reader{}=Reader) ->
|
|
case read_block(Reader) of
|
|
eof ->
|
|
eof;
|
|
{ok, Block, Reader1} ->
|
|
convert_header(Block, Reader1)
|
|
end.
|
|
|
|
%% Converts the tar header to a record.
|
|
to_v7(Bin) when is_binary(Bin), byte_size(Bin) =:= ?BLOCK_SIZE ->
|
|
#header_v7{
|
|
name=binary_part(Bin, ?V7_NAME, ?V7_NAME_LEN),
|
|
mode=binary_part(Bin, ?V7_MODE, ?V7_MODE_LEN),
|
|
uid=binary_part(Bin, ?V7_UID, ?V7_UID_LEN),
|
|
gid=binary_part(Bin, ?V7_GID, ?V7_GID_LEN),
|
|
size=binary_part(Bin, ?V7_SIZE, ?V7_SIZE_LEN),
|
|
mtime=binary_part(Bin, ?V7_MTIME, ?V7_MTIME_LEN),
|
|
checksum=binary_part(Bin, ?V7_CHKSUM, ?V7_CHKSUM_LEN),
|
|
typeflag=binary:at(Bin, ?V7_TYPE),
|
|
linkname=binary_part(Bin, ?V7_LINKNAME, ?V7_LINKNAME_LEN)
|
|
};
|
|
to_v7(_) ->
|
|
{error, header_block_too_small}.
|
|
|
|
to_gnu(#header_v7{}=V7, Bin)
|
|
when is_binary(Bin), byte_size(Bin) =:= ?BLOCK_SIZE ->
|
|
#header_gnu{
|
|
header_v7=V7,
|
|
magic=binary_part(Bin, ?GNU_MAGIC, ?GNU_MAGIC_LEN),
|
|
version=binary_part(Bin, ?GNU_VERSION, ?GNU_VERSION_LEN),
|
|
uname=binary_part(Bin, 265, 32),
|
|
gname=binary_part(Bin, 297, 32),
|
|
devmajor=binary_part(Bin, 329, 8),
|
|
devminor=binary_part(Bin, 337, 8),
|
|
atime=binary_part(Bin, 345, 12),
|
|
ctime=binary_part(Bin, 357, 12),
|
|
sparse=to_sparse_array(binary_part(Bin, 386, 24*4+1)),
|
|
real_size=binary_part(Bin, 483, 12)
|
|
}.
|
|
|
|
to_star(#header_v7{}=V7, Bin)
|
|
when is_binary(Bin), byte_size(Bin) =:= ?BLOCK_SIZE ->
|
|
#header_star{
|
|
header_v7=V7,
|
|
magic=binary_part(Bin, ?USTAR_MAGIC, ?USTAR_MAGIC_LEN),
|
|
version=binary_part(Bin, ?USTAR_VERSION, ?USTAR_VERSION_LEN),
|
|
uname=binary_part(Bin, ?USTAR_UNAME, ?USTAR_UNAME_LEN),
|
|
gname=binary_part(Bin, ?USTAR_GNAME, ?USTAR_GNAME_LEN),
|
|
devmajor=binary_part(Bin, ?USTAR_DEVMAJ, ?USTAR_DEVMAJ_LEN),
|
|
devminor=binary_part(Bin, ?USTAR_DEVMIN, ?USTAR_DEVMIN_LEN),
|
|
prefix=binary_part(Bin, 345, 131),
|
|
atime=binary_part(Bin, 476, 12),
|
|
ctime=binary_part(Bin, 488, 12),
|
|
trailer=binary_part(Bin, ?STAR_TRAILER, ?STAR_TRAILER_LEN)
|
|
}.
|
|
|
|
to_ustar(#header_v7{}=V7, Bin)
|
|
when is_binary(Bin), byte_size(Bin) =:= ?BLOCK_SIZE ->
|
|
#header_ustar{
|
|
header_v7=V7,
|
|
magic=binary_part(Bin, ?USTAR_MAGIC, ?USTAR_MAGIC_LEN),
|
|
version=binary_part(Bin, ?USTAR_VERSION, ?USTAR_VERSION_LEN),
|
|
uname=binary_part(Bin, ?USTAR_UNAME, ?USTAR_UNAME_LEN),
|
|
gname=binary_part(Bin, ?USTAR_GNAME, ?USTAR_GNAME_LEN),
|
|
devmajor=binary_part(Bin, ?USTAR_DEVMAJ, ?USTAR_DEVMAJ_LEN),
|
|
devminor=binary_part(Bin, ?USTAR_DEVMIN, ?USTAR_DEVMIN_LEN),
|
|
prefix=binary_part(Bin, 345, 155)
|
|
}.
|
|
|
|
to_sparse_array(Bin) when is_binary(Bin) ->
|
|
MaxEntries = byte_size(Bin) div 24,
|
|
IsExtended = 1 =:= binary:at(Bin, 24*MaxEntries),
|
|
Entries = parse_sparse_entries(Bin, MaxEntries-1, []),
|
|
#sparse_array{
|
|
entries=Entries,
|
|
max_entries=MaxEntries,
|
|
is_extended=IsExtended
|
|
}.
|
|
|
|
parse_sparse_entries(<<>>, _, Acc) ->
|
|
Acc;
|
|
parse_sparse_entries(_, -1, Acc) ->
|
|
Acc;
|
|
parse_sparse_entries(Bin, N, Acc) ->
|
|
case to_sparse_entry(binary_part(Bin, N*24, 24)) of
|
|
nil ->
|
|
parse_sparse_entries(Bin, N-1, Acc);
|
|
Entry = #sparse_entry{} ->
|
|
parse_sparse_entries(Bin, N-1, [Entry|Acc])
|
|
end.
|
|
|
|
-define(EMPTY_ENTRY, <<0,0,0,0,0,0,0,0,0,0,0,0>>).
|
|
to_sparse_entry(Bin) when is_binary(Bin), byte_size(Bin) =:= 24 ->
|
|
OffsetBin = binary_part(Bin, 0, 12),
|
|
NumBytesBin = binary_part(Bin, 12, 12),
|
|
case {OffsetBin, NumBytesBin} of
|
|
{?EMPTY_ENTRY, ?EMPTY_ENTRY} ->
|
|
nil;
|
|
_ ->
|
|
#sparse_entry{
|
|
offset=parse_numeric(OffsetBin),
|
|
num_bytes=parse_numeric(NumBytesBin)}
|
|
end.
|
|
|
|
-spec get_format(binary()) -> {ok, pos_integer(), header_v7()}
|
|
| ?FORMAT_UNKNOWN
|
|
| {error, term()}.
|
|
get_format(Bin) when is_binary(Bin), byte_size(Bin) =:= ?BLOCK_SIZE ->
|
|
do_get_format(to_v7(Bin), Bin).
|
|
|
|
do_get_format({error, _} = Err, _Bin) ->
|
|
Err;
|
|
do_get_format(#header_v7{}=V7, Bin)
|
|
when is_binary(Bin), byte_size(Bin) =:= ?BLOCK_SIZE ->
|
|
Checksum = parse_octal(V7#header_v7.checksum),
|
|
Chk1 = compute_checksum(Bin),
|
|
Chk2 = compute_signed_checksum(Bin),
|
|
if Checksum =/= Chk1 andalso Checksum =/= Chk2 ->
|
|
?FORMAT_UNKNOWN;
|
|
true ->
|
|
%% guess magic
|
|
Ustar = to_ustar(V7, Bin),
|
|
Star = to_star(V7, Bin),
|
|
Magic = Ustar#header_ustar.magic,
|
|
Version = Ustar#header_ustar.version,
|
|
Trailer = Star#header_star.trailer,
|
|
Format = if
|
|
Magic =:= ?MAGIC_USTAR, Trailer =:= ?TRAILER_STAR ->
|
|
?FORMAT_STAR;
|
|
Magic =:= ?MAGIC_USTAR ->
|
|
?FORMAT_USTAR;
|
|
Magic =:= ?MAGIC_GNU, Version =:= ?VERSION_GNU ->
|
|
?FORMAT_GNU;
|
|
true ->
|
|
?FORMAT_V7
|
|
end,
|
|
{ok, Format, V7}
|
|
end.
|
|
|
|
unpack_format(Format, #header_v7{}=V7, Bin, Reader)
|
|
when is_binary(Bin), byte_size(Bin) =:= ?BLOCK_SIZE ->
|
|
Mtime = parse_numeric(V7#header_v7.mtime),
|
|
Header0 = #tar_header{
|
|
name=parse_string(V7#header_v7.name),
|
|
mode=parse_numeric(V7#header_v7.mode),
|
|
uid=parse_numeric(V7#header_v7.uid),
|
|
gid=parse_numeric(V7#header_v7.gid),
|
|
size=parse_numeric(V7#header_v7.size),
|
|
mtime=Mtime,
|
|
atime=Mtime,
|
|
ctime=Mtime,
|
|
typeflag=V7#header_v7.typeflag,
|
|
linkname=parse_string(V7#header_v7.linkname)
|
|
},
|
|
Typeflag = Header0#tar_header.typeflag,
|
|
Header1 = if Format > ?FORMAT_V7 ->
|
|
unpack_modern(Format, V7, Bin, Header0);
|
|
true ->
|
|
Name = Header0#tar_header.name,
|
|
Header0#tar_header{name=safe_join_path("", Name)}
|
|
end,
|
|
HeaderOnly = is_header_only_type(Typeflag),
|
|
Header2 = if HeaderOnly ->
|
|
Header1#tar_header{size=0};
|
|
true ->
|
|
Header1
|
|
end,
|
|
if Typeflag =:= ?TYPE_GNU_SPARSE ->
|
|
Gnu = to_gnu(V7, Bin),
|
|
RealSize = parse_numeric(Gnu#header_gnu.real_size),
|
|
{Sparsemap, Reader2} = parse_sparse_map(Gnu, Reader),
|
|
Header3 = Header2#tar_header{size=RealSize},
|
|
{Header3, new_sparse_file_reader(Reader2, Sparsemap, RealSize)};
|
|
true ->
|
|
FileReader = #reg_file_reader{
|
|
handle=Reader,
|
|
num_bytes=Header2#tar_header.size,
|
|
size=Header2#tar_header.size,
|
|
pos = 0
|
|
},
|
|
{Header2, FileReader}
|
|
end.
|
|
|
|
unpack_modern(Format, #header_v7{}=V7, Bin, #tar_header{}=Header0)
|
|
when is_binary(Bin) ->
|
|
Typeflag = Header0#tar_header.typeflag,
|
|
Ustar = to_ustar(V7, Bin),
|
|
H0 = Header0#tar_header{
|
|
uname=parse_string(Ustar#header_ustar.uname),
|
|
gname=parse_string(Ustar#header_ustar.gname)},
|
|
H1 = if Typeflag =:= ?TYPE_CHAR
|
|
orelse Typeflag =:= ?TYPE_BLOCK ->
|
|
Ma = parse_numeric(Ustar#header_ustar.devmajor),
|
|
Mi = parse_numeric(Ustar#header_ustar.devminor),
|
|
H0#tar_header{
|
|
devmajor=Ma,
|
|
devminor=Mi
|
|
};
|
|
true ->
|
|
H0
|
|
end,
|
|
{Prefix, H2} = case Format of
|
|
?FORMAT_USTAR ->
|
|
{parse_string(Ustar#header_ustar.prefix), H1};
|
|
?FORMAT_STAR ->
|
|
Star = to_star(V7, Bin),
|
|
Prefix0 = parse_string(Star#header_star.prefix),
|
|
Atime0 = Star#header_star.atime,
|
|
Atime = parse_numeric(Atime0),
|
|
Ctime0 = Star#header_star.ctime,
|
|
Ctime = parse_numeric(Ctime0),
|
|
{Prefix0, H1#tar_header{
|
|
atime=Atime,
|
|
ctime=Ctime
|
|
}};
|
|
_ ->
|
|
{"", H1}
|
|
end,
|
|
Name = H2#tar_header.name,
|
|
H2#tar_header{name=safe_join_path(Prefix, Name)}.
|
|
|
|
|
|
safe_join_path([], Name) ->
|
|
filename:join([Name]);
|
|
safe_join_path(Prefix, []) ->
|
|
filename:join([Prefix]);
|
|
safe_join_path(Prefix, Name) ->
|
|
filename:join(Prefix, Name).
|
|
|
|
new_sparse_file_reader(Reader, Sparsemap, RealSize) ->
|
|
true = validate_sparse_entries(Sparsemap, RealSize),
|
|
#sparse_file_reader{
|
|
handle = Reader,
|
|
num_bytes = RealSize,
|
|
pos = 0,
|
|
size = RealSize,
|
|
sparse_map = Sparsemap}.
|
|
|
|
validate_sparse_entries(Entries, RealSize) ->
|
|
validate_sparse_entries(Entries, RealSize, 0, 0).
|
|
validate_sparse_entries([], _RealSize, _I, _LastOffset) ->
|
|
true;
|
|
validate_sparse_entries([#sparse_entry{}=Entry|Rest], RealSize, I, LastOffset) ->
|
|
Offset = Entry#sparse_entry.offset,
|
|
NumBytes = Entry#sparse_entry.num_bytes,
|
|
if
|
|
Offset > ?MAX_INT64-NumBytes ->
|
|
throw({error, {invalid_sparse_map_entry, offset_too_large}});
|
|
Offset+NumBytes > RealSize ->
|
|
throw({error, {invalid_sparse_map_entry, offset_too_large}});
|
|
I > 0 andalso LastOffset > Offset ->
|
|
throw({error, {invalid_sparse_map_entry, overlapping_offsets}});
|
|
true ->
|
|
ok
|
|
end,
|
|
validate_sparse_entries(Rest, RealSize, I+1, Offset+NumBytes).
|
|
|
|
|
|
-spec parse_sparse_map(header_gnu(), reader_type()) ->
|
|
{[sparse_entry()], reader_type()}.
|
|
parse_sparse_map(#header_gnu{sparse=Sparse}, Reader)
|
|
when Sparse#sparse_array.is_extended ->
|
|
parse_sparse_map(Sparse, Reader, []);
|
|
parse_sparse_map(#header_gnu{sparse=Sparse}, Reader) ->
|
|
{Sparse#sparse_array.entries, Reader}.
|
|
parse_sparse_map(#sparse_array{is_extended=true,entries=Entries}, Reader, Acc) ->
|
|
case read_block(Reader) of
|
|
eof ->
|
|
throw({error, eof});
|
|
{ok, Block, Reader2} ->
|
|
Sparse2 = to_sparse_array(Block),
|
|
parse_sparse_map(Sparse2, Reader2, Entries++Acc)
|
|
end;
|
|
parse_sparse_map(#sparse_array{entries=Entries}, Reader, Acc) ->
|
|
Sorted = lists:sort(fun (#sparse_entry{offset=A},#sparse_entry{offset=B}) ->
|
|
A =< B
|
|
end, Entries++Acc),
|
|
{Sorted, Reader}.
|
|
|
|
%% Defined by taking the sum of the unsigned byte values of the
|
|
%% entire header record, treating the checksum bytes to as ASCII spaces
|
|
compute_checksum(<<H1:?V7_CHKSUM/binary,
|
|
H2:?V7_CHKSUM_LEN/binary,
|
|
Rest:(?BLOCK_SIZE - ?V7_CHKSUM - ?V7_CHKSUM_LEN)/binary,
|
|
_/binary>>) ->
|
|
C0 = checksum(H1) + (byte_size(H2) * $\s),
|
|
C1 = checksum(Rest),
|
|
C0 + C1.
|
|
|
|
compute_signed_checksum(<<H1:?V7_CHKSUM/binary,
|
|
H2:?V7_CHKSUM_LEN/binary,
|
|
Rest:(?BLOCK_SIZE - ?V7_CHKSUM - ?V7_CHKSUM_LEN)/binary,
|
|
_/binary>>) ->
|
|
C0 = signed_checksum(H1) + (byte_size(H2) * $\s),
|
|
C1 = signed_checksum(Rest),
|
|
C0 + C1.
|
|
|
|
%% Returns the checksum of a binary.
|
|
checksum(Bin) -> checksum(Bin, 0).
|
|
checksum(<<A/unsigned,Rest/binary>>, Sum) ->
|
|
checksum(Rest, Sum+A);
|
|
checksum(<<>>, Sum) -> Sum.
|
|
|
|
signed_checksum(Bin) -> signed_checksum(Bin, 0).
|
|
signed_checksum(<<A/signed,Rest/binary>>, Sum) ->
|
|
signed_checksum(Rest, Sum+A);
|
|
signed_checksum(<<>>, Sum) -> Sum.
|
|
|
|
-spec parse_numeric(binary()) -> non_neg_integer().
|
|
parse_numeric(<<>>) ->
|
|
0;
|
|
parse_numeric(<<First, _/binary>> = Bin) ->
|
|
%% check for base-256 format first
|
|
%% if the bit is set, then all following bits constitute a two's
|
|
%% complement encoded number in big-endian byte order
|
|
if
|
|
First band 16#80 =/= 0 ->
|
|
%% Handling negative numbers relies on the following identity:
|
|
%% -a-1 == ^a
|
|
%% If the number is negative, we use an inversion mask to invert
|
|
%% the data bytes and treat the value as an unsigned number
|
|
Inv = if First band 16#40 =/= 0 -> 16#00; true -> 16#FF end,
|
|
Bytes = binary:bin_to_list(Bin),
|
|
Reducer = fun (C, {I, X}) ->
|
|
C1 = C bxor Inv,
|
|
C2 = if I =:= 0 -> C1 band 16#7F; true -> C1 end,
|
|
if (X bsr 56) > 0 ->
|
|
throw({error,integer_overflow});
|
|
true ->
|
|
{I+1, (X bsl 8) bor C2}
|
|
end
|
|
end,
|
|
{_, N} = lists:foldl(Reducer, {0,0}, Bytes),
|
|
if (N bsr 63) > 0 ->
|
|
throw({error, integer_overflow});
|
|
true ->
|
|
if Inv =:= 16#FF ->
|
|
-1 bxor N;
|
|
true ->
|
|
N
|
|
end
|
|
end;
|
|
true ->
|
|
%% normal case is an octal number
|
|
parse_octal(Bin)
|
|
end.
|
|
|
|
parse_octal(Bin) when is_binary(Bin) ->
|
|
%% skip leading/trailing zero bytes and spaces
|
|
do_parse_octal(Bin, <<>>).
|
|
do_parse_octal(<<>>, <<>>) ->
|
|
0;
|
|
do_parse_octal(<<>>, Acc) ->
|
|
case io_lib:fread("~8u", binary:bin_to_list(Acc)) of
|
|
{error, _} -> throw({error, invalid_tar_checksum});
|
|
{ok, [Octal], []} -> Octal;
|
|
{ok, _, _} -> throw({error, invalid_tar_checksum})
|
|
end;
|
|
do_parse_octal(<<$\s,Rest/binary>>, Acc) ->
|
|
do_parse_octal(Rest, Acc);
|
|
do_parse_octal(<<0, Rest/binary>>, Acc) ->
|
|
do_parse_octal(Rest, Acc);
|
|
do_parse_octal(<<C, Rest/binary>>, Acc) ->
|
|
do_parse_octal(Rest, <<Acc/binary, C>>).
|
|
|
|
parse_string(Bin) when is_binary(Bin) ->
|
|
do_parse_string(Bin, <<>>).
|
|
do_parse_string(<<>>, Acc) ->
|
|
case unicode:characters_to_list(Acc) of
|
|
Str when is_list(Str) ->
|
|
Str;
|
|
{incomplete, _Str, _Rest} ->
|
|
binary:bin_to_list(Acc);
|
|
{error, _Str, _Rest} ->
|
|
throw({error, {bad_header, invalid_string}})
|
|
end;
|
|
do_parse_string(<<0, _/binary>>, Acc) ->
|
|
do_parse_string(<<>>, Acc);
|
|
do_parse_string(<<C, Rest/binary>>, Acc) ->
|
|
do_parse_string(Rest, <<Acc/binary, C>>).
|
|
|
|
convert_header(Bin, #reader{pos=Pos}=Reader)
|
|
when byte_size(Bin) =:= ?BLOCK_SIZE, (Pos rem ?BLOCK_SIZE) =:= 0 ->
|
|
case get_format(Bin) of
|
|
?FORMAT_UNKNOWN ->
|
|
throw({error, bad_header});
|
|
{ok, Format, V7} ->
|
|
unpack_format(Format, V7, Bin, Reader);
|
|
{error, Reason} ->
|
|
throw({error, {bad_header, Reason}})
|
|
end;
|
|
convert_header(Bin, #reader{pos=Pos}) when byte_size(Bin) =:= ?BLOCK_SIZE ->
|
|
throw({error, misaligned_read, Pos});
|
|
convert_header(Bin, _Reader) when byte_size(Bin) =:= 0 ->
|
|
eof;
|
|
convert_header(_Bin, _Reader) ->
|
|
throw({error, eof}).
|
|
|
|
%% Creates a partially-populated header record based
|
|
%% on the provided file_info record. If the file is
|
|
%% a symlink, then `link` is used as the link target.
|
|
%% If the file is a directory, a slash is appended to the name.
|
|
fileinfo_to_header(Name, #file_info{}=Fi, Link) when is_list(Name) ->
|
|
BaseHeader = #tar_header{name=Name,
|
|
mtime=0,
|
|
atime=0,
|
|
ctime=0,
|
|
mode=Fi#file_info.mode,
|
|
typeflag=?TYPE_REGULAR},
|
|
do_fileinfo_to_header(BaseHeader, Fi, Link).
|
|
|
|
do_fileinfo_to_header(Header, #file_info{size=Size,type=regular}, _Link) ->
|
|
Header#tar_header{size=Size,typeflag=?TYPE_REGULAR};
|
|
do_fileinfo_to_header(#tar_header{name=Name}=Header,
|
|
#file_info{type=directory}, _Link) ->
|
|
Header#tar_header{name=Name++"/",typeflag=?TYPE_DIR};
|
|
do_fileinfo_to_header(Header, #file_info{type=symlink}, Link) ->
|
|
Header#tar_header{typeflag=?TYPE_SYMLINK,linkname=Link};
|
|
do_fileinfo_to_header(Header, #file_info{type=device,mode=Mode}=Fi, _Link)
|
|
when (Mode band ?S_IFMT) =:= ?S_IFCHR ->
|
|
Header#tar_header{typeflag=?TYPE_CHAR,
|
|
devmajor=Fi#file_info.major_device,
|
|
devminor=Fi#file_info.minor_device};
|
|
do_fileinfo_to_header(Header, #file_info{type=device,mode=Mode}=Fi, _Link)
|
|
when (Mode band ?S_IFMT) =:= ?S_IFBLK ->
|
|
Header#tar_header{typeflag=?TYPE_BLOCK,
|
|
devmajor=Fi#file_info.major_device,
|
|
devminor=Fi#file_info.minor_device};
|
|
do_fileinfo_to_header(Header, #file_info{type=other,mode=Mode}, _Link)
|
|
when (Mode band ?S_IFMT) =:= ?S_FIFO ->
|
|
Header#tar_header{typeflag=?TYPE_FIFO};
|
|
do_fileinfo_to_header(Header, Fi, _Link) ->
|
|
{error, {invalid_file_type, Header#tar_header.name, Fi}}.
|
|
|
|
is_ascii(Str) when is_list(Str) ->
|
|
not lists:any(fun (Char) -> Char >= 16#80 end, Str);
|
|
is_ascii(Bin) when is_binary(Bin) ->
|
|
is_ascii1(Bin).
|
|
|
|
is_ascii1(<<>>) ->
|
|
true;
|
|
is_ascii1(<<C,_Rest/binary>>) when C >= 16#80 ->
|
|
false;
|
|
is_ascii1(<<_, Rest/binary>>) ->
|
|
is_ascii1(Rest).
|
|
|
|
to_ascii(Str) when is_list(Str) ->
|
|
case is_ascii(Str) of
|
|
true ->
|
|
unicode:characters_to_binary(Str);
|
|
false ->
|
|
Chars = lists:filter(fun (Char) -> Char < 16#80 end, Str),
|
|
unicode:characters_to_binary(Chars)
|
|
end;
|
|
to_ascii(Bin) when is_binary(Bin) ->
|
|
to_ascii(Bin, <<>>).
|
|
to_ascii(<<>>, Acc) ->
|
|
Acc;
|
|
to_ascii(<<C, Rest/binary>>, Acc) when C < 16#80 ->
|
|
to_ascii(Rest, <<Acc/binary,C>>);
|
|
to_ascii(<<_, Rest/binary>>, Acc) ->
|
|
to_ascii(Rest, Acc).
|
|
|
|
is_header_only_type(?TYPE_SYMLINK) -> true;
|
|
is_header_only_type(?TYPE_LINK) -> true;
|
|
is_header_only_type(?TYPE_DIR) -> true;
|
|
is_header_only_type(_) -> false.
|
|
|
|
foldl_read(#reader{access=read}=Reader, Fun, Accu, #read_opts{}=Opts)
|
|
when is_function(Fun,4) ->
|
|
case foldl_read0(Reader, Fun, Accu, Opts) of
|
|
{ok, Result, _Reader2} ->
|
|
Result;
|
|
{error, _} = Err ->
|
|
Err
|
|
end;
|
|
foldl_read(#reader{access=Access}, _Fun, _Accu, _Opts) ->
|
|
{error, {read_mode_expected, Access}};
|
|
foldl_read(TarName, Fun, Accu, #read_opts{}=Opts)
|
|
when is_function(Fun,4) ->
|
|
try open(TarName, [read|Opts#read_opts.open_mode]) of
|
|
{ok, #reader{access=read}=Reader} ->
|
|
try
|
|
foldl_read(Reader, Fun, Accu, Opts)
|
|
after
|
|
_ = close(Reader)
|
|
end;
|
|
{error, _} = Err ->
|
|
Err
|
|
catch
|
|
throw:Err ->
|
|
Err
|
|
end.
|
|
|
|
foldl_read0(Reader, Fun, Accu, Opts) ->
|
|
try foldl_read1(Fun, Accu, Reader, Opts, #{}) of
|
|
{ok,_,_} = Ok ->
|
|
Ok
|
|
catch
|
|
throw:{error, {Reason, Format, Args}} ->
|
|
read_verbose(Opts, Format, Args),
|
|
{error, Reason};
|
|
throw:Err ->
|
|
Err
|
|
end.
|
|
|
|
foldl_read1(Fun, Accu0, Reader0, Opts, ExtraHeaders) ->
|
|
{ok, Reader1} = skip_unread(Reader0),
|
|
case get_header(Reader1) of
|
|
eof ->
|
|
Fun(eof, Reader1, Opts, Accu0);
|
|
{Header, Reader2} ->
|
|
case Header#tar_header.typeflag of
|
|
?TYPE_X_HEADER ->
|
|
{ExtraHeaders2, Reader3} = parse_pax(Reader2),
|
|
ExtraHeaders3 = maps:merge(ExtraHeaders, ExtraHeaders2),
|
|
foldl_read1(Fun, Accu0, Reader3, Opts, ExtraHeaders3);
|
|
?TYPE_GNU_LONGNAME ->
|
|
{RealName, Reader3} = get_real_name(Reader2),
|
|
ExtraHeaders2 = maps:put(?PAX_PATH,
|
|
parse_string(RealName), ExtraHeaders),
|
|
foldl_read1(Fun, Accu0, Reader3, Opts, ExtraHeaders2);
|
|
?TYPE_GNU_LONGLINK ->
|
|
{RealName, Reader3} = get_real_name(Reader2),
|
|
ExtraHeaders2 = maps:put(?PAX_LINKPATH,
|
|
parse_string(RealName), ExtraHeaders),
|
|
foldl_read1(Fun, Accu0, Reader3, Opts, ExtraHeaders2);
|
|
_ ->
|
|
Header1 = merge_pax(Header, ExtraHeaders),
|
|
{ok, NewAccu, Reader3} = Fun(Header1, Reader2, Opts, Accu0),
|
|
foldl_read1(Fun, NewAccu, Reader3, Opts, #{})
|
|
end
|
|
end.
|
|
|
|
%% Applies all known PAX attributes to the current tar header
|
|
-spec merge_pax(tar_header(), #{binary() => binary()}) -> tar_header().
|
|
merge_pax(Header, ExtraHeaders) when is_map(ExtraHeaders) ->
|
|
do_merge_pax(Header, maps:to_list(ExtraHeaders)).
|
|
|
|
do_merge_pax(Header, []) ->
|
|
Header;
|
|
do_merge_pax(Header, [{?PAX_PATH, Path}|Rest]) ->
|
|
do_merge_pax(Header#tar_header{name=unicode:characters_to_list(Path)}, Rest);
|
|
do_merge_pax(Header, [{?PAX_LINKPATH, LinkPath}|Rest]) ->
|
|
do_merge_pax(Header#tar_header{linkname=unicode:characters_to_list(LinkPath)}, Rest);
|
|
do_merge_pax(Header, [{?PAX_GNAME, Gname}|Rest]) ->
|
|
do_merge_pax(Header#tar_header{gname=unicode:characters_to_list(Gname)}, Rest);
|
|
do_merge_pax(Header, [{?PAX_UNAME, Uname}|Rest]) ->
|
|
do_merge_pax(Header#tar_header{uname=unicode:characters_to_list(Uname)}, Rest);
|
|
do_merge_pax(Header, [{?PAX_UID, Uid}|Rest]) ->
|
|
Uid2 = binary_to_integer(Uid),
|
|
do_merge_pax(Header#tar_header{uid=Uid2}, Rest);
|
|
do_merge_pax(Header, [{?PAX_GID, Gid}|Rest]) ->
|
|
Gid2 = binary_to_integer(Gid),
|
|
do_merge_pax(Header#tar_header{gid=Gid2}, Rest);
|
|
do_merge_pax(Header, [{?PAX_ATIME, Atime}|Rest]) ->
|
|
Atime2 = parse_pax_time(Atime),
|
|
do_merge_pax(Header#tar_header{atime=Atime2}, Rest);
|
|
do_merge_pax(Header, [{?PAX_MTIME, Mtime}|Rest]) ->
|
|
Mtime2 = parse_pax_time(Mtime),
|
|
do_merge_pax(Header#tar_header{mtime=Mtime2}, Rest);
|
|
do_merge_pax(Header, [{?PAX_CTIME, Ctime}|Rest]) ->
|
|
Ctime2 = parse_pax_time(Ctime),
|
|
do_merge_pax(Header#tar_header{ctime=Ctime2}, Rest);
|
|
do_merge_pax(Header, [{?PAX_SIZE, Size}|Rest]) ->
|
|
Size2 = binary_to_integer(Size),
|
|
do_merge_pax(Header#tar_header{size=Size2}, Rest);
|
|
do_merge_pax(Header, [{<<?PAX_XATTR_STR, _Key/binary>>, _Value}|Rest]) ->
|
|
do_merge_pax(Header, Rest);
|
|
do_merge_pax(Header, [_Ignore|Rest]) ->
|
|
do_merge_pax(Header, Rest).
|
|
|
|
%% Returns the time since UNIX epoch as a datetime
|
|
-spec parse_pax_time(binary()) -> tar_time().
|
|
parse_pax_time(Bin) when is_binary(Bin) ->
|
|
TotalNano = case binary:split(Bin, [<<$.>>]) of
|
|
[SecondsStr, NanoStr0] ->
|
|
Seconds = binary_to_integer(SecondsStr),
|
|
if byte_size(NanoStr0) < ?MAX_NANO_INT_SIZE ->
|
|
%% right pad
|
|
PaddingN = ?MAX_NANO_INT_SIZE-byte_size(NanoStr0),
|
|
Padding = binary:copy(<<$0>>, PaddingN),
|
|
NanoStr1 = <<NanoStr0/binary,Padding/binary>>,
|
|
Nano = binary_to_integer(NanoStr1),
|
|
(Seconds*?BILLION)+Nano;
|
|
byte_size(NanoStr0) > ?MAX_NANO_INT_SIZE ->
|
|
%% right truncate
|
|
NanoStr1 = binary_part(NanoStr0, 0, ?MAX_NANO_INT_SIZE),
|
|
Nano = binary_to_integer(NanoStr1),
|
|
(Seconds*?BILLION)+Nano;
|
|
true ->
|
|
(Seconds*?BILLION)+binary_to_integer(NanoStr0)
|
|
end;
|
|
[SecondsStr] ->
|
|
binary_to_integer(SecondsStr)*?BILLION
|
|
end,
|
|
%% truncate to microseconds
|
|
Micro = TotalNano div 1000,
|
|
Mega = Micro div 1000000000000,
|
|
Secs = Micro div 1000000 - (Mega*1000000),
|
|
Secs.
|
|
|
|
%% Given a regular file reader, reads the whole file and
|
|
%% parses all extended attributes it contains.
|
|
parse_pax(#reg_file_reader{handle=Handle,num_bytes=0}) ->
|
|
{#{}, Handle};
|
|
parse_pax(#reg_file_reader{handle=Handle0,num_bytes=NumBytes}) ->
|
|
case do_read(Handle0, NumBytes) of
|
|
{ok, Bytes, Handle1} ->
|
|
do_parse_pax(Handle1, Bytes, #{});
|
|
{error, _} = Err ->
|
|
throw(Err)
|
|
end.
|
|
|
|
do_parse_pax(Reader, <<>>, Headers) ->
|
|
{Headers, Reader};
|
|
do_parse_pax(Reader, Bin, Headers) ->
|
|
{Key, Value, Residual} = parse_pax_record(Bin),
|
|
NewHeaders = maps:put(Key, Value, Headers),
|
|
do_parse_pax(Reader, Residual, NewHeaders).
|
|
|
|
%% Parse an extended attribute
|
|
parse_pax_record(Bin) when is_binary(Bin) ->
|
|
case binary:split(Bin, [<<$\n>>]) of
|
|
[Record, Residual] ->
|
|
case [X || X <- binary:split(Record, [<<$\s>>], [global]), X =/= <<>>] of
|
|
[_Len, Record1] ->
|
|
case [X || X <- binary:split(Record1, [<<$=>>], [global]), X =/= <<>>] of
|
|
[AttrName, AttrValue] ->
|
|
{AttrName, AttrValue, Residual};
|
|
_Other ->
|
|
throw({error, malformed_pax_record})
|
|
end;
|
|
_Other ->
|
|
throw({error, malformed_pax_record})
|
|
end;
|
|
_Other ->
|
|
throw({error, malformed_pax_record})
|
|
end.
|
|
|
|
get_real_name(#reg_file_reader{handle=Handle,num_bytes=0}) ->
|
|
{"", Handle};
|
|
get_real_name(#reg_file_reader{handle=Handle0,num_bytes=NumBytes}) ->
|
|
case do_read(Handle0, NumBytes) of
|
|
{ok, RealName, Handle1} ->
|
|
{RealName, Handle1};
|
|
{error, _} = Err ->
|
|
throw(Err)
|
|
end;
|
|
get_real_name(#sparse_file_reader{num_bytes=NumBytes}=Reader0) ->
|
|
case do_read(Reader0, NumBytes) of
|
|
{ok, RealName, Reader1} ->
|
|
{RealName, Reader1};
|
|
{error, _} = Err ->
|
|
throw(Err)
|
|
end.
|
|
|
|
%% Skip the remaining bytes for the current file entry
|
|
skip_file(#reg_file_reader{handle=Handle0,pos=Pos,size=Size}=Reader) ->
|
|
Padding = skip_padding(Size),
|
|
AbsPos = Handle0#reader.pos + (Size-Pos) + Padding,
|
|
case do_position(Handle0, AbsPos) of
|
|
{ok, _, Handle1} ->
|
|
Reader#reg_file_reader{handle=Handle1,num_bytes=0,pos=Size};
|
|
Err ->
|
|
throw(Err)
|
|
end;
|
|
skip_file(#sparse_file_reader{pos=Pos,size=Size}=Reader) ->
|
|
case do_read(Reader, Size-Pos) of
|
|
{ok, _, Reader2} ->
|
|
Reader2;
|
|
Err ->
|
|
throw(Err)
|
|
end.
|
|
|
|
skip_padding(0) ->
|
|
0;
|
|
skip_padding(Size) when (Size rem ?BLOCK_SIZE) =:= 0 ->
|
|
0;
|
|
skip_padding(Size) when Size =< ?BLOCK_SIZE ->
|
|
?BLOCK_SIZE - Size;
|
|
skip_padding(Size) ->
|
|
?BLOCK_SIZE - (Size rem ?BLOCK_SIZE).
|
|
|
|
skip_unread(#reader{pos=Pos}=Reader0) when (Pos rem ?BLOCK_SIZE) > 0 ->
|
|
Padding = skip_padding(Pos + ?BLOCK_SIZE),
|
|
AbsPos = Pos + Padding,
|
|
case do_position(Reader0, AbsPos) of
|
|
{ok, _, Reader1} ->
|
|
{ok, Reader1};
|
|
Err ->
|
|
throw(Err)
|
|
end;
|
|
skip_unread(#reader{}=Reader) ->
|
|
{ok, Reader};
|
|
skip_unread(#reg_file_reader{handle=Handle,num_bytes=0}) ->
|
|
skip_unread(Handle);
|
|
skip_unread(#reg_file_reader{}=Reader) ->
|
|
#reg_file_reader{handle=Handle} = skip_file(Reader),
|
|
{ok, Handle};
|
|
skip_unread(#sparse_file_reader{handle=Handle,num_bytes=0}) ->
|
|
skip_unread(Handle);
|
|
skip_unread(#sparse_file_reader{}=Reader) ->
|
|
#sparse_file_reader{handle=Handle} = skip_file(Reader),
|
|
{ok, Handle}.
|
|
|
|
write_extracted_element(#tar_header{name=Name,typeflag=Type},
|
|
Bin,
|
|
#read_opts{output=memory}=Opts) ->
|
|
case typeflag(Type) of
|
|
regular ->
|
|
read_verbose(Opts, "x ~ts~n", [Name]),
|
|
{ok, {Name, Bin}};
|
|
_ ->
|
|
ok
|
|
end;
|
|
write_extracted_element(#tar_header{name=Name0}=Header, Bin, Opts) ->
|
|
Name1 = make_safe_path(Name0, Opts),
|
|
Created =
|
|
case typeflag(Header#tar_header.typeflag) of
|
|
regular ->
|
|
create_regular(Name1, Name0, Bin, Opts);
|
|
directory ->
|
|
read_verbose(Opts, "x ~ts~n", [Name0]),
|
|
create_extracted_dir(Name1, Opts);
|
|
symlink ->
|
|
read_verbose(Opts, "x ~ts~n", [Name0]),
|
|
create_symlink(Name1, Header#tar_header.linkname, Opts);
|
|
Device when Device =:= char orelse Device =:= block ->
|
|
%% char/block devices will be created as empty files
|
|
%% and then have their major/minor device set later
|
|
create_regular(Name1, Name0, <<>>, Opts);
|
|
fifo ->
|
|
%% fifo devices will be created as empty files
|
|
create_regular(Name1, Name0, <<>>, Opts);
|
|
Other -> % Ignore.
|
|
read_verbose(Opts, "x ~ts - unsupported type ~p~n",
|
|
[Name0, Other]),
|
|
not_written
|
|
end,
|
|
case Created of
|
|
ok -> set_extracted_file_info(Name1, Header);
|
|
not_written -> ok
|
|
end.
|
|
|
|
make_safe_path([$/|Path], Opts) ->
|
|
make_safe_path(Path, Opts);
|
|
make_safe_path(Path, #read_opts{cwd=Cwd}) ->
|
|
case safe_relative_path_links(Path, Cwd) of
|
|
unsafe ->
|
|
throw({error,{Path,unsafe_path}});
|
|
SafePath ->
|
|
filename:absname(SafePath, Cwd)
|
|
end.
|
|
|
|
safe_relative_path_links(Path, Cwd) ->
|
|
case filename:pathtype(Path) of
|
|
relative -> safe_relative_path_links(filename:split(Path), Cwd, [], "");
|
|
_ -> unsafe
|
|
end.
|
|
|
|
safe_relative_path_links([], _Cwd, _PrevLinks, Acc) ->
|
|
Acc;
|
|
|
|
safe_relative_path_links([Segment | Segments], Cwd, PrevLinks, Acc) ->
|
|
AccSegment = join(Acc, Segment),
|
|
|
|
case r3_hex_filename:safe_relative_path(AccSegment) of
|
|
unsafe ->
|
|
unsafe;
|
|
|
|
SafeAccSegment ->
|
|
case file:read_link(join(Cwd, SafeAccSegment)) of
|
|
{ok, LinkPath} ->
|
|
case lists:member(LinkPath, PrevLinks) of
|
|
true ->
|
|
unsafe;
|
|
false ->
|
|
case safe_relative_path_links(filename:split(LinkPath), Cwd, [LinkPath | PrevLinks], Acc) of
|
|
unsafe -> unsafe;
|
|
NewAcc -> safe_relative_path_links(Segments, Cwd, [], NewAcc)
|
|
end
|
|
end;
|
|
|
|
{error, _} ->
|
|
safe_relative_path_links(Segments, Cwd, PrevLinks, SafeAccSegment)
|
|
end
|
|
end.
|
|
|
|
join([], Path) -> Path;
|
|
join(Left, Right) -> filename:join(Left, Right).
|
|
|
|
create_regular(Name, NameInArchive, Bin, Opts) ->
|
|
case write_extracted_file(Name, Bin, Opts) of
|
|
not_written ->
|
|
read_verbose(Opts, "x ~ts - exists, not created~n", [NameInArchive]),
|
|
not_written;
|
|
Ok ->
|
|
read_verbose(Opts, "x ~ts~n", [NameInArchive]),
|
|
Ok
|
|
end.
|
|
|
|
create_extracted_dir(Name, _Opts) ->
|
|
case file:make_dir(Name) of
|
|
ok -> ok;
|
|
{error,enotsup} -> not_written;
|
|
{error,eexist} -> not_written;
|
|
{error,enoent} -> make_dirs(Name, dir);
|
|
{error,Reason} -> throw({error, Reason})
|
|
end.
|
|
|
|
create_symlink(Name, Linkname, Opts) ->
|
|
case file:make_symlink(Linkname, Name) of
|
|
ok -> ok;
|
|
{error,enoent} ->
|
|
ok = make_dirs(Name, file),
|
|
create_symlink(Name, Linkname, Opts);
|
|
{error,eexist} -> not_written;
|
|
{error,enotsup} ->
|
|
read_verbose(Opts, "x ~ts - symbolic links not supported~n", [Name]),
|
|
not_written;
|
|
{error,Reason} -> throw({error, Reason})
|
|
end.
|
|
|
|
write_extracted_file(Name, Bin, Opts) ->
|
|
Write =
|
|
case Opts#read_opts.keep_old_files of
|
|
true ->
|
|
case file:read_file_info(Name) of
|
|
{ok, _} -> false;
|
|
_ -> true
|
|
end;
|
|
false -> true
|
|
end,
|
|
case Write of
|
|
true -> write_file(Name, Bin);
|
|
false -> not_written
|
|
end.
|
|
|
|
write_file(Name, Bin) ->
|
|
case file:write_file(Name, Bin) of
|
|
ok -> ok;
|
|
{error,enoent} ->
|
|
case make_dirs(Name, file) of
|
|
ok ->
|
|
write_file(Name, Bin);
|
|
{error,Reason} ->
|
|
throw({error, Reason})
|
|
end;
|
|
{error,Reason} ->
|
|
throw({error, Reason})
|
|
end.
|
|
|
|
set_extracted_file_info(_, #tar_header{typeflag = ?TYPE_SYMLINK}) -> ok;
|
|
set_extracted_file_info(_, #tar_header{typeflag = ?TYPE_LINK}) -> ok;
|
|
set_extracted_file_info(Name, #tar_header{typeflag = ?TYPE_CHAR}=Header) ->
|
|
set_device_info(Name, Header);
|
|
set_extracted_file_info(Name, #tar_header{typeflag = ?TYPE_BLOCK}=Header) ->
|
|
set_device_info(Name, Header);
|
|
set_extracted_file_info(Name, #tar_header{mtime=Mtime,mode=Mode}) ->
|
|
Info = #file_info{mode=Mode, mtime=Mtime},
|
|
file:write_file_info(Name, Info, [{time, posix}]).
|
|
|
|
set_device_info(Name, #tar_header{}=Header) ->
|
|
Mtime = Header#tar_header.mtime,
|
|
Mode = Header#tar_header.mode,
|
|
Devmajor = Header#tar_header.devmajor,
|
|
Devminor = Header#tar_header.devminor,
|
|
Info = #file_info{
|
|
mode=Mode,
|
|
mtime=Mtime,
|
|
major_device=Devmajor,
|
|
minor_device=Devminor
|
|
},
|
|
file:write_file_info(Name, Info).
|
|
|
|
%% Makes all directories leading up to the file.
|
|
|
|
make_dirs(Name, file) ->
|
|
filelib:ensure_dir(Name);
|
|
make_dirs(Name, dir) ->
|
|
filelib:ensure_dir(filename:join(Name,"*")).
|
|
|
|
%% Prints the message on if the verbose option is given (for reading).
|
|
read_verbose(#read_opts{verbose=true}, Format, Args) ->
|
|
io:format(Format, Args);
|
|
read_verbose(_, _, _) ->
|
|
ok.
|
|
|
|
%% Prints the message on if the verbose option is given.
|
|
add_verbose(#add_opts{verbose=true}, Format, Args) ->
|
|
io:format(Format, Args);
|
|
add_verbose(_, _, _) ->
|
|
ok.
|
|
|
|
%%%%%%%%%%%%%%%%%%
|
|
%% I/O primitives
|
|
%%%%%%%%%%%%%%%%%%
|
|
|
|
do_write(#reader{handle=Handle,func=Fun}=Reader0, Data)
|
|
when is_function(Fun,2) ->
|
|
case Fun(write,{Handle,Data}) of
|
|
ok ->
|
|
{ok, Pos, Reader1} = do_position(Reader0, {cur,0}),
|
|
{ok, Reader1#reader{pos=Pos}};
|
|
{error, _} = Err ->
|
|
Err
|
|
end.
|
|
|
|
do_copy(#reader{func=Fun}=Reader, Source, #add_opts{chunk_size=0}=Opts)
|
|
when is_function(Fun, 2) ->
|
|
do_copy(Reader, Source, Opts#add_opts{chunk_size=65536});
|
|
do_copy(#reader{func=Fun}=Reader, Source, #add_opts{chunk_size=ChunkSize})
|
|
when is_function(Fun, 2) ->
|
|
case file:open(Source, [read, binary]) of
|
|
{ok, SourceFd} ->
|
|
case copy_chunked(Reader, SourceFd, ChunkSize, 0) of
|
|
{ok, _Copied, _Reader2} = Ok->
|
|
_ = file:close(SourceFd),
|
|
Ok;
|
|
Err ->
|
|
_ = file:close(SourceFd),
|
|
throw(Err)
|
|
end;
|
|
Err ->
|
|
throw(Err)
|
|
end.
|
|
|
|
copy_chunked(#reader{}=Reader, Source, ChunkSize, Copied) ->
|
|
case file:read(Source, ChunkSize) of
|
|
{ok, Bin} ->
|
|
{ok, Reader2} = do_write(Reader, Bin),
|
|
copy_chunked(Reader2, Source, ChunkSize, Copied+byte_size(Bin));
|
|
eof ->
|
|
{ok, Copied, Reader};
|
|
Other ->
|
|
Other
|
|
end.
|
|
|
|
|
|
do_position(#reader{handle=Handle,func=Fun}=Reader, Pos)
|
|
when is_function(Fun,2)->
|
|
case Fun(position, {Handle,Pos}) of
|
|
{ok, NewPos} ->
|
|
%% since Pos may not always be an absolute seek,
|
|
%% make sure we update the reader with the new absolute position
|
|
{ok, AbsPos} = Fun(position, {Handle, {cur, 0}}),
|
|
{ok, NewPos, Reader#reader{pos=AbsPos}};
|
|
Other ->
|
|
Other
|
|
end.
|
|
|
|
do_read(#reg_file_reader{handle=Handle,pos=Pos,size=Size}=Reader, Len) ->
|
|
NumBytes = Size - Pos,
|
|
ActualLen = if NumBytes - Len < 0 -> NumBytes; true -> Len end,
|
|
case do_read(Handle, ActualLen) of
|
|
{ok, Bin, Handle2} ->
|
|
NewPos = Pos + ActualLen,
|
|
NumBytes2 = Size - NewPos,
|
|
Reader1 = Reader#reg_file_reader{
|
|
handle=Handle2,
|
|
pos=NewPos,
|
|
num_bytes=NumBytes2},
|
|
{ok, Bin, Reader1};
|
|
Other ->
|
|
Other
|
|
end;
|
|
do_read(#sparse_file_reader{}=Reader, Len) ->
|
|
do_sparse_read(Reader, Len);
|
|
do_read(#reader{pos=Pos,handle=Handle,func=Fun}=Reader, Len)
|
|
when is_function(Fun,2)->
|
|
%% Always convert to binary internally
|
|
case Fun(read2,{Handle,Len}) of
|
|
{ok, List} when is_list(List) ->
|
|
Bin = list_to_binary(List),
|
|
NewPos = Pos+byte_size(Bin),
|
|
{ok, Bin, Reader#reader{pos=NewPos}};
|
|
{ok, Bin} when is_binary(Bin) ->
|
|
NewPos = Pos+byte_size(Bin),
|
|
{ok, Bin, Reader#reader{pos=NewPos}};
|
|
Other ->
|
|
Other
|
|
end.
|
|
|
|
|
|
do_sparse_read(Reader, Len) ->
|
|
do_sparse_read(Reader, Len, <<>>).
|
|
|
|
do_sparse_read(#sparse_file_reader{sparse_map=[#sparse_entry{num_bytes=0}|Entries]
|
|
}=Reader0, Len, Acc) ->
|
|
%% skip all empty fragments
|
|
Reader1 = Reader0#sparse_file_reader{sparse_map=Entries},
|
|
do_sparse_read(Reader1, Len, Acc);
|
|
do_sparse_read(#sparse_file_reader{sparse_map=[],
|
|
pos=Pos,size=Size}=Reader0, Len, Acc)
|
|
when Pos < Size ->
|
|
%% if there are no more fragments, it is possible that there is one last sparse hole
|
|
%% this behaviour matches the BSD tar utility
|
|
%% however, GNU tar stops returning data even if we haven't reached the end
|
|
{ok, Bin, Reader1} = read_sparse_hole(Reader0, Size, Len),
|
|
do_sparse_read(Reader1, Len-byte_size(Bin), <<Acc/binary,Bin/binary>>);
|
|
do_sparse_read(#sparse_file_reader{sparse_map=[]}=Reader, _Len, Acc) ->
|
|
{ok, Acc, Reader};
|
|
do_sparse_read(#sparse_file_reader{}=Reader, 0, Acc) ->
|
|
{ok, Acc, Reader};
|
|
do_sparse_read(#sparse_file_reader{sparse_map=[#sparse_entry{offset=Offset}|_],
|
|
pos=Pos}=Reader0, Len, Acc)
|
|
when Pos < Offset ->
|
|
{ok, Bin, Reader1} = read_sparse_hole(Reader0, Offset, Offset-Pos),
|
|
do_sparse_read(Reader1, Len-byte_size(Bin), <<Acc/binary,Bin/binary>>);
|
|
do_sparse_read(#sparse_file_reader{sparse_map=[Entry|Entries],
|
|
pos=Pos}=Reader0, Len, Acc) ->
|
|
%% we're in a data fragment, so read from it
|
|
%% end offset of fragment
|
|
EndPos = Entry#sparse_entry.offset + Entry#sparse_entry.num_bytes,
|
|
%% bytes left in fragment
|
|
NumBytes = EndPos - Pos,
|
|
ActualLen = if Len > NumBytes -> NumBytes; true -> Len end,
|
|
case do_read(Reader0#sparse_file_reader.handle, ActualLen) of
|
|
{ok, Bin, Handle} ->
|
|
BytesRead = byte_size(Bin),
|
|
ActualEndPos = Pos+BytesRead,
|
|
Reader1 = if ActualEndPos =:= EndPos ->
|
|
Reader0#sparse_file_reader{sparse_map=Entries};
|
|
true ->
|
|
Reader0
|
|
end,
|
|
Size = Reader1#sparse_file_reader.size,
|
|
NumBytes2 = Size - ActualEndPos,
|
|
Reader2 = Reader1#sparse_file_reader{
|
|
handle=Handle,
|
|
pos=ActualEndPos,
|
|
num_bytes=NumBytes2},
|
|
do_sparse_read(Reader2, Len-byte_size(Bin), <<Acc/binary,Bin/binary>>);
|
|
Other ->
|
|
Other
|
|
end.
|
|
|
|
%% Reads a sparse hole ending at Offset
|
|
read_sparse_hole(#sparse_file_reader{pos=Pos}=Reader, Offset, Len) ->
|
|
N = Offset - Pos,
|
|
N2 = if N > Len ->
|
|
Len;
|
|
true ->
|
|
N
|
|
end,
|
|
Bin = <<0:N2/unit:8>>,
|
|
NumBytes = Reader#sparse_file_reader.size - (Pos+N2),
|
|
{ok, Bin, Reader#sparse_file_reader{
|
|
num_bytes=NumBytes,
|
|
pos=Pos+N2}}.
|
|
|
|
-spec do_close(reader()) -> ok | {error, term()}.
|
|
do_close(#reader{handle=Handle,func=Fun}) when is_function(Fun,2) ->
|
|
Fun(close,Handle).
|
|
|
|
%%%%%%%%%%%%%%%%%%
|
|
%% Option parsing
|
|
%%%%%%%%%%%%%%%%%%
|
|
|
|
extract_opts(List) ->
|
|
extract_opts(List, default_options()).
|
|
|
|
table_opts(List) ->
|
|
read_opts(List, default_options()).
|
|
|
|
default_options() ->
|
|
{ok, Cwd} = file:get_cwd(),
|
|
#read_opts{cwd=Cwd}.
|
|
|
|
extract_opts([keep_old_files|Rest], Opts) ->
|
|
extract_opts(Rest, Opts#read_opts{keep_old_files=true});
|
|
extract_opts([{cwd, Cwd}|Rest], Opts) ->
|
|
extract_opts(Rest, Opts#read_opts{cwd=Cwd});
|
|
extract_opts([{files, Files}|Rest], Opts) ->
|
|
Set = ordsets:from_list(Files),
|
|
extract_opts(Rest, Opts#read_opts{files=Set});
|
|
extract_opts([memory|Rest], Opts) ->
|
|
extract_opts(Rest, Opts#read_opts{output=memory});
|
|
extract_opts([compressed|Rest], Opts=#read_opts{open_mode=OpenMode}) ->
|
|
extract_opts(Rest, Opts#read_opts{open_mode=[compressed|OpenMode]});
|
|
extract_opts([cooked|Rest], Opts=#read_opts{open_mode=OpenMode}) ->
|
|
extract_opts(Rest, Opts#read_opts{open_mode=[cooked|OpenMode]});
|
|
extract_opts([verbose|Rest], Opts) ->
|
|
extract_opts(Rest, Opts#read_opts{verbose=true});
|
|
extract_opts([Other|Rest], Opts) ->
|
|
extract_opts(Rest, read_opts([Other], Opts));
|
|
extract_opts([], Opts) ->
|
|
Opts.
|
|
|
|
read_opts([compressed|Rest], Opts=#read_opts{open_mode=OpenMode}) ->
|
|
read_opts(Rest, Opts#read_opts{open_mode=[compressed|OpenMode]});
|
|
read_opts([cooked|Rest], Opts=#read_opts{open_mode=OpenMode}) ->
|
|
read_opts(Rest, Opts#read_opts{open_mode=[cooked|OpenMode]});
|
|
read_opts([verbose|Rest], Opts) ->
|
|
read_opts(Rest, Opts#read_opts{verbose=true});
|
|
read_opts([_|Rest], Opts) ->
|
|
read_opts(Rest, Opts);
|
|
read_opts([], Opts) ->
|
|
Opts.
|