mirror of
https://github.com/mikpe/pdp10-tools.git
synced 2026-02-16 21:01:26 +00:00
1161 lines
37 KiB
Erlang
1161 lines
37 KiB
Erlang
%%% -*- erlang-indent-level: 2 -*-
|
|
%%%
|
|
%%% 'ar' clone for PDP10
|
|
%%% Copyright (C) 2013-2020 Mikael Pettersson
|
|
%%%
|
|
%%% This file is part of pdp10-tools.
|
|
%%%
|
|
%%% pdp10-tools is free software: you can redistribute it and/or modify
|
|
%%% it under the terms of the GNU General Public License as published by
|
|
%%% the Free Software Foundation, either version 3 of the License, or
|
|
%%% (at your option) any later version.
|
|
%%%
|
|
%%% pdp10-tools is distributed in the hope that it will be useful,
|
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
%%% GNU General Public License for more details.
|
|
%%%
|
|
%%% You should have received a copy of the GNU General Public License
|
|
%%% along with pdp10-tools. If not, see <http://www.gnu.org/licenses/>.
|
|
%%%
|
|
%%%=============================================================================
|
|
%%%
|
|
%%% Requirements:
|
|
%%%
|
|
%%% - members are ordered as stored in the archive
|
|
%%% - members can be appended at the end of the archive, inserted before or
|
|
%%% after another named member, updated in place, or deleted
|
|
%%% - there can be multiple members with the same name
|
|
%%%
|
|
%%% Recall that Erlang orders lists lexicographically.
|
|
%%%
|
|
%%% Define a label to be a non-empty list of integers.
|
|
%%%
|
|
%%% Members in the pre-existing archive are labelled with their positions I in
|
|
%%% the archive, as singleton lists [I].
|
|
%%%
|
|
%%% Members appended after some pre-existing member are labelled [I,J+1], where
|
|
%%% [I] is the label of the pre-existing member, and J is the number of newly
|
|
%%% appended members after [I].
|
|
%%%
|
|
%%% Members inserted before a pre-existing member with label [I] are treated as
|
|
%%% if appended after a member with label [I-1].
|
|
%%%
|
|
%%% Members appended at the end of the archive are treated as if appended after
|
|
%%% the pre-existing archive's last member, with label [N]. For an empty
|
|
%%% archive the label of the imaginary last member is defined to be [0].
|
|
%%%
|
|
%%% The in-core version of an archive stores the members in a gb_tree with their
|
|
%%% labels as keys. A separate structure maps each member name to an ordered
|
|
%%% list of the labels of its occurrences in the archive.
|
|
|
|
-module(ar).
|
|
-export([main/1]).
|
|
|
|
-include_lib("kernel/include/file.hrl").
|
|
-include_lib("ar/include/pdp10_ar.hrl").
|
|
|
|
%% in-core version of the ar header
|
|
-record(arhdr,
|
|
{ ar_name :: string() | non_neg_integer()
|
|
, ar_date :: non_neg_integer()
|
|
, ar_uid :: non_neg_integer()
|
|
, ar_gid :: non_neg_integer()
|
|
, ar_mode :: non_neg_integer()
|
|
, ar_size :: non_neg_integer()
|
|
}).
|
|
|
|
-record(member,
|
|
{ arhdr :: #arhdr{}
|
|
, data :: non_neg_integer() % at this offset in old archive
|
|
| string() % in this external file
|
|
}).
|
|
|
|
-type label() :: nonempty_list(integer()).
|
|
|
|
-record(archive,
|
|
{ symtab % TODO: implement SymTab
|
|
, members :: gb_trees:tree(label(),
|
|
{non_neg_integer(), #member{} | []})
|
|
, labelmap :: #{string() => nonempty_list(label())}
|
|
}).
|
|
|
|
-record(options,
|
|
{ operation % d, q, r, t, or x
|
|
, mod_c = false % true iff c modifier present
|
|
, mod_u = false % true iff u modifier present
|
|
, mod_v = false % true iff v modifier present
|
|
, mod_D = false % true iff D modifier present
|
|
, mod_o = false % true iff o modifier present
|
|
, mod_O = false % true iff O modifier present
|
|
}).
|
|
|
|
-type file() :: pdp10_stdio:file().
|
|
|
|
%% Command-line interface ======================================================
|
|
|
|
main(Argv) ->
|
|
escript_runtime:start(fun main_/1, Argv).
|
|
|
|
-spec main_([string()]) -> no_return().
|
|
main_(Argv) ->
|
|
case parse_argv(Argv) of
|
|
{ok, {Opts, ArchiveFile, Files}} ->
|
|
ar(Opts, ArchiveFile, Files),
|
|
halt(0);
|
|
{error, ErrMsg} ->
|
|
escript_runtime:errmsg("~s\n", [ErrMsg]),
|
|
usage()
|
|
end.
|
|
|
|
usage() ->
|
|
escript_runtime:fmterr(
|
|
"Usage: ~s [-]{d,q,r,t,x}[cuvV] <archive> <member..>\n",
|
|
[escript_runtime:progname()]),
|
|
halt(1).
|
|
|
|
parse_argv([[$- | Arg] | Argv]) -> parse_operation(Arg, Argv);
|
|
parse_argv([Arg | Argv]) -> parse_operation(Arg, Argv);
|
|
parse_argv([]) -> {error, "no operation specified"}.
|
|
|
|
parse_operation(Arg, Argv) ->
|
|
%% m - NYI
|
|
%% p - NYI
|
|
%% s - NYI - TODO
|
|
case Arg of
|
|
[$d | Mod] -> parse_modifiers(Mod, Argv, $d, []);
|
|
[$q | Mod] -> parse_modifiers(Mod, Argv, $q, []); % TODO: f
|
|
[$r | Mod] -> parse_modifiers(Mod, Argv, $r, [$u]); % TODO: a, b/i, f
|
|
[$t | Mod] -> parse_modifiers(Mod, Argv, $t, [$O]);
|
|
[$x | Mod] -> parse_modifiers(Mod, Argv, $x, [$o]);
|
|
[$V | _] -> version();
|
|
[C | _] -> {error, io_lib:format("invalid operation: ~c", [C])};
|
|
[] -> {error, "no operation specified"}
|
|
end.
|
|
|
|
parse_modifiers(Mod, Argv, Op, OpMods) ->
|
|
Opts = #options{operation = Op},
|
|
parse_modifiers2(Mod, Argv, Opts, OpMods).
|
|
|
|
parse_modifiers2([], Argv, Opts, _OpMods) ->
|
|
%% TODO: check for a or b/i modifier -> get relpos arg
|
|
%% TODO: check for N modifier -> get count arg
|
|
parse_archive(Argv, Opts);
|
|
parse_modifiers2([C | Mod], Argv, Opts, OpMods) ->
|
|
case parse_modifier(C, Opts, OpMods) of
|
|
{ok, NewOpts} -> parse_modifiers2(Mod, Argv, NewOpts, OpMods);
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
parse_modifier(C, Opts, OpMods) ->
|
|
%% a - NYI
|
|
%% b/i - NYI
|
|
%% N - NYI
|
|
%% f - NYI
|
|
%% P - NYI
|
|
%% s - NYI - TODO
|
|
%% S - NYI - TODO
|
|
%% T - NYI
|
|
case C of
|
|
$c -> {ok, Opts#options{mod_c = true}};
|
|
$D -> {ok, Opts#options{mod_D = true}};
|
|
$U -> {ok, Opts#options{mod_D = false}};
|
|
$o -> check_opmods($o, OpMods, Opts#options{mod_o = true});
|
|
$O -> check_opmods($O, OpMods, Opts#options{mod_O = true});
|
|
$u -> check_opmods($u, OpMods, Opts#options{mod_u = true});
|
|
$v -> {ok, Opts#options{mod_v = true}};
|
|
$V -> version();
|
|
_ -> {error, io_lib:format("invalid modifier: ~c", [C])} end.
|
|
|
|
check_opmods(C, OpMods, Opts) ->
|
|
case lists:member(C, OpMods) of
|
|
true -> {op, Opts};
|
|
false -> {error, io_lib:format("invalid modifier for operation: ~c", [C])}
|
|
end.
|
|
|
|
version() ->
|
|
io:format(standard_io, "pdp10-tools ar version 0.1\n", []),
|
|
halt(0).
|
|
|
|
parse_archive(Argv, Opts) ->
|
|
case Argv of
|
|
[ArchiveFile | Files] ->
|
|
{ok, {Opts, ArchiveFile, Files}};
|
|
[] ->
|
|
{error, "no archive specified"}
|
|
end.
|
|
|
|
%% ar dispacher ================================================================
|
|
|
|
ar(Opts, ArchiveFile, Files) ->
|
|
case Opts#options.operation of
|
|
Op when Op =:= $d; Op =:= $q; Op =:= $r ->
|
|
ar_dqr(Opts, ArchiveFile, Files);
|
|
Op when Op =:= $t; Op =:= $x ->
|
|
ar_tx(Opts, ArchiveFile, Files)
|
|
end.
|
|
|
|
%% ar d/q/r code ===============================================================
|
|
|
|
ar_dqr(Opts, ArchiveFile, Files) ->
|
|
case read_output_archive(ArchiveFile) of
|
|
{ok, {FP, Archive}} ->
|
|
case ar_dqr(Opts, ArchiveFile, FP, Archive, Files) of
|
|
{ok, TmpFile} -> file:rename(TmpFile, ArchiveFile);
|
|
{error, Reason} -> escript_runtime:fatal("~p\n", [Reason])
|
|
end;
|
|
{error, Reason} ->
|
|
escript_runtime:fatal("failed to read ~s: ~p\n", [ArchiveFile, Reason])
|
|
end.
|
|
|
|
read_output_archive(ArchiveFile) ->
|
|
case read_archive_file(ArchiveFile) of
|
|
{ok, {_FP, _Archive}} = Result -> Result;
|
|
{error, enoent} ->
|
|
FP = [],
|
|
Archive = make_archive(symtab_none(), _Members = []),
|
|
{ok, {FP, Archive}};
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
ar_dqr(Opts, ArchiveFile, OldFP, Archive, Files) ->
|
|
try
|
|
{ok, NewArchive} = ar_dqr_dispatch(Opts, Archive, Files),
|
|
write_tmp_archive(ArchiveFile, OldFP, NewArchive)
|
|
after
|
|
case OldFP of
|
|
[] -> ok;
|
|
_ -> pdp10_stdio:fclose(OldFP)
|
|
end
|
|
end.
|
|
|
|
ar_dqr_dispatch(Opts, Archive, Files) ->
|
|
case Opts#options.operation of
|
|
$d -> ar_d(Opts, Archive, Files);
|
|
$q -> ar_q(Opts, Archive, Files);
|
|
$r -> ar_r(Opts, Archive, Files)
|
|
end.
|
|
|
|
ar_d(Opts, Archive, Files) ->
|
|
NewArchive =
|
|
lists:foldl(fun(File, Archive0) ->
|
|
ar_d_1(Opts, Archive0, File)
|
|
end, Archive, Files),
|
|
{ok, NewArchive}.
|
|
|
|
ar_d_1(Opts, Archive, File) ->
|
|
Name = filename:basename(File),
|
|
case archive_lookup_label(Archive, Name) of
|
|
false ->
|
|
case Opts#options.mod_v of
|
|
true -> io:format(standard_io, "No member named ~s~n", [File]);
|
|
false -> ok
|
|
end,
|
|
Archive;
|
|
Label ->
|
|
case Opts#options.mod_v of
|
|
true -> io:format(standard_io, "d - ~s~n", [File]);
|
|
false -> ok
|
|
end,
|
|
archive_delete_member(Archive, Name, Label)
|
|
end.
|
|
|
|
ar_q(Opts, Archive, Files) ->
|
|
LastLabel = archive_last_label(Archive),
|
|
NewArchive =
|
|
lists:foldl(fun(File, Archive0) ->
|
|
ar_q_1(Opts, Archive0, LastLabel, File)
|
|
end, Archive, Files),
|
|
{ok, NewArchive}.
|
|
|
|
ar_q_1(Opts, Archive, LastLabel, File) ->
|
|
case file:read_file_info(File, [{time, posix}]) of
|
|
{ok, #file_info{mtime = Date, uid = Uid, gid = Gid, mode = Mode,
|
|
size = OctetSize}} ->
|
|
NonetSize = (OctetSize div 9) * 8 + ((OctetSize rem 9) * 8) div 9,
|
|
Name = filename:basename(File),
|
|
ArHdr = #arhdr{ ar_name = Name
|
|
, ar_date = Date
|
|
, ar_uid = Uid
|
|
, ar_gid = Gid
|
|
, ar_mode = Mode
|
|
, ar_size = NonetSize
|
|
},
|
|
Member = #member{arhdr = ArHdr, data = File},
|
|
case Opts#options.mod_v of
|
|
true -> io:format(standard_io, "a - ~s~n", [File]);
|
|
false -> ok
|
|
end,
|
|
%% FIXME: this differs from GNU ar which treats 'ar qs' as 'ar r',
|
|
%% i.e. performing in-place replacement of existing members
|
|
archive_insert_member_after(Archive, LastLabel, Member);
|
|
{error, Reason} ->
|
|
escript_runtime:fatal("~s: ~s~n", [File, file:format_error(Reason)])
|
|
end.
|
|
|
|
ar_r(Opts, Archive, Files) ->
|
|
LastLabel = archive_last_label(Archive),
|
|
NewArchive =
|
|
lists:foldl(fun(File, Archive0) ->
|
|
ar_r_1(Opts, Archive0, LastLabel, File)
|
|
end, Archive, Files),
|
|
{ok, NewArchive}.
|
|
|
|
ar_r_1(Opts, Archive, LastLabel, File) ->
|
|
case file:read_file_info(File, [{time, posix}]) of
|
|
{ok, #file_info{mtime = Date, uid = Uid, gid = Gid, mode = Mode,
|
|
size = OctetSize}} ->
|
|
NonetSize = (OctetSize div 9) * 8 + ((OctetSize rem 9) * 8) div 9,
|
|
Name = filename:basename(File),
|
|
ArHdr = #arhdr{ ar_name = Name
|
|
, ar_date = Date
|
|
, ar_uid = Uid
|
|
, ar_gid = Gid
|
|
, ar_mode = Mode
|
|
, ar_size = NonetSize
|
|
},
|
|
Member = #member{arhdr = ArHdr, data = File},
|
|
%% FIXME: this doesn't match GNU ar when duplicate Names occur
|
|
case archive_lookup_label(Archive, Name) of
|
|
false ->
|
|
case Opts#options.mod_v of
|
|
true -> io:format(standard_io, "a - ~s~n", [File]);
|
|
false -> ok
|
|
end,
|
|
archive_insert_member_after(Archive, LastLabel, Member);
|
|
Label ->
|
|
case Opts#options.mod_v of
|
|
true -> io:format(standard_io, "r - ~s~n", [File]);
|
|
false -> ok
|
|
end,
|
|
archive_update_member(Archive, Label, Member)
|
|
end;
|
|
{error, Reason} ->
|
|
escript_runtime:fatal("~s: ~s~n", [File, file:format_error(Reason)])
|
|
end.
|
|
|
|
%% ar t/x code =================================================================
|
|
|
|
ar_tx(Opts, ArchiveFile, Files) ->
|
|
FileSet = ar_tx_fileset_from_list(Files),
|
|
case read_archive_file(ArchiveFile) of
|
|
{ok, {FP, Archive}} ->
|
|
try
|
|
case ar_tx_loop(Opts, FP, archive_members_iterator(Archive), FileSet) of
|
|
{ok, []} ->
|
|
ok;
|
|
{ok, RestFiles} ->
|
|
[escript_runtime:errmsg("no entry ~s in archive\n", [File])
|
|
|| File <- RestFiles],
|
|
halt(1);
|
|
{error, Reason} ->
|
|
escript_runtime:fatal("~p\n", [Reason])
|
|
end
|
|
after
|
|
pdp10_stdio:fclose(FP)
|
|
end;
|
|
{error, Reason} ->
|
|
escript_runtime:fatal("failed to read ~s: ~p\n", [ArchiveFile, Reason])
|
|
end.
|
|
|
|
ar_tx_loop(Opts, FP, Members, FileSet) ->
|
|
case members_iterator_next(Members) of
|
|
none ->
|
|
{ok, ar_tx_fileset_to_list(FileSet)};
|
|
{_Label, Member, RestMembers} ->
|
|
case ar_tx_should_process_member(Member, FileSet) of
|
|
{true, RestFileSet} ->
|
|
Status =
|
|
case Opts#options.operation of
|
|
$t -> ar_t_member(Opts, Member);
|
|
$x -> ar_x_member(Opts, FP, Member)
|
|
end,
|
|
case Status of
|
|
ok -> ar_tx_loop(Opts, FP, RestMembers, RestFileSet);
|
|
{error, _Reason} = Error -> Error
|
|
end;
|
|
false ->
|
|
ar_tx_loop(Opts, FP, RestMembers, FileSet)
|
|
end
|
|
end.
|
|
|
|
ar_tx_should_process_member(Member, FileSet) ->
|
|
case ar_tx_fileset_is_none(FileSet) of
|
|
true -> {true, FileSet};
|
|
false ->
|
|
%% Note: this relies on read_archive/1 finalising member names.
|
|
File = Member#member.arhdr#arhdr.ar_name,
|
|
case fileset_is_element(File, FileSet) of
|
|
true -> {true, fileset_delete(File, FileSet)};
|
|
false -> false
|
|
end
|
|
end.
|
|
|
|
ar_tx_fileset_from_list([]) -> false;
|
|
ar_tx_fileset_from_list(Files) -> fileset_from_list(Files).
|
|
|
|
ar_tx_fileset_to_list(false) -> [];
|
|
ar_tx_fileset_to_list(FileSet) -> fileset_to_list(FileSet).
|
|
|
|
ar_tx_fileset_is_none(false) -> true;
|
|
ar_tx_fileset_is_none(_FileSet) -> false.
|
|
|
|
fileset_delete(File, FileSet) -> gb_sets:delete(File, FileSet).
|
|
fileset_from_list(Files) -> gb_sets:from_list(Files).
|
|
fileset_is_element(File, FileSet) -> gb_sets:is_element(File, FileSet).
|
|
fileset_to_list(FileSet) -> gb_sets:to_list(FileSet).
|
|
|
|
%% ar t ========================================================================
|
|
|
|
ar_t_member(Opts, #member{arhdr = ArHdr})->
|
|
case Opts#options.mod_v of
|
|
true ->
|
|
#arhdr{ ar_date = Date
|
|
, ar_uid = Uid
|
|
, ar_gid = Gid
|
|
, ar_mode = Mode
|
|
, ar_size = Size
|
|
} = ArHdr,
|
|
io:format(standard_io,
|
|
"~s~s~s ~B/~B ~10B ~s ",
|
|
[ rwx(Mode bsr 6)
|
|
, rwx(Mode bsr 3)
|
|
, rwx(Mode)
|
|
, Uid
|
|
, Gid
|
|
, Size
|
|
, date_string(Date)
|
|
]);
|
|
false -> ok
|
|
end,
|
|
io:format(standard_io, "~s~n", [ArHdr#arhdr.ar_name]).
|
|
|
|
rwx(Mode) -> [r(Mode), w(Mode), x(Mode)].
|
|
|
|
r(Mode) -> test_bit(Mode, 4, $r).
|
|
w(Mode) -> test_bit(Mode, 2, $w).
|
|
x(Mode) -> test_bit(Mode, 1, $x).
|
|
|
|
test_bit(Mode, Bit, Ch) ->
|
|
if Mode band Bit =/= 0 -> Ch;
|
|
true -> $-
|
|
end.
|
|
|
|
%% Convert Unix time, seconds since 1970-01-01, to human-readable format
|
|
%% as per strftime(3) "%b %e %H:%M %Y".
|
|
date_string(UnixTime) ->
|
|
{{Y, M, D}, {HH, MM, _SS}} = unixtime_to_localtime(UnixTime),
|
|
io_lib:format("~s ~2B ~2..0B:~2..0B ~B",
|
|
[abbreviated_month_name(M), D, HH, MM, Y]).
|
|
|
|
abbreviated_month_name(M) ->
|
|
%% FIXME: should take locale into consideration
|
|
element(M, {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}).
|
|
|
|
unixtime_to_localtime(SecondsSinceEpoch) ->
|
|
calendar:universal_time_to_local_time(
|
|
calendar:gregorian_seconds_to_datetime(
|
|
calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}})
|
|
+ SecondsSinceEpoch)).
|
|
|
|
%% ar x ========================================================================
|
|
|
|
ar_x_member(Opts, ArchiveFP, Member) ->
|
|
#member{ arhdr = #arhdr{ar_name = Name, ar_size = Size, ar_mode = Mode}
|
|
, data = SrcOffset
|
|
} = Member,
|
|
case Opts#options.mod_v of
|
|
true -> io:format(standard_io, "x - ~s~n", [Name]);
|
|
false -> ok
|
|
end,
|
|
case pdp10_stdio:fopen(Name, [raw, write, delayed_write]) of
|
|
{ok, MemberFP} ->
|
|
Status = iocpy(MemberFP, ArchiveFP, SrcOffset, Size),
|
|
pdp10_stdio:fclose(MemberFP),
|
|
case Status of
|
|
ok -> file:change_mode(Name, Mode band 8#0777);
|
|
Error -> Error
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
%% archive output ==============================================================
|
|
|
|
write_tmp_archive(ArchiveFile, OldFP, Archive) ->
|
|
{StrTab, RawArchive} = archive_strtabify(Archive),
|
|
case mkstemp(filename:dirname(ArchiveFile), ".artmp") of
|
|
{ok, {TmpFile, TmpFP}} ->
|
|
try
|
|
case write_archive(TmpFP, StrTab, RawArchive, OldFP) of
|
|
ok -> {ok, TmpFile};
|
|
{error, _Reason} = Error -> Error
|
|
end
|
|
after
|
|
pdp10_stdio:fclose(TmpFP)
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
archive_strtabify(Archive) ->
|
|
{NewArchive, {_Offset, StrTabRev}} =
|
|
archive_members_mapfoldl(Archive, {0, []}, fun member_strtabify/2),
|
|
{lists:reverse(StrTabRev), NewArchive}.
|
|
|
|
member_strtabify(Member, Acc = {Offset, StrTabRev}) ->
|
|
ArHdr = Member#member.arhdr,
|
|
Name = ArHdr#arhdr.ar_name,
|
|
Length = length(Name),
|
|
case Length < 16 of
|
|
true ->
|
|
{Member, Acc};
|
|
false ->
|
|
NewArHdr = ArHdr#arhdr{ar_name = Offset},
|
|
NewMember = Member#member{arhdr = NewArHdr},
|
|
NewStrTabRev = [16#0A, 16#2F | lists:reverse(Name, StrTabRev)],
|
|
NewOffset = Offset + Length + 2,
|
|
{NewMember, {NewOffset, NewStrTabRev}}
|
|
end.
|
|
|
|
write_archive(DstFP, StrTab, RawArchive, OldFP) ->
|
|
%% FIXME: handle SymTab
|
|
case write_ar_mag(DstFP) of
|
|
{error, _Reason} = Error -> Error;
|
|
ok ->
|
|
case write_strtab(DstFP, StrTab) of
|
|
{error, _Reason} = Error -> Error;
|
|
ok ->
|
|
Members = archive_members_iterator(RawArchive),
|
|
write_members(DstFP, Members, OldFP)
|
|
end
|
|
end.
|
|
|
|
write_strtab(_FP, []) -> ok;
|
|
write_strtab(FP, StrTab) ->
|
|
Size = length(StrTab),
|
|
ArHdr = #arhdr{ ar_name = "//"
|
|
, ar_date = 0
|
|
, ar_uid = 0
|
|
, ar_gid = 0
|
|
, ar_mode = 0
|
|
, ar_size = Size
|
|
},
|
|
case write_arhdr(FP, ArHdr) of
|
|
{error, _Reason} = Error -> Error;
|
|
ok ->
|
|
case fputs(StrTab, FP) of
|
|
{error, _Reason} = Error -> Error;
|
|
ok -> write_padding(FP, Size)
|
|
end
|
|
end.
|
|
|
|
write_members(DstFP, Members, OldFP) ->
|
|
case members_iterator_next(Members) of
|
|
none -> ok;
|
|
{_Label, Member, RestMembers} ->
|
|
case write_member(DstFP, Member, OldFP) of
|
|
{error, _Reason} = Error -> Error;
|
|
ok -> write_members(DstFP, RestMembers, OldFP)
|
|
end
|
|
end.
|
|
|
|
write_member(DstFP, Member, OldFP) ->
|
|
#member{arhdr = ArHdr, data = SrcData} = Member,
|
|
#arhdr{ar_size = Size} = ArHdr,
|
|
case write_arhdr(DstFP, ArHdr) of
|
|
{error, _Reason} = Error -> Error;
|
|
ok ->
|
|
case write_member_data(DstFP, Size, SrcData, OldFP) of
|
|
{error, _Reason} = Error -> Error;
|
|
ok -> write_padding(DstFP, Size)
|
|
end
|
|
end.
|
|
|
|
write_member_data(DstFP, Size, SrcOffset, OldFP) when is_integer(SrcOffset) ->
|
|
iocpy(DstFP, OldFP, SrcOffset, Size);
|
|
write_member_data(DstFP, Size, SrcFile, _OldFP) ->
|
|
case pdp10_stdio:fopen(SrcFile, [raw, read]) of
|
|
{ok, SrcFP} ->
|
|
try
|
|
iocpy(DstFP, SrcFP, Size)
|
|
after
|
|
pdp10_stdio:fclose(SrcFP)
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
write_padding(FP, Size) ->
|
|
case Size band 1 of
|
|
0 -> ok;
|
|
1 -> pdp10_stdio:fputc(16#0A, FP)
|
|
end.
|
|
|
|
%% create a temporary file =====================================================
|
|
|
|
-spec mkstemp(string(), string()) -> {ok, {string(), file()}} | {error, any()}.
|
|
mkstemp(Dir, Prefix) ->
|
|
mkstemp(Dir, Prefix ++ os:getpid() ++ "_", 0).
|
|
|
|
mkstemp(Dir, Prefix, Count) when Count < 100 ->
|
|
Path = filename:join(Dir, Prefix ++ integer_to_list(Count)),
|
|
case pdp10_stdio:fopen(Path, [raw, read, write, exclusive]) of
|
|
{ok, FP} -> {ok, {Path, FP}};
|
|
{error, _Reason} -> mkstemp(Dir, Prefix, Count + 1)
|
|
end;
|
|
mkstemp(_Dir, _Prefix, _Count) -> {error, eexist}.
|
|
|
|
%% copy data between I/O devices ===============================================
|
|
|
|
iocpy(DstFP, SrcFP, SrcOffset, NrBytes) ->
|
|
case pdp10_stdio:fseek(SrcFP, {bof, SrcOffset}) of
|
|
ok -> iocpy(DstFP, SrcFP, NrBytes);
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
iocpy(_DstFP, _SrcFP, _NrBytes = 0) -> ok;
|
|
iocpy(DstFP, SrcFP, NrBytes) ->
|
|
case pdp10_stdio:fgetc(SrcFP) of
|
|
{ok, Byte} ->
|
|
case pdp10_stdio:fputc(Byte, DstFP) of
|
|
ok -> iocpy(DstFP, SrcFP, NrBytes - 1);
|
|
{error, _Reason} = Error -> Error
|
|
end;
|
|
{error, _Reason} = Error -> Error;
|
|
eof -> {error, eof}
|
|
end.
|
|
|
|
%% archive input ===============================================================
|
|
|
|
-spec read_archive_file(string())
|
|
-> {ok, {file(), #archive{}}} | {error, any()}.
|
|
read_archive_file(ArchiveFile) ->
|
|
case pdp10_stdio:fopen(ArchiveFile, [raw, read]) of
|
|
{ok, FP} ->
|
|
try
|
|
case read_archive_fp(FP) of
|
|
{ok, Archive} -> {ok, {FP, Archive}};
|
|
{error, _Reason} = Error ->
|
|
pdp10_stdio:fclose(FP),
|
|
Error
|
|
end
|
|
catch error:Reason ->
|
|
pdp10_stdio:fclose(FP),
|
|
{error, Reason}
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
-spec read_archive_fp(file()) -> {ok, #archive{}} | {error, any()}.
|
|
read_archive_fp(FP) ->
|
|
case read_ar_mag(FP) of
|
|
ok -> read_archive_symtab(FP);
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
read_archive_symtab(FP) ->
|
|
case read_arhdr(FP) of
|
|
{ok, ArHdr} ->
|
|
case ArHdr#arhdr.ar_name of
|
|
"/" ->
|
|
case read_symtab(FP, ArHdr) of
|
|
{ok, SymTab} -> read_archive_strtab(FP, SymTab);
|
|
{error, _Reason} = Error -> Error
|
|
end;
|
|
_ -> read_archive_strtab(FP, symtab_none(), ArHdr)
|
|
end;
|
|
{error, eof} ->
|
|
{ok, make_archive(symtab_none(), _Members = [])};
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
read_archive_strtab(FP, SymTab) ->
|
|
case read_arhdr(FP) of
|
|
{ok, ArHdr} -> read_archive_strtab(FP, SymTab, ArHdr);
|
|
{error, eof} ->
|
|
{ok, make_archive(SymTab, _Members = [])};
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
read_archive_strtab(FP, SymTab, ArHdr) ->
|
|
case ArHdr#arhdr.ar_name of
|
|
"//" ->
|
|
case read_strtab(FP, ArHdr) of
|
|
{ok, StrTab} -> read_archive_members(FP, SymTab, StrTab, []);
|
|
{error, _Reason} = Error -> Error
|
|
end;
|
|
_ -> read_archive_members(FP, SymTab, strtab_none(), [], ArHdr)
|
|
end.
|
|
|
|
read_archive_members(FP, SymTab, StrTab, Members) ->
|
|
case read_arhdr(FP) of
|
|
{ok, ArHdr} ->
|
|
read_archive_members(FP, SymTab, StrTab, Members, ArHdr);
|
|
{error, eof} ->
|
|
{ok, make_archive(SymTab, lists:reverse(Members))};
|
|
{error, _Reason} = Error ->
|
|
Error
|
|
end.
|
|
|
|
read_archive_members(FP, SymTab, StrTab, Members, ArHdr) ->
|
|
case finalise_ar_name(StrTab, ArHdr#arhdr.ar_name) of
|
|
{ok, Name} ->
|
|
SrcOffset = pdp10_stdio:ftell(FP),
|
|
Member = #member{arhdr = ArHdr#arhdr{ar_name = Name},
|
|
data = SrcOffset},
|
|
NewMembers = [Member | Members],
|
|
case skip_member(FP, ArHdr#arhdr.ar_size) of
|
|
ok ->
|
|
read_archive_members(FP, SymTab, StrTab, NewMembers);
|
|
eof ->
|
|
{ok, make_archive(SymTab, lists:reverse(NewMembers))};
|
|
{error, _Reason} = Error ->
|
|
Error
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
finalise_ar_name(_StrTab, Name) when is_list(Name) -> {ok, Name};
|
|
finalise_ar_name(StrTab, Offset) when is_integer(Offset) ->
|
|
case strtab_lookup(StrTab, Offset) of
|
|
{ok, _Name} = Result -> Result;
|
|
false -> {error, invalid_strtab_offset}
|
|
end.
|
|
|
|
%% Unfortunately fseek() and file:position/2 allow seeking past the end of
|
|
%% the file, so we seek to the last byte of the member, read that, and then
|
|
%% optionally read a padding byte so the next header starts at an even offset.
|
|
%% An EOF when reading the member's last byte is an error, while an EOF when
|
|
%% reading the padding byte simply means the end of the archive.
|
|
skip_member(_FP, _Size = 0) -> ok;
|
|
skip_member(FP, Size) when Size > 0 ->
|
|
case pdp10_stdio:fseek(FP, {cur, Size - 1}) of
|
|
ok ->
|
|
case pdp10_stdio:fgetc(FP) of
|
|
{ok, _Byte} -> read_padding(FP, Size);
|
|
eof -> {error, eof};
|
|
{error, _Reason} = Error -> Error
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
read_padding(FP, Size) ->
|
|
case Size band 1 of
|
|
0 -> ok;
|
|
1 ->
|
|
case pdp10_stdio:fgetc(FP) of
|
|
{ok, 16#0A} -> ok;
|
|
{ok, Ch} -> {error, {invalid_padding, Ch}};
|
|
eof -> eof;
|
|
{error, _Reason} = Error -> Error
|
|
end
|
|
end.
|
|
|
|
%% cooked archives =============================================================
|
|
|
|
make_archive(SymTab, Members) ->
|
|
{LabelledMembers, LabelMap} = make_archive(Members),
|
|
#archive{ symtab = SymTab
|
|
, members = gb_trees:from_orddict(lists:reverse(LabelledMembers))
|
|
, labelmap = maps:map(fun(_Name, Labels) -> lists:reverse(Labels) end,
|
|
LabelMap)
|
|
}.
|
|
|
|
make_archive(Members) ->
|
|
make_archive(Members, 1, [{[0], {0, []}}], maps:new()).
|
|
|
|
make_archive([], _Index, LabelledMembers, LabelMap) ->
|
|
{LabelledMembers, LabelMap};
|
|
make_archive([Member | Members], Index, LabelledMembers, LabelMap) ->
|
|
Name = Member#member.arhdr#arhdr.ar_name,
|
|
Label = [Index],
|
|
NameLabels = maps:get(Name, LabelMap, []),
|
|
NewLabelMap = maps:put(Name, [Label | NameLabels], LabelMap),
|
|
NewLabelledMembers = [{Label, {0, Member}} | LabelledMembers],
|
|
make_archive(Members, Index + 1, NewLabelledMembers, NewLabelMap).
|
|
|
|
archive_members_iterator(#archive{members = Members}) ->
|
|
{[0], {_NrRight, []}, Iterator} = gb_trees:next(gb_trees:iterator(Members)),
|
|
Iterator.
|
|
|
|
members_iterator_next(Iterator) ->
|
|
case gb_trees:next(Iterator) of
|
|
none -> none;
|
|
{Label, {_NrRight, Member}, NewIterator} -> {Label, Member, NewIterator}
|
|
end.
|
|
|
|
archive_last_label(#archive{members = Members}) ->
|
|
{LastLabel, {_NrRight, _Member}} = gb_trees:largest(Members),
|
|
LastLabel.
|
|
|
|
archive_lookup_label(Archive, Name) ->
|
|
#archive{labelmap = LabelMap} = Archive,
|
|
case maps:find(Name, LabelMap) of
|
|
{ok, [Label | _Labels]} -> Label; % TODO: handle "N count"
|
|
error -> false
|
|
end.
|
|
|
|
archive_delete_member(Archive, Name, Label) ->
|
|
#archive{members = Members, labelmap = LabelMap} = Archive,
|
|
NewMembers = gb_trees:delete(Label, Members),
|
|
NewLabelMap =
|
|
case maps:get(Name, LabelMap) -- [Label] of
|
|
[] -> maps:remove(Name, LabelMap);
|
|
Labels -> maps:put(Name, Labels, LabelMap)
|
|
end,
|
|
Archive#archive{ symtab = symtab_none()
|
|
, members = NewMembers
|
|
, labelmap = NewLabelMap
|
|
}.
|
|
|
|
archive_insert_member_after(Archive, AfterLabel, Member) ->
|
|
#archive{members = Members, labelmap = LabelMap} = Archive,
|
|
Name = Member#member.arhdr#arhdr.ar_name,
|
|
{NrRight, AfterMember} = gb_trees:get(AfterLabel, Members),
|
|
NewLabel = AfterLabel ++ [NrRight + 1],
|
|
NameLabels = maps:get(Name, LabelMap, []),
|
|
NewNameLabels = ordsets:add_element(NewLabel, NameLabels),
|
|
NewLabelMap = maps:put(Name, NewNameLabels, LabelMap),
|
|
NewMembers = gb_trees:insert(NewLabel, {0, Member},
|
|
gb_trees:update(AfterLabel,
|
|
{NrRight + 1, AfterMember},
|
|
Members)),
|
|
Archive#archive{ symtab = symtab_none()
|
|
, members = NewMembers
|
|
, labelmap = NewLabelMap
|
|
}.
|
|
|
|
archive_update_member(Archive, Label, Member) ->
|
|
#archive{members = Members} = Archive,
|
|
NewMembers = gb_trees:update(Label, Member, Members),
|
|
Archive#archive{symtab = symtab_none(), members = NewMembers}.
|
|
|
|
archive_members_mapfoldl(Archive, Init, Fun) ->
|
|
[HiddenMember = {[0], {_, []}} | OrigMembers] =
|
|
gb_trees:to_list(Archive#archive.members),
|
|
{UpdatedMembers, Result} =
|
|
lists:mapfoldl(fun({Label, {NrRight, Member}}, Acc) ->
|
|
{NewMember, NewAcc} = Fun(Member, Acc),
|
|
{{Label, {NrRight, NewMember}}, NewAcc}
|
|
end, Init, OrigMembers),
|
|
NewMembers = gb_trees:from_orddict([HiddenMember | UpdatedMembers]),
|
|
{Archive#archive{members = NewMembers}, Result}.
|
|
|
|
%% symbol table ================================================================
|
|
|
|
-define(WORDSIZE, 4).
|
|
|
|
read_symtab(FP, #arhdr{ar_size = Size}) ->
|
|
true = Size >= ?WORDSIZE, % assert
|
|
case read_word_be(FP) of
|
|
{ok, NrSymbols} ->
|
|
true = Size >= (NrSymbols + 1) * ?WORDSIZE, % assert
|
|
case read_words_be(FP, NrSymbols) of
|
|
{ok, Offsets} ->
|
|
case read_string(FP, Size - (NrSymbols + 1) * ?WORDSIZE) of
|
|
{ok, StrBuf} ->
|
|
case read_padding(FP, Size) of
|
|
{error, _Reason} = Error -> Error;
|
|
_ -> make_symtab(Offsets, StrBuf) % ok or eof
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
make_symtab(Offsets, StrBuf) ->
|
|
case split_strbuf(StrBuf) of
|
|
{ok, Names} ->
|
|
case safe_zip(Offsets, Names) of
|
|
{ok, OffsetNamePairs} ->
|
|
{ok, lists:foldl(fun({Offset, Name}, SymTab) ->
|
|
symtab_insert(SymTab, Name, Offset)
|
|
end, symtab_new(), OffsetNamePairs)};
|
|
{error, _Reason} -> {error, invalid_symbol_table}
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
%% Split StrBuf into a list of NUL-terminated Names.
|
|
split_strbuf(StrBuf) -> split_strbuf(StrBuf, []).
|
|
|
|
split_strbuf([], Names) -> {ok, lists:reverse(Names)};
|
|
split_strbuf(String, Names) ->
|
|
case split_string(String) of
|
|
{ok, {[_|_] = Name, Rest}} -> split_strbuf(Rest, [Name | Names]);
|
|
_ -> {error, invalid_symbol_table}
|
|
end.
|
|
|
|
split_string(String) -> split_string(String, []).
|
|
|
|
split_string([16#00 | String], Name) -> {ok, {lists:reverse(Name), String}};
|
|
split_string([Ch | String], Name) -> split_string(String, [Ch | Name]);
|
|
split_string([], _Name) -> {error, missing_nul}.
|
|
|
|
safe_zip(As, Bs) ->
|
|
try
|
|
{ok, lists:zip(As, Bs)}
|
|
catch
|
|
error:Reason -> {error, Reason}
|
|
end.
|
|
|
|
symtab_none() -> [].
|
|
symtab_new() -> gb_trees:empty().
|
|
symtab_insert(SymTab, Name, Offset) -> gb_trees:insert(Name, Offset, SymTab).
|
|
-ifdef(notdef).
|
|
symtab_lookup(SymTab, Name) when SymTab =/= [] ->
|
|
case gb_trees:lookup(SymTab, Name) of
|
|
{value, Offset} -> {ok, Offset};
|
|
none -> false
|
|
end.
|
|
-endif.
|
|
|
|
read_words_be(FP, NrWords) -> read_words_be(FP, NrWords, []).
|
|
|
|
read_words_be(_FP, 0, Words) -> {ok, lists:reverse(Words)};
|
|
read_words_be(FP, N, Words) ->
|
|
case read_word_be(FP) of
|
|
{ok, Word} -> read_words_be(FP, N - 1, [Word | Words]);
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
%% FIXME: functionally equivalent to nm:pdp10_elf36_read_uint36/1
|
|
read_word_be(FP) -> read_word_be(FP, ?WORDSIZE, []).
|
|
|
|
read_word_be(_FP, 0, [B4, B3, B2, B1]) ->
|
|
{ok, ((B1 band 16#1FF) bsl 27) bor
|
|
((B2 band 16#1FF) bsl 19) bor
|
|
((B3 band 16#1FF) bsl 9) bor
|
|
(B4 band 16#1FF)};
|
|
read_word_be(FP, N, Acc) ->
|
|
case pdp10_stdio:fgetc(FP) of
|
|
{ok, Byte} -> read_word_be(FP, N - 1, [Byte | Acc]);
|
|
eof -> {error, invalid_symbol_table};
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
%% string table ================================================================
|
|
|
|
read_strtab(FP, #arhdr{ar_size = Size}) ->
|
|
case read_string(FP, Size) of
|
|
{ok, String} ->
|
|
case read_padding(FP, Size) of
|
|
ok -> scan_strtab(String);
|
|
eof -> {ok, strtab_none()};
|
|
{error, _Reason} = Error -> Error
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
scan_strtab(String) -> scan_strtab(String, 0, strtab_new()).
|
|
|
|
scan_strtab([], _Offset, StrTab) -> {ok, StrTab};
|
|
scan_strtab([16#0A], _Offset, StrTab) -> {ok, StrTab};
|
|
scan_strtab(String, Offset, StrTab) ->
|
|
case split_strtab(String) of
|
|
{ok, {First, Rest}} ->
|
|
scan_strtab(Rest, Offset + length(First) + 2,
|
|
strtab_insert(StrTab, Offset, First));
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
split_strtab(String) -> split_strtab(String, []).
|
|
|
|
split_strtab([16#2F, 16#0A | Rest], First) ->
|
|
{ok, {lists:reverse(First), Rest}};
|
|
split_strtab([16#2F | _Rest], _First) ->
|
|
{error, invalid_strtab_entry_terminator};
|
|
split_strtab([16#0A | _Rest], _First) ->
|
|
{error, invalid_strtab_entry_terminator};
|
|
split_strtab([Ch | Rest], First) ->
|
|
split_strtab(Rest, [Ch | First]).
|
|
|
|
strtab_none() -> [].
|
|
strtab_new() -> gb_trees:empty().
|
|
strtab_insert(StrTab, Offset, String) -> gb_trees:insert(Offset, String, StrTab).
|
|
strtab_lookup(StrTab, Offset) when StrTab =/= [] ->
|
|
case gb_trees:lookup(Offset, StrTab) of
|
|
{value, String} -> {ok, String};
|
|
none -> false % ar_name doesn't match the start of a strtab entry
|
|
end.
|
|
|
|
%% descriptor-based record I/O =================================================
|
|
|
|
-type read_field() :: fun((pdp10_stdio:file()) -> {ok, term()} | {error, term()}).
|
|
-type write_field() :: fun((pdp10_stdio:file(), term()) -> ok | {error, term()}).
|
|
-type read_tail() :: fun((pdp10_stdio:file()) -> ok | {error, term()}).
|
|
-type write_tail() :: fun((pdp10_stdio:file()) -> ok | {error, term()}).
|
|
|
|
-record(record_desc,
|
|
{ tag :: atom()
|
|
, fields :: [{read_field(), write_field()}]
|
|
, tail :: {read_tail(), write_tail()}
|
|
}).
|
|
|
|
read_record(FP, #record_desc{tag = Tag, fields = Fields, tail = Tail}) ->
|
|
read_record(FP, Fields, Tail, [Tag]).
|
|
|
|
read_record(FP, [{Reader, _Writer} | Fields], Tail, Values) ->
|
|
case Reader(FP) of
|
|
{ok, Value} ->
|
|
read_record(FP, Fields, Tail, [Value | Values]);
|
|
{error, _Reason} = Error ->
|
|
Error
|
|
end;
|
|
read_record(FP, _Fields = [], _Tail = {Reader, _Writer}, Values) ->
|
|
case Reader(FP) of
|
|
ok -> {ok, list_to_tuple(lists:reverse(Values))};
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
write_record(FP, Record, #record_desc{tag = Tag, fields = Fields, tail = Tail}) ->
|
|
[Tag | Values] = tuple_to_list(Record),
|
|
write_record(FP, Fields, Tail, Values).
|
|
|
|
write_record(FP, [{_Reader, Writer} | Fields], Tail, [Value | Values]) ->
|
|
case Writer(FP, Value) of
|
|
ok -> write_record(FP, Fields, Tail, Values);
|
|
{error, _Reason} = Error -> Error
|
|
end;
|
|
write_record(FP, _Fields = [], _Tail = {_Reader, Writer}, _Values = []) ->
|
|
Writer(FP).
|
|
|
|
%% raw archive output ==========================================================
|
|
|
|
arhdr_desc() ->
|
|
7 = record_info(size, arhdr), % assert
|
|
#record_desc{ tag = arhdr
|
|
, fields =
|
|
[ { fun read_ar_name/1, fun write_ar_name/2 } % ar_name
|
|
, { fun read_ar_date/1, fun write_ar_date/2 } % ar_date
|
|
, { fun read_ar_uid/1, fun write_ar_uid/2 } % ar_uid
|
|
, { fun read_ar_gid/1, fun write_ar_gid/2 } % ar_gid
|
|
, { fun read_ar_mode/1, fun write_ar_mode/2 } % ar_mode
|
|
, { fun read_ar_size/1, fun write_ar_size/2 } % ar_size
|
|
]
|
|
, tail =
|
|
{ fun read_ar_fmag/1, fun write_ar_fmag/1 } % ar_fmag
|
|
}.
|
|
|
|
write_arhdr(FP, ArHdr) ->
|
|
write_record(FP, ArHdr, arhdr_desc()).
|
|
|
|
write_ar_date(FP, PosixSecs) ->
|
|
write_number(FP, PosixSecs, 10, 12).
|
|
|
|
write_ar_fmag(FP) ->
|
|
fputs(?PDP10_ARFMAG, FP).
|
|
|
|
write_ar_gid(FP, Gid) ->
|
|
write_number(FP, Gid, 10, 6).
|
|
|
|
write_ar_mag(FP) ->
|
|
fputs(?PDP10_ARMAG, FP).
|
|
|
|
write_ar_mode(FP, Mode) ->
|
|
write_number(FP, Mode, 8, 8).
|
|
|
|
write_ar_name(FP, Name0) ->
|
|
Name =
|
|
case Name0 of
|
|
_ when is_integer(Name0) -> "/" ++ integer_to_list(Name0, 10);
|
|
_ when is_list(Name0) -> Name0 ++ "/"
|
|
end,
|
|
write_string(FP, Name, 16).
|
|
|
|
write_ar_size(FP, Size) ->
|
|
write_number(FP, Size, 10, 10).
|
|
|
|
write_ar_uid(FP, Uid) ->
|
|
write_number(FP, Uid, 10, 6).
|
|
|
|
%% write Number in Base, padding with spaces to exactly FieldSize characters
|
|
write_number(FP, Number, Base, FieldSize) ->
|
|
String = integer_to_list(Number, Base),
|
|
write_string(FP, String, FieldSize).
|
|
|
|
%% write String, padding with spaces to exactly FieldSize characters
|
|
write_string(FP, String, FieldSize) ->
|
|
Length = length(String),
|
|
true = Length =< FieldSize,
|
|
fputs(String ++ lists:duplicate(FieldSize - Length, $\s), FP).
|
|
|
|
fputs(String, FP) ->
|
|
pdp10_stdio:fputs(String, FP).
|
|
|
|
%% raw archive input ===========================================================
|
|
|
|
read_arhdr(FP) ->
|
|
read_record(FP, arhdr_desc()).
|
|
|
|
read_ar_date(FP) ->
|
|
read_number(FP, 10, 12).
|
|
|
|
read_ar_fmag(FP) ->
|
|
case read_string(FP, 2) of
|
|
{ok, ?PDP10_ARFMAG} -> ok;
|
|
{ok, Str} -> {error, {invalid_arfmag, Str}};
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
read_ar_gid(FP) ->
|
|
read_number(FP, 10, 6).
|
|
|
|
read_ar_mag(FP) ->
|
|
case read_string(FP, ?PDP10_SARMAG) of
|
|
{ok, ?PDP10_ARMAG} -> ok;
|
|
{ok, Str} -> {errror, {invalid_armag, Str}};
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
read_ar_mode(FP) ->
|
|
read_number(FP, 8, 8).
|
|
|
|
read_ar_name(FP) ->
|
|
case read_string(FP, 16) of
|
|
{ok, String0} ->
|
|
String = trim_trailing_spaces(String0),
|
|
case String of
|
|
"/" -> {ok, String}; % archive symbol table
|
|
"//" -> {ok, String}; % archive string table
|
|
[$/ | Numeral] -> % offset into archive string table
|
|
strtol(Numeral, 10);
|
|
_ ->
|
|
case string:split(String ++ "$", "/") of
|
|
[FileName, "$"] -> {ok, FileName};
|
|
_ -> {error, {invalid_name, String}}
|
|
end
|
|
end;
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
read_ar_size(FP) ->
|
|
read_number(FP, 10, 10).
|
|
|
|
read_ar_uid(FP) ->
|
|
read_number(FP, 10, 6).
|
|
|
|
read_number(FP, Base, FieldSize) ->
|
|
case read_string(FP, FieldSize) of
|
|
{ok, String} -> strtol(trim_trailing_spaces(String), Base);
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
trim_trailing_spaces(String) ->
|
|
string:trim(String, trailing, [$\s]).
|
|
|
|
strtol(String, Base) ->
|
|
case strtol:parse(String, Base) of
|
|
{ok, {Value, _Rest = []}} -> {ok, Value};
|
|
{ok, {_Value, _Rest}} -> {error, trailing_garbage};
|
|
{error, _Reason} = Error -> Error
|
|
end.
|
|
|
|
%% read FieldSize characters
|
|
read_string(FP, FieldSize) ->
|
|
case pdp10_stdio:fread(1, FieldSize, FP) of
|
|
eof -> {error, eof};
|
|
Result -> Result
|
|
end.
|