zx/zomp/lib/otpr/zx/0.3.0/src/zx_zsp.erl

307 lines
9.2 KiB
Erlang

%%% @doc
%%% ZX ZSP: The ZSP package interface
%%%
%%% ZSP files are project package files managed by Zomp and ZX. This module provides
%%% a common interface for interfacing with the contents of a ZSP file and a few helper
%%% functions.
%%% @end
-module(zx_zsp).
-vsn("0.3.0").
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").
-export([new_meta/0,
pack/2,
unpack/1, blithely_unpack/1,
extract/2, blithely_extract/2,
verify/1, verify/2,
meta/1, package_id/1,
resign/2, resign/3]).
-export_type([meta/0]).
-include("zx_logger.hrl").
-type meta() :: #{package_id := undefined | zx:package_id(),
name := string(),
desc := string(),
author := string(),
a_email := string(),
copyright := string(),
c_email := string(),
ws_url := string(),
repo_url := string(),
prefix := string(),
tags := [string()],
deps := [zx:package_id()],
modules := [string()],
type := undefined | zx:package_type(),
key_name := none | zx:key_name()}.
-spec new_meta() -> meta().
new_meta() ->
#{package_id => undefined,
name => "",
desc => "",
author => "",
a_email => "",
copyright => "",
c_email => "",
ws_url => "",
repo_url => "",
prefix => "",
tags => [],
deps => [],
modules => [],
type => undefined,
key_name => none}.
-spec pack(TargetDir, Key) -> Result
when TargetDir :: file:filename(),
Key :: public_key:rsa_public_key(),
Result :: ok
| {error, file:posix()}.
pack(TargetDir, Key) ->
case zx_lib:read_project_meta(TargetDir) of
{ok, Meta} -> pack2(TargetDir, Key, Meta);
Error -> Error
end.
pack2(TargetDir, Key, Meta) ->
PackageID = maps:get(package_id, Meta),
{ok, PackageString} = zx_lib:package_string(PackageID),
ZspFile = PackageString ++ ".zsp",
case filelib:is_regular(ZspFile) of
false -> pack3(TargetDir, PackageID, Meta, Key, ZspFile);
true -> {error, eexists}
end.
pack3(TargetDir, PackageID, Meta, {KeyName, Key}, ZspFile) ->
Beams = filelib:wildcard("**/*.{beam,ez}", TargetDir),
ToDelete = [filename:join(TargetDir, Beam) || Beam <- Beams],
ok = lists:foreach(fun file:delete/1, ToDelete),
ok = zx_lib:rm_rf(filename:join(TargetDir, "erl_crash.dump")),
{ok, Everything} = file:list_dir(TargetDir),
DotFiles = filelib:wildcard(".*", TargetDir),
Targets = lists:subtract(Everything, DotFiles),
{ok, CWD} = file:get_cwd(),
ok = file:set_cwd(TargetDir),
ok = zx_local:update_app_file(),
Name = element(2, PackageID),
AppFile = filename:join("ebin", Name ++ ".app"),
{ok, [{application, _, AppData}]} = file:consult(AppFile),
Modules = lists:map(fun atom_to_list/1, proplists:get_value(modules, AppData)),
TarGzPath = filename:join(zx_lib:path(tmp), ZspFile ++ ".tgz"),
ok = erl_tar:create(TarGzPath, Targets, [compressed]),
{ok, TgzBin} = file:read_file(TarGzPath),
ok = file:delete(TarGzPath),
MetaData = Meta#{key_name := KeyName, modules := Modules},
MetaBin = term_to_binary(MetaData),
MetaSize = byte_size(MetaBin),
SignMe = <<MetaSize:24, MetaBin:MetaSize/binary, TgzBin/binary>>,
Sig = zx_key:sign(SignMe, Key),
SigSize = byte_size(Sig),
ZspData = <<SigSize:24, Sig:SigSize/binary, SignMe/binary>>,
ok = file:set_cwd(CWD),
case file:write_file(ZspFile, ZspData) of
ok -> {ok, ZspFile};
Error -> Error
end.
-spec unpack(ZspFile) -> Outcome
when ZspFile :: file:filename(),
Outcome :: ok
| {error, Reason},
Reason :: bad_zsp
| bad_sig
| bad_key
| file:posix().
unpack(ZspFile) ->
case file:read_file(ZspFile) of
{ok, ZspBin} -> extract(ZspBin, cwd);
Error -> Error
end.
-spec extract(ZspBin, Location) -> Outcome
when ZspBin :: binary(),
Location :: cwd
| lib,
Outcome :: ok
| {error, Reason},
Reason :: bad_zsp
| bad_sig
| bad_key.
extract(ZspBin, Location) ->
case verify(ZspBin) of
ok -> blithely_extract(ZspBin, Location);
Error -> Error
end.
-spec blithely_unpack(ZspFile) -> Outcome
when ZspFile :: file:filename(),
Outcome :: ok
| {error, Reason},
Reason :: bad_zsp
| file:posix().
blithely_unpack(ZspFile) ->
case file:read_file(ZspFile) of
{ok, ZspBin} -> blithely_extract(ZspBin, cwd);
Error -> Error
end.
blithely_extract(ZspBin, cwd) ->
{ok, Meta} = meta(ZspBin),
PackageID = maps:get(package_id, Meta),
{ok, PackageString} = zx_lib:package_string(PackageID),
install(ZspBin, PackageString);
blithely_extract(ZspBin, lib) ->
{ok, Meta} = meta(ZspBin),
PackageID = maps:get(package_id, Meta),
Path = zx_lib:ppath(lib, PackageID),
install(ZspBin, Path).
install(<<SS:24, _:SS/binary, MS:24, _:MS/binary, TarGZ/binary>>, Path) ->
ok = filelib:ensure_dir(Path),
ok = zx_lib:rm_rf(Path),
ok = file:make_dir(Path),
erl_tar:extract({binary, TarGZ}, [{cwd, Path}, compressed]).
-spec verify(ZspBin) -> Outcome
when ZspBin :: binary(),
Outcome :: ok
| {error, Reason},
Reason :: bad_zsp
| bad_sig
| bad_key.
verify(<<Size:24, Sig:Size/binary, Signed/binary>>) ->
verify2(Sig, Signed);
verify(_) ->
{error, bad_zsp}.
verify2(Sig, Signed = <<MetaSize:24, MetaBin:MetaSize/binary, _/binary>>) ->
case zx_lib:b_to_ts(MetaBin) of
{ok, {{Realm, _, _}, SigKeyName, _, _, _}} ->
SigKeyID = {Realm, SigKeyName},
verify3(Sig, Signed, SigKeyID);
error ->
{error, bad_zsp}
end;
verify2(_, _) ->
{error, bad_zsp}.
verify3(Sig, Signed, SigKeyID) ->
case zx_key:load(public, SigKeyID) of
{ok, PubKey} ->
verify4(Signed, Sig, PubKey);
{error, Reason} ->
Message = "zx_key:load(public, ~tp) failed with: ~tp",
ok = log(warning, Message, [SigKeyID, Reason]),
{error, bad_key}
end.
verify4(Signed, Sig, PubKey) ->
case zx_key:verify(Signed, Sig, PubKey) of
true -> ok;
false -> {error, bad_sig}
end.
-spec verify(ZspBin, PubKey) -> boolean()
when ZspBin :: binary(),
PubKey :: public_key:rsa_public_key().
verify(<<Size:24, Sig:Size/binary, Signed/binary>>, PubKey) ->
zx_key:verify(Signed, Sig, PubKey).
-spec meta(binary()) -> {ok, meta()} | {error, bad_zsp}.
meta(<<SS:24, _:SS/binary, MS:24, MetaBin:MS/binary, _/binary>>) ->
case zx_lib:b_to_ts(MetaBin) of
{ok, Meta} -> {ok, Meta};
_ -> {error, bad_zsp}
end;
meta(_) ->
{error, bad_zsp}.
-spec package_id(binary()) -> {ok, zx:package_id()} | {error, bad_zsp}.
package_id(Bin) ->
case meta(Bin) of
{ok, Meta} -> {ok, maps:get(package_id, Meta)};
Error -> Error
end.
-spec resign(KeyID, ZspBin) -> Outcome
when KeyID :: zx:key_id(),
ZspBin :: binary(),
Outcome :: {ok, binary()}
| {error, Reason},
Reason :: bad_zsp
| bad_realm
| no_key
| bad_key.
resign(KeyID = {Realm, KeyName},
<<SS:24, _:SS/binary, MS:24, MetaBin:MS/binary, TarGZ/binary>>) ->
case zx_daemon:get_key(private, KeyID) of
{ok, Key} -> resign2(Realm, KeyName, Key, MetaBin, TarGZ);
Error -> Error
end;
resign(_, _) ->
{error, bad_zsp}.
-spec resign(KeyID, Key, ZspBin) -> Outcome
when KeyID :: zx:key_id(),
Key :: public_key:rsa_private_key(),
ZspBin :: binary(),
Outcome :: {ok, binary()}
| {error, Reason},
Reason :: bad_zsp
| bad_realm
| no_key
| bad_key.
resign({Realm, KeyName},
Key,
<<SS:24, _:SS/binary, MS:24, MetaBin:MS/binary, TarGZ/binary>>) ->
resign2(Realm, KeyName, Key, MetaBin, TarGZ);
resign(_, _, _) ->
{error, bad_zsp}.
resign2(Realm, KeyName, Key, MetaBin, TarGZ) ->
case zx_lib:b_to_ts(MetaBin) of
{ok, Meta} -> resign3(Realm, KeyName, Key, Meta, TarGZ);
error -> {error, bad_zsp}
end.
resign3(Realm, KeyName, Key, Meta = #{package_id := {_, Name, Version}}, TarGZ) ->
NewMeta = Meta#{package_id := {Realm, Name, Version}, key_name := KeyName},
MetaBin = term_to_binary(NewMeta),
MetaSize = byte_size(MetaBin),
SignMe = <<MetaSize:24, MetaBin:MetaSize/binary, TarGZ/binary>>,
Sig = zx_key:sign(SignMe, Key),
SigSize = byte_size(Sig),
ZspBin = <<SigSize:24, Sig:SigSize/binary, SignMe/binary>>,
{ok, ZspBin}.