Module nh_packbeam

Reading an AtomVM packbeam archive.

Description

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.

Byte length

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.

Terms

Application metadata is decoded with 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.

Data Types

entry()

entry() = #{size := pos_integer(), flags := non_neg_integer(), name := binary(), data_offset := pos_integer(), data := binary(), offset := non_neg_integer()}

header()

header() = #{size := pos_integer(), flags := non_neg_integer(), name := binary(), data_offset := pos_integer()}

Function Index

application/1The application this archive is, from a whole archive in memory.
application_from_data/1Decode the contents of an application.bin entry.
byte_length/1The length of the archive itself, ignoring whatever follows it.
entry_header/1Parse one entry header.
header_size/0
is_application_entry/1Whether an entry name is an application spec.
magic/0The 24 bytes every packbeam starts with.
magic_size/0
scan/1Walk a whole archive, returning every entry in file order.
terminator_size/0
window_size/0How many bytes entry_header/1 needs to see.

Function Details

application/1

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, but atomvm_rebar3_plugin writes the project's own ahead of the dependency archives it appends, so first is the project's.

application_from_data/1

application_from_data(X1::binary()) -> {ok, map()} | {error, term()}

Decode the contents of an application.bin entry.

A 4 byte length, then {application, Name, Props} in the external term format.

byte_length/1

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/1

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.

Returns 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/0

header_size() -> pos_integer()

is_application_entry/1

is_application_entry(Name::binary()) -> boolean()

Whether an entry name is an application spec.

Matches atomvm_packbeam:is_application_file/1: the name splits into exactly three components, rather than any path that happens to end the same way.

magic/0

magic() -> binary()

The 24 bytes every packbeam starts with.

magic_size/0

magic_size() -> pos_integer()

scan/1

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 with entry_header/1 instead and read only the entry that is wanted.

terminator_size/0

terminator_size() -> pos_integer()

window_size/0

window_size() -> pos_integer()

How many bytes entry_header/1 needs to see.

Only relevant when walking a partition a chunk at a time, where the caller chooses how much to read.


Generated by EDoc