%%
%% Copyright (c) dushin.net
%% 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.
%%
%%-----------------------------------------------------------------------------
%% @doc A library used to generate an
%% <a href="http://github.com/bettio/AtomVM">AtomVM</a> AVM file from a set of
%% files (beam files, previously built AVM files, or even arbitrary data files).
%% @end
%%-----------------------------------------------------------------------------
-module(packbeam_api).
%% API exports
-export([create/2, create/3, create/4, create/5, list/1, extract/3, delete/3]).
%% AVM Entry functions
-export([is_beam/1, is_entrypoint/1, get_element_name/1, get_element_data/1, get_element_module/1]).
-define(AVM_HEADER,
16#23, 16#21, 16#2f, 16#75,
16#73, 16#72, 16#2f, 16#62,
16#69, 16#6e, 16#2f, 16#65,
16#6e, 16#76, 16#20, 16#41,
16#74, 16#6f, 16#6d, 16#56,
16#4d, 16#0a, 16#00, 16#00
).
-define(ALLOWED_CHUNKS, [
"AtU8", "Code", "ExpT", "LocT", "ImpT", "LitU", "FunT", "StrT", "LitT"
]).
-define(BEAM_START_FLAG, 1).
-define(BEAM_CODE_FLAG, 2).
-define(NORMAL_FILE_FLAG, 4).
-opaque avm_element() :: [atom() | {atom(), term()}].
-type path() :: string().
-type avm_element_name() :: string().
-type options() :: #{
prune => boolean(),
start_module => module() | undefined,
application_module => module() | undefined,
include_lines => boolean()
}.
-export_type([
path/0,
avm_element/0,
avm_element_name/0,
options/0
]).
-define(DEFAULT_OPTIONS, #{
prune => false,
start_module => undefined,
application_module => undefined,
include_lines => true
}).
%%
%% Public API
%%
%%-----------------------------------------------------------------------------
%% @param OutputPath the path to write the AVM file
%% @param InputPaths a list of paths of beam files, AVM files, or normal data files
%% @returns ok if the file was created.
%% @throws Reason::string()
%% @doc Create an AVM file.
%%
%% Equivalent to `create(OutputPath, InputPaths, DefaultOptions)'
%%
%% where `DefaultOptions' is `#{
%% prune => false,
%% start_module => undefined,
%% application_module => undefined,
%% include_lines => false
%% }'
%%
%% @end
%%-----------------------------------------------------------------------------
-spec create(
OutputPath :: path(),
InputPaths :: [path()]
) -> ok | {error, Reason :: term()}.
create(OutputPath, InputPaths) ->
create(OutputPath, InputPaths, ?DEFAULT_OPTIONS).
%%-----------------------------------------------------------------------------
%% @param OutputPath the path to write the AVM file
%% @param InputPaths a list of paths of beam files, AVM files, or normal data files
%% @param Options creation options
%% @returns ok if the file was created.
%% @throws Reason::string()
%% @doc Create an AVM file.
%%
%% This function will create an AVM file at the location specified in
%% OutputPath, using the input files specified in InputPaths.
%% @end
%%-----------------------------------------------------------------------------
-spec create(
OutputPath :: path(),
InputPaths :: [path()],
Options :: options()
) -> ok | {error, Reason :: term()}.
create(OutputPath, InputPaths, Options) ->
#{
prune := Prune,
start_module := StartModule,
application_module := ApplicationModule,
include_lines := IncludeLines
} = Options,
ParsedFiles = parse_files(InputPaths, StartModule, IncludeLines),
write_packbeam(
OutputPath,
case Prune of
true -> prune(ParsedFiles, ApplicationModule);
_ -> ParsedFiles
end
).
%%-----------------------------------------------------------------------------
%% @param OutputPath the path to write the AVM file
%% @param InputPaths a list of paths of beam files, AVM files, or normal data files
%% @param Prune whether to prune the archive. Without pruning, all found BEAM files are included
%% in the output AVM file. With pruning, then packbeam will attempt to
%% determine which BEAM files are needed to run the application, depending
%% on which modules are (transitively) referenced from the AVM entrypoint.
%% @param StartModule if `undefined', then this parameter is a module that
%% is intended to the the start module for the application. This module
%% will occur first in the generated AVM file.
%% @returns ok if the file was created.
%% @throws Reason::string()
%% @deprecated This function is deprecated. Use `create/3' instead.
%% @doc Create an AVM file.
%%
%% Equivalent to create(OutputPath, InputPaths, undefined, Prune, StartModule).
%% @end
%% @hidden
%%-----------------------------------------------------------------------------
-spec create(
OutputPath :: path(),
InputPaths :: [path()],
Prune :: boolean(),
StartModule :: module() | undefined
) ->
ok | {error, Reason :: term()}.
create(OutputPath, InputPaths, Prune, StartModule) ->
io:format("WARNING: Deprecated function: ~p:create/4~n", [?MODULE]),
Options = #{prune => Prune, start_module => StartModule},
create(OutputPath, InputPaths, maps:merge(?DEFAULT_OPTIONS, Options)).
%%-----------------------------------------------------------------------------
%% @param OutputPath the path to write the AVM file
%% @param InputPaths a list of paths of beam files, AVM files, or normal data files
%% @param ApplicationModule If not `undefined', then this parameter designates
%% the name of an OTP application, from which additional dependencies
%% will be computed.
%% @param Prune whether to prune the archive. Without pruning, all found BEAM files are included
%% in the output AVM file. With pruning, then packbeam will attempt to
%% determine which BEAM files are needed to run the application, depending
%% on which modules are (transitively) referenced from the AVM entrypoint.
%% @param StartModule if `undefined', then this parameter is a module that
%% is intended to the the start module for the application. This module
%% will occur first in the generated AVM file.
%% @returns ok if the file was created.
%% @throws Reason::string()
%% @deprecated This function is deprecated. Use `create/3' instead.
%% @doc Create an AVM file.
%%
%% This function will create an AVM file at the location specified in
%% OutputPath, using the input files specified in InputPaths.
%% @end
%% @hidden
%%-----------------------------------------------------------------------------
-spec create(
OutputPath :: path(),
InputPaths :: [path()],
ApplicationModule :: module() | undefined,
Prune :: boolean(),
StartModule :: module() | undefined
) ->
ok | {error, Reason :: term()}.
create(OutputPath, InputPaths, ApplicationModule, Prune, StartModule) ->
io:format("WARNING: Deprecated function: ~p:create/5~n", [?MODULE]),
Options = #{prune => Prune, start_module => StartModule, application_module => ApplicationModule},
create(OutputPath, InputPaths, maps:merge(?DEFAULT_OPTIONS, Options)).
%%-----------------------------------------------------------------------------
%% @param InputPath the AVM file from which to list elements
%% @returns list of element data
%% @throws Reason::string()
%% @doc List the contents of an AVM file.
%%
%% This function will list the contents of an AVM file at the
%% location specified in InputPath.
%% @end
%%-----------------------------------------------------------------------------
-spec list(InputPath :: path()) -> [avm_element()].
list(InputPath) ->
case file_type(InputPath) of
avm ->
parse_file(InputPath, false);
_ ->
throw(io_lib:format("Expected AVM file: ~p", [InputPath]))
end.
%%-----------------------------------------------------------------------------
%% @param InputPath the AVM file from which to extract elements
%% @param AVMElementNames a list of elements from the source AVM file to extract. If
%% empty, then extract all elements.
%% @param OutputDir the directory to write the contents
%% @returns ok if the file was created.
%% @throws Reason::string()
%% @doc Extract all or selected elements from an AVM file.
%%
%% This function will extract elements of an AVM file at the location specified in
%% InputPath, specified by the supplied list of names. The elements
%% from the input AVM file will be written into the specified output directory,
%% creating any subdirectories if the AVM file elements contain path information.
%% @end
%%-----------------------------------------------------------------------------
-spec extract(
InputPath :: path(),
AVMElementNames :: [avm_element_name()],
OutputDir :: path()
) -> ok | {error, Reason :: term()}.
extract(InputPath, AVMElementNames, OutputDir) ->
case file_type(InputPath) of
avm ->
ParsedFiles = parse_file(InputPath, false),
write_files(filter_names(AVMElementNames, ParsedFiles), OutputDir);
_ ->
throw(io_lib:format("Expected AVM file: ~p", [InputPath]))
end.
%%-----------------------------------------------------------------------------
%% @param InputPath the AVM file from which to delete elements
%% @param OutputPath the path to write the AVM file
%% @param AVMElementNames a list of elements from the source AVM file to delete
%% @returns ok if the file was created.
%% @throws Reason::string()
%% @doc Delete selected elements of an AVM file.
%%
%% This function will delete elements of an AVM file at the location specified in
%% InputPath, specified by the supplied list of names. The output AVM file
%% is written to OutputPath, which may be the same as InputPath.
%% @end
%%-----------------------------------------------------------------------------
-spec delete(
OutputPath :: path(),
InputPath :: path(),
AVMElementNames :: [avm_element_name()]
) -> ok | {error, Reason :: term()}.
delete(OutputPath, InputPath, AVMElementNames) ->
case file_type(InputPath) of
avm ->
ParsedFiles = parse_file(InputPath, false),
write_packbeam(OutputPath, remove_names(AVMElementNames, ParsedFiles));
_ ->
throw(io_lib:format("Expected AVM file: ~p", [InputPath]))
end.
%%-----------------------------------------------------------------------------
%% @param AVMElement An AVM file element
%% @returns the name of the element.
%% @doc Return the name of the element.
%% @end
%%-----------------------------------------------------------------------------
-spec get_element_name(AVMElement :: avm_element()) -> avm_element_name().
get_element_name(AVMElement) ->
proplists:get_value(element_name, AVMElement).
%%-----------------------------------------------------------------------------
%% @param AVMElement An AVM file element
%% @returns the AVM element data.
%% @doc Return AVM element data.
%% @end
%%-----------------------------------------------------------------------------
-spec get_element_data(AVMElement :: avm_element()) -> binary().
get_element_data(AVMElement) ->
proplists:get_value(data, AVMElement).
%%-----------------------------------------------------------------------------
%% @param AVMElement An AVM file element
%% @returns the AVM element module, if the element is a BEAM file; `undefined',
%% otherwise.
%% @doc Return AVM element module, if the element is a BEAM file.
%% @end
%%-----------------------------------------------------------------------------
-spec get_element_module(AVMElement :: avm_element()) -> module() | undefined.
get_element_module(AVMElement) ->
proplists:get_value(module, AVMElement).
%%-----------------------------------------------------------------------------
%% @param AVMElement An AVM file element
%% @returns `true' if the AVM element is an entrypoint (i.e., exports a `start/0'
%% function); `false' otherwise.
%% @doc Indicates whether the AVM file element is an entrypoint.
%% @end
%%-----------------------------------------------------------------------------
-spec is_entrypoint(AVMElement :: avm_element()) -> boolean().
is_entrypoint(AVMElement) ->
(get_flags(AVMElement) band ?BEAM_START_FLAG) =:= ?BEAM_START_FLAG.
%%-----------------------------------------------------------------------------
%% @param AVMElement An AVM file element
%% @returns `true' if the AVM element is a BEAM file; `false' otherwise.
%% @doc Indicates whether the AVM file element is a BEAM file.
%% @end
%%-----------------------------------------------------------------------------
-spec is_beam(AVMElement :: avm_element()) -> boolean().
is_beam(AVMElement) ->
(get_flags(AVMElement) band ?BEAM_CODE_FLAG) =:= ?BEAM_CODE_FLAG.
%%
%% Internal API functions
%%
%% @private
get_flags(AVMElement) ->
proplists:get_value(flags, AVMElement).
%% @private
parse_files(InputPaths, StartModule, IncludeLines) ->
Files = lists:foldl(
fun(InputPath, Accum) ->
Accum ++ parse_file(InputPath, IncludeLines)
end,
[],
InputPaths
),
case StartModule of
undefined ->
Files;
_ ->
reorder_start_module(StartModule, Files)
end.
%% @private
parse_file({InputPath, ModuleName}, IncludeLines) ->
parse_file(file_type(InputPath), ModuleName, load_file(InputPath), IncludeLines);
parse_file(InputPath, IncludeLines) ->
parse_file(file_type(InputPath), InputPath, load_file(InputPath), IncludeLines).
%% @private
file_type(InputPath) ->
case ends_with(InputPath, ".beam") of
true ->
beam;
_ ->
case ends_with(InputPath, ".avm") of
true -> avm;
_ -> normal
end
end.
%% @private
load_file(Path) ->
case file:read_file(Path) of
{ok, Data} ->
Data;
{error, Reason} ->
throw(io_lib:format("Unable to load file ~s. Reason: ~p", [Path, Reason]))
end.
%% @private
prune(ParsedFiles, RootApplicationModule) ->
case find_entrypoint(ParsedFiles) of
false ->
throw("No input beam files contain start/0 entrypoint");
{value, Entrypoint} ->
BeamFiles = lists:filter(fun is_beam/1, ParsedFiles),
Modules = closure(Entrypoint, BeamFiles, [get_element_module(Entrypoint)]),
ApplicationStartModules = find_application_modules(ParsedFiles, RootApplicationModule),
ApplicationModules = find_dependencies(ApplicationStartModules, BeamFiles),
filter_modules(Modules ++ ApplicationModules, ParsedFiles)
end.
find_application_modules(_ParsedFiles, undefined) ->
[];
find_application_modules(ParsedFiles, ApplicationModule) ->
ApplicationSpecs = find_all_application_specs(ParsedFiles),
find_application_start_modules(ParsedFiles, ApplicationSpecs, ApplicationModule).
find_all_application_specs(ParsedFiles) ->
ApplicationFiles = lists:filter(
fun is_application_file/1,
ParsedFiles
),
[get_application_spec(ApplicationFile) || ApplicationFile <- ApplicationFiles].
find_application_start_modules(ParsedFiles, ApplicationSpecs, ApplicationModule) ->
case find_application_spec(ApplicationSpecs, ApplicationModule) of
false ->
[];
{value, {application, _ApplicationModule, Properties}} ->
ChildApplicationStartModules = case proplists:get_value(applications, Properties) of
Applications when is_list(Applications) ->
lists:foldl(
fun(Application, InnerAccum) ->
find_application_start_modules(ParsedFiles, ApplicationSpecs, Application) ++ InnerAccum
end,
[],
Applications
);
_ ->
[]
end,
StartModules = case proplists:get_value(mod, Properties) of
{StartModule, _Args} when is_atom(StartModule) ->
[StartModule];
_ ->
[]
end,
ChildApplicationStartModules ++ StartModules
end.
find_dependencies(Entrypoints, BeamFiles) ->
deduplicate(
lists:flatten(
[
closure(
get_parsed_file(Entrypoint, BeamFiles),
BeamFiles,
[Entrypoint]
) || Entrypoint <- Entrypoints
]
)
).
find_application_spec(ApplicationSpecs, ApplicationModule) ->
lists:search(
fun({application, Module, _Properties}) ->
Module =:= ApplicationModule
end,
ApplicationSpecs
).
get_application_spec(ApplicationFile) ->
ApplicationData = get_element_data(ApplicationFile),
<<_Size:4/binary, SerializedTerm/binary>> = ApplicationData,
binary_to_term(SerializedTerm).
is_application_file(ParsedFile) ->
case not is_beam(ParsedFile) of
true ->
ModuleName = get_element_name(ParsedFile),
Components = string:split(ModuleName, "/", all),
case Components of
[_ModuleName, "priv", "application.bin"] ->
true;
_ ->
false
end;
false ->
false
end.
%% @private
find_entrypoint(ParsedFiles) ->
lists:search(fun is_entrypoint/1, ParsedFiles).
%% @private
closure(_Current, [], Accum) ->
lists:reverse(Accum);
closure(Current, Candidates, Accum) ->
CandidateModules = get_element_modules(Candidates),
CurrentsImports = get_imports(Current),
CurrentsAtoms = get_atoms(Current),
DepModules = intersection(CurrentsImports ++ CurrentsAtoms, CandidateModules) -- Accum,
lists:foldl(
fun(DepModule, A) ->
case lists:member(DepModule, A) of
true ->
A;
_ ->
NewCandidates = remove(DepModule, Candidates),
closure(get_parsed_file(DepModule, Candidates), NewCandidates, [DepModule | A])
end
end,
Accum,
DepModules
).
%% @private
remove(Module, ParsedFiles) ->
[P || P <- ParsedFiles, Module =/= get_element_module(P)].
%% @private
get_imports(ParsedFile) ->
Imports = proplists:get_value(imports, proplists:get_value(chunk_refs, ParsedFile)),
lists:usort([M || {M, _F, _A} <- Imports]).
%% @private
get_atoms(ParsedFile) ->
AtomsT = [
Atom || {_Index, Atom} <- proplists:get_value(atoms, proplists:get_value(chunk_refs, ParsedFile))
],
AtomsFromLiterals = get_atom_literals(proplists:get_value(uncompressed_literals, ParsedFile)),
lists:usort(AtomsT ++ AtomsFromLiterals).
%% @private
get_atom_literals(undefined) ->
[];
get_atom_literals(UncompressedLiterals) ->
<<NumLiterals:32, Rest/binary>> = UncompressedLiterals,
get_atom_literals(NumLiterals, Rest, []).
%% @private
get_atom_literals(0, <<"">>, Accum) ->
Accum;
get_atom_literals(I, Data, Accum) ->
<<Length:32, StartData/binary>> = Data,
<<EncodedLiteral:Length/binary, Rest/binary>> = StartData,
Literal = binary_to_term(EncodedLiteral),
ExtractedAtoms = extract_atoms(Literal, []),
get_atom_literals(I - 1, Rest, ExtractedAtoms ++ Accum).
%% @private
extract_atoms(Term, Accum) when is_atom(Term) ->
[Term|Accum];
extract_atoms(Term, Accum) when is_tuple(Term) ->
extract_atoms(tuple_to_list(Term), Accum);
extract_atoms(Term, Accum) when is_map(Term) ->
extract_atoms(maps:to_list(Term), Accum);
extract_atoms([H|T], Accum) ->
HeadAtoms = extract_atoms(H, []),
extract_atoms(T, HeadAtoms ++ Accum);
extract_atoms(_Term, Accum) ->
Accum.
%% @private
get_element_modules(ParsedFiles) ->
[get_element_module(ParsedFile) || ParsedFile <- ParsedFiles].
%% @private
get_parsed_file(Module, ParsedFiles) ->
SearchResult = lists:search(
fun(ParsedFile) ->
get_element_module(ParsedFile) =:= Module
end,
ParsedFiles
),
case SearchResult of
{value, V} ->
V;
false ->
exit({module_not_found, Module, ParsedFiles})
end.
%% @private
intersection(A, B) ->
lists:filter(
fun(E) ->
lists:member(E, B)
end,
A
).
%% @private
filter_modules(Modules, ParsedFiles) ->
lists:filter(
fun(ParsedFile) ->
case is_beam(ParsedFile) of
true ->
lists:member(get_element_module(ParsedFile), Modules);
_ ->
true
end
end,
ParsedFiles
).
%% @private
parse_file(beam, _ModuleName, Data, IncludeLines) ->
{ok, Module, Chunks} = beam_lib:all_chunks(Data),
{UncompressedChunks, UncompressedLiterals} = uncompress_literals(Chunks),
FilteredChunks = filter_chunks(UncompressedChunks, IncludeLines),
{ok, Binary} = beam_lib:build_module(FilteredChunks),
{ok, {Module, ChunkRefs}} = beam_lib:chunks(Data, [imports, exports, atoms]),
Exports = proplists:get_value(exports, ChunkRefs),
Flags =
case lists:member({start, 0}, Exports) of
true ->
?BEAM_CODE_FLAG bor ?BEAM_START_FLAG;
_ ->
?BEAM_CODE_FLAG
end,
[
[
{module, Module},
{element_name, io_lib:format("~s.beam", [atom_to_list(Module)])},
{flags, Flags},
{data, Binary},
{chunk_refs, ChunkRefs},
{uncompressed_literals, UncompressedLiterals}
]
];
parse_file(avm, ModuleName, Data, _IncludeLines) ->
case Data of
<<?AVM_HEADER, AVMData/binary>> ->
parse_avm_data(AVMData);
_ ->
throw(io_lib:format("Invalid AVM header: ~p", [ModuleName]))
end;
parse_file(normal, ModuleName, Data, _IncludeLines) ->
DataSize = byte_size(Data),
Blob = <<DataSize:32, Data/binary>>,
[[{element_name, ModuleName}, {flags, ?NORMAL_FILE_FLAG}, {data, Blob}]].
%% @private
reorder_start_module(StartModule, Files) ->
{StartProps, Other} =
lists:partition(
fun(Props) ->
% io:format("Props: ~w~n", [Props]),
case get_element_module(Props) of
StartModule ->
case is_entrypoint(Props) of
true -> true;
_ -> throw({start_module_not_start_beam, StartModule})
end;
_ ->
false
end
end,
Files
),
case StartProps of
[] ->
throw({start_module_not_found, StartModule});
_ ->
StartProps ++ Other
end.
%% @private
parse_avm_data(AVMData) ->
parse_avm_data(AVMData, []).
%% @private
parse_avm_data(<<"">>, Accum) ->
lists:reverse(Accum);
parse_avm_data(<<0:32/integer, _AVMRest/binary>>, Accum) ->
lists:reverse(Accum);
parse_avm_data(<<Size:32/integer, AVMRest/binary>>, Accum) ->
SizeWithoutSizeField = Size - 4,
case SizeWithoutSizeField =< erlang:byte_size(AVMRest) of
true ->
<<AVMBeamData:SizeWithoutSizeField/binary, AVMNext/binary>> = AVMRest,
Beam = parse_beam(AVMBeamData, [], in_header, 0, []),
parse_avm_data(AVMNext, [Beam | Accum]);
_ ->
throw(
io_lib:format("Invalid AVM data size: ~p (AVMRest=~p)", [
Size, erlang:byte_size(AVMRest)
])
)
end.
%% @private
parse_beam(<<Flags:32, _Reserved:32, Rest/binary>>, [], in_header, Pos, Accum) ->
parse_beam(Rest, [], in_element_name, Pos + 8, [{flags, Flags} | Accum]);
parse_beam(<<0:8, Rest/binary>>, Tmp, in_element_name, Pos, Accum) ->
ModuleName = lists:reverse(Tmp),
case (Pos + 1) rem 4 of
0 ->
parse_beam(Rest, Tmp, in_data, Pos, [{element_name, ModuleName} | Accum]);
_ ->
parse_beam(Rest, [], eat_padding, Pos + 1, [{element_name, ModuleName} | Accum])
end;
parse_beam(<<C:8, Rest/binary>>, Tmp, in_element_name, Pos, Accum) ->
parse_beam(Rest, [C | Tmp], in_element_name, Pos + 1, Accum);
parse_beam(<<0:8, Rest/binary>>, Tmp, eat_padding, Pos, Accum) ->
case (Pos + 1) rem 4 of
0 ->
parse_beam(Rest, Tmp, in_data, Pos, Accum);
_ ->
parse_beam(Rest, Tmp, eat_padding, Pos + 1, Accum)
end;
parse_beam(Bin, Tmp, eat_padding, Pos, Accum) ->
parse_beam(Bin, Tmp, in_data, Pos, Accum);
parse_beam(Data, _Tmp, in_data, _Pos, Accum) ->
case is_beam(Accum) orelse is_entrypoint(Accum) of
true ->
StrippedData = strip_padding(Data),
{ok, {Module, ChunkRefs}} = beam_lib:chunks(StrippedData, [imports, exports, atoms]),
[{module, Module}, {chunk_refs, ChunkRefs}, {data, StrippedData} | Accum];
_ ->
[{data, Data} | Accum]
end.
strip_padding(<<0:8, Rest/binary>>) ->
strip_padding(Rest);
strip_padding(Data) ->
Data.
%% @private
uncompress_literals(Chunks) ->
case proplists:get_value("LitT", Chunks) of
undefined ->
{Chunks, undefined};
<<_Header:4/binary, Data/binary>> ->
UncompressedData = zlib:uncompress(Data),
{
lists:keyreplace(
"LitT",
1,
Chunks,
{"LitU", UncompressedData}
),
UncompressedData
}
end.
%% @private
write_packbeam(OutputFilePath, ParsedFiles) ->
PackedData =
[<<?AVM_HEADER>> | [pack_data(ParsedFile) || ParsedFile <- ParsedFiles]] ++
[create_header(0, 0, <<"end">>)],
file:write_file(OutputFilePath, PackedData).
%% @private
pack_data(ParsedFile) ->
ModuleName = list_to_binary(get_element_name(ParsedFile)),
Flags = get_flags(ParsedFile),
Data = get_element_data(ParsedFile),
HeaderSize = header_size(ModuleName),
HeaderPadding = create_padding(HeaderSize),
DataSize = byte_size(Data),
DataPadding = create_padding(DataSize),
Size = HeaderSize + byte_size(HeaderPadding) + DataSize + byte_size(DataPadding),
Header = create_header(Size, Flags, ModuleName),
<<Header/binary, HeaderPadding/binary, Data/binary, DataPadding/binary>>.
%% @private
header_size(ModuleName) ->
4 + 4 + 4 + byte_size(ModuleName) + 1.
%% @private
create_header(Size, Flags, ModuleName) ->
<<Size:32, Flags:32, 0:32, ModuleName/binary, 0:8>>.
%% @private
create_padding(Size) ->
case Size rem 4 of
0 -> <<"">>;
K -> list_to_binary(lists:duplicate(4 - K, 0))
end.
%% @private
ends_with(String, Suffix) ->
string:find(String, Suffix, trailing) =:= Suffix.
%% @private
filter_chunks(Chunks, IncludeLines) ->
AllowedChunks = allowed_chunks(IncludeLines),
lists:filter(
fun({Tag, _Data}) -> lists:member(Tag, AllowedChunks) end,
Chunks
).
allowed_chunks(false) ->
?ALLOWED_CHUNKS;
allowed_chunks(true) ->
["Line" | ?ALLOWED_CHUNKS].
%% @private
remove_names(Names, ParsedFiles) ->
lists:filter(
fun(ParsedFile) ->
ModuleName = get_element_name(ParsedFile),
not lists:member(ModuleName, Names)
end,
ParsedFiles
).
%% @private
write_files(ParsedFiles, OutputDir) ->
case filelib:is_dir(OutputDir) of
true ->
io:format("Writing to ~s ...~n", [OutputDir]),
lists:foreach(
fun(ParsedFile) ->
ModuleName = get_element_name(ParsedFile),
Path = OutputDir ++ "/" ++ ModuleName,
case filelib:ensure_dir(Path) of
ok ->
io:format("x ~s~n", [ModuleName]),
RawData = get_element_data(ParsedFile),
Data =
case file_type(ModuleName) of
normal ->
<<Size:32, Rest/binary>> = RawData,
<<Contents:Size/binary, _/binary>> = Rest,
Contents;
_ ->
RawData
end,
file:write_file(Path, Data);
Error ->
throw(Error)
end
end,
ParsedFiles
);
_ ->
throw(enoent)
end.
%% @private
filter_names([], ParsedFiles) ->
ParsedFiles;
filter_names(Names, ParsedFiles) ->
lists:filter(
fun(ParsedFile) ->
ModuleName = get_element_name(ParsedFile),
lists:member(ModuleName, Names)
end,
ParsedFiles
).
%% @private
deduplicate([]) ->
[];
deduplicate([H | T]) ->
[H | [X || X <- deduplicate(T), X /= H]].