Reading an AtomVM packbeam archive.
A packbeam is what AtomVM runs: a flat archive of compiled BEAM modules and data files, mounted by the VM rather than unpacked. It is the firmware NervesHub manages for an AtomVM device — not the ESP-IDF image underneath, which is AtomVM itself and is updated on its own schedule.
0x00 "#!/usr/bin/env AtomVM\n\0\0" 24 bytes, doubles as a shebang
0x18 entry, entry, ... until the terminator
size:32 flags:32 reserved:32 name\0 (padded to 4) data
0:32 0:32 0:32 "end\0"
size covers the whole entry, header included, so walking is
Offset + Size.
NervesHub derives a firmware's UUID from the SHA-256 of the archive as uploaded, so a device reporting a digest has to hash exactly the same bytes. A partition is larger than the archive written into it, which makes the archive's own length the thing that has to be recovered rather than assumed.
It is recoverable exactly. packbeam_api:write_packbeam/2 ends every archive
with create_header(0, 0, <<"end">>) — a 12 byte zeroed header
followed by a 4 byte "end\0" — so the archive ends 16 bytes after the
terminator starts. See byte_length/1.
binary_to_term/1. That is safe here
and is not on the server: this reads the device's own firmware, whose atoms
are by definition already loaded, while NervesHub reads uploads from anyone
and cannot afford to intern atoms out of them.
entry() = #{size := pos_integer(), flags := non_neg_integer(), name := binary(), data_offset := pos_integer(), data := binary(), offset := non_neg_integer()}
header() = #{size := pos_integer(), flags := non_neg_integer(), name := binary(), data_offset := pos_integer()}
| application/1 | The application this archive is, from a whole archive in memory. |
| application_from_data/1 | Decode the contents of an application.bin entry. |
| byte_length/1 | The length of the archive itself, ignoring whatever follows it. |
| entry_header/1 | Parse one entry header. |
| header_size/0 | |
| is_application_entry/1 | Whether an entry name is an application spec. |
| magic/0 | The 24 bytes every packbeam starts with. |
| magic_size/0 | |
| scan/1 | Walk a whole archive, returning every entry in file order. |
| terminator_size/0 | |
| window_size/0 | How many bytes entry_header/1 needs to see. |
application(Archive::binary()) -> {ok, map()} | {error, term()}
The application this archive is, from a whole archive in memory.
The first application spec wins. An archive built from a project with dependencies holds one per application and nothing marks the root, butatomvm_rebar3_plugin writes the project's own ahead of the dependency
archives it appends, so first is the project's.
application_from_data(X1::binary()) -> {ok, map()} | {error, term()}
Decode the contents of an application.bin entry.
{application, Name, Props} in the external term
format.
byte_length(Bin::binary()) -> {ok, pos_integer()} | {error, term()}
The length of the archive itself, ignoring whatever follows it.
This is the byte range NervesHub hashed when the archive was uploaded, so it is the range a device has to hash to report a matching digest. See the note above about how it is recovered.entry_header(Bin::binary()) -> {ok, header()} | terminator | {error, term()}
Parse one entry header.
Bin starts at the entry and must run to the end of the name — pass
window_size() bytes, or the rest of the archive if less remains.
terminator at the end of the archive. A zero size or a zero flags
word both end it: the terminator has both, and neither can occur on a real
entry.
header_size() -> pos_integer()
is_application_entry(Name::binary()) -> boolean()
Whether an entry name is an application spec.
Matchesatomvm_packbeam:is_application_file/1: the name splits into exactly
three components, rather than any path that happens to end the same way.
magic() -> binary()
The 24 bytes every packbeam starts with.
magic_size() -> pos_integer()
scan(Bin::binary()) -> {ok, [entry()]} | {error, term()}
Walk a whole archive, returning every entry in file order.
Each entry carries its data. For a partition, where holding the archive in memory is the thing to avoid, walk withentry_header/1 instead and read
only the entry that is wanted.
terminator_size() -> pos_integer()
window_size() -> pos_integer()
How many bytes entry_header/1 needs to see.
Generated by EDoc