File Formats:SAVF

From Try-AS/400
Jump to navigation Jump to search

A SAVF (save file) is the container OS/400 and IBM i write with SAVOBJ and SAVLIB, and read back with RSTOBJ and RSTLIB. It holds Machine Interface objects — programs, files, data areas, libraries — in the form they have on disk, together with a catalogue describing what was saved and from which machine.

Understanding the format is useful well beyond restoring a save file on a machine. It is the only practical way to get at the bytes of an MI object offline: the same byte stream also appears inside optical images and tape images, so a distribution medium, a PTF, or a backup is an extractable archive of real MI objects. What can be done with one once it is extracted is described in File Formats:PGM for programs.

Everything below is big endian, and every text field is EBCDIC.

Three layers

It helps to separate three things that are often run together:

  • the chunk layer, which wraps the save stream in 528-byte units with a checksum on each;
  • the item layer, which is a chain of saved objects, each a 512-byte descriptor followed by that object's segments;
  • the catalogue, a single special item per save that lists the library and its objects.

Only the chunk layer is specific to a save file held in QSYS. When the same save is carried on optical or tape media, the chunk wrapper is absent and the item chain sits in the medium directly. A reader that separates the layers handles all three media with one item parser.

The chunk layer

A save file is exactly chunk count × 528 bytes. Chunk n, counting from zero, begins at file offset n × 528 and consists of 512 bytes of save-stream data followed by a 16-byte checksum. The logical save stream is the concatenation of the data halves, so translating a stream offset to a file offset is

file_offset = (stream_offset / 512) * 528 + (stream_offset % 512)

File header

The first 512 bytes of the stream — which are also the data half of chunk 1 — describe the container.

Offset Size Field Notes
+0x00 8 MI creation timestamp Also the seed for every chunk checksum.
+0x08 4 Chunk count 32-bit form.
+0x10 8 Machine type and model EBCDIC, model first: 42A 8286 is an 8286-42A.
+0x30 4 Release level
+0x34 4 Header length The first item starts here. Zero means 512.
+0x38 8 Chunk count 64-bit form, used when the 32-bit field would overflow.
+0x40 16 Global checksum backup See below.

A valid file satisfies chunk count == file size / 528; anything else means truncation. Because the header is itself chunk 1, verifying its checksum is the cheapest reliable test of whether a file is a save file at all.

The global checksum displaces real data

This is the trap worth knowing about before writing any extractor. The last 16 bytes of the logical stream, at chunk count × 512 − 16, are overwritten with a whole-file checksum. The data bytes they replaced are preserved in the file header at +0x40, and a reader must put them back. A reader that does not will silently corrupt the tail of the last object it extracts, in a way that no chunk checksum will catch, because the chunk checksums are computed over the overwritten bytes.

The chunk checksum

Each chunk's 16-byte trailer is the chunk number, counting from one, as a 4-byte big-endian integer, followed by a 12-byte digest. The digest is two folding passes built from a single primitive:

chkadd(a, b):          # b is exactly one byte longer than a
    b[1:] += a << 1    # big-endian magnitudes, carry propagated
    b[0]  += carry + (the bit shifted out of the top of a)

The first pass takes the chunk number followed by the 512 data bytes and cuts them into eight parts of 61, 62, 63, 64, 65, 66, 67 and 68 bytes, then folds them in ascending order: chkadd of the 61-byte part into the 62-byte part, that into the 63-byte part, and so on. The second pass takes the resulting 68 bytes, cuts them into parts of 5, 6, 7, 8, 9, 10, 11 and 12 bytes, and folds them the same way, leaving 12 bytes. The first 8 of those are then XORed with the file header's MI timestamp.

There is one non-obvious detail. In both passes the parts are filled from the input in the order 61, 62, 63, 64, 65, 66, 68, 67 — and 5, 6, 7, 8, 9, 10, 12, 11 — while being folded in ascending order. An implementation that fills them in plain ascending order produces a plausible-looking wrong answer.

The construction is a checksum, not a cryptographic hash. It detects media damage. It is not a tamper seal, and it carries no secret beyond a timestamp printed in the same header.

The item layer

Items are chained: parse the descriptor at the header length, add the item's computed length, repeat, and stop when the next item would begin past (chunk count − 1) × 512. Every descriptor begins with FFFFFFFF, which makes resynchronising after damage feasible.

Item descriptor

Offset Size Field Notes
+0x00 4 FFFFFFFF Eyecatcher.
+0x04 10 Object name EBCDIC. Replaced by a file ID for integrated file system objects.
+0x0E 20 Member name Empty unless the object has members.
+0x22 2 MI object type The same code space used throughout the machine: 0x0201 *PGM, 0x0401 *LIB, 0x1901 *FILE, 0x19DB *SRDS.
+0x24 1 Flags Bit 0x40 or 0x80 means the item is an IFS object.
+0x25 3 Catalogue offset Where this object's entry sits inside the catalogue.
+0x44 4 Header occupation Bytes of the descriptor page actually used.
+0x48 4 Stored length In 512-byte units, minus one.
+0x54 2 Release created for Governs the section-entry layout below.
+0x56 4 Metadata length For a catalogue item, the size of the catalogue.
+0x64 4 Section count
+0xC0 8 Save timestamp
+0xCC 4 Total length In 512-byte units.
+0xD4 4 Data length In 512-byte units. Always the total length less 4096 bytes.
+0x100 14 × 8 Pointer array See below.
+0x170 8 Packed length Stored as the length less 512.

The pointer array holds single-level store addresses, but only the low 24 bits matter: they are an offset into the item, biased by 4096, so (pointer & 0xFFFFFF) − 4096 gives an offset within the descriptor page. Index 1 locates the section length table and index 8 the section type table.

Sections

A saved MI object is a set of segments, and each segment is one section of the item. Sections start at the first 4096-byte boundary after the descriptor and run consecutively, which is why the data length is always the total length less one page.

The length table has one entry per section. On any modern release — when the release created for field exceeds 32 — an entry is 16 bytes: capacity, length, and the segment's single-level store address. Older saves use a 10-byte entry with a shifted 6-byte address and no separate length.

The type table has one 8-byte entry per section, of which the first halfword is the segment type. This is the same value as the first halfword of the segment's own segment header, which is a useful consistency check when extracting: the item's idea of what a section is must agree with the segment's own.

Item length and compression

An uncompressed item occupies its stored length. A packed item records a non-zero packed length, and the writer then realigns the following item on the enclosing 512-byte boundary, so the step from one item to the next is

if packed == 512:        step = stored
elif packed > stored:    step = packed
else:
    slack  = stored - packed
    within = item_offset % 512
    if within == 0:      step = stored
    elif within < slack: step = packed + slack - within
    else:                step = stored + 512 - within

The compression method is identifiable from the first six bytes of the packed data, which are EBCDIC eyecatchers:

Bytes EBCDIC DTACPR
D3C46DE3D9E2 LD_TRS *MEDIUM, the TERSE algorithm
D3C46DD3E9F1 LD_LZ1 *HIGH, IBM's LZ1
D3C46DE9D3C2 LD_ZLB *ZLIB; the deflate stream begins 12 bytes in
none *LOW, an SNA-style run-length scheme, recognisable only by decoding it

Note that these are not the compression used for a compressed LID payload; see Data Structures:LID, where all four of these were tried and rejected.

The catalogue

Every save writes one save/restore descriptor space item, MI type 0x19DB, named QSRDSSPC.n, ahead of the objects it describes. Items named QSR.ADDITIONAL_INFO.* or QSRDSSPC.EA SRD* are continuation blocks belonging to the preceding descriptor, not new saves.

The catalogue itself is the last metadata length bytes of that item's data. Its first byte is the save command, and that selects the shape: a library save has the layout below, while a save of the integrated file system produces a different, tree-structured descriptor.

Offset Size Field
+0x00 1 Save command
+0x01 1 Target release
+0x02 30 Saved library name
+0x20 2 Base object type, 0x0401
+0x22 8 Save timestamp
+0x2A 4 Object count
+0x34 8 System serial number
+0x3C 2 ASP number
+0x3E 10 Create authority
+0x4C 1 Save-while-active indicator
+0x50 10 ASP name, target release 66 and later only

Object descriptors follow, at +0x50 or at +0x70 when the ASP name is present. Each is 151 bytes on target release 49 and later, and 87 bytes before that. The first fields are the object name at +0x00 for 30 bytes, the MI object type at +0x1E, and the owning user profile at +0x20 for 10 bytes. The size is a 4-byte field at +0x43, multiplied by a second 4-byte field at +0x79 when that is non-zero. The remainder of the descriptor holds offsets, into the same buffer, of chains of length-prefixed records carrying the object's text description, service description, usage information, work-unit references and user information.

Worth noticing for its own sake: the catalogue records the serial number of the machine that produced the save. A save file is not anonymous.

Practical consequences

  • Any MI object can be lifted out of a save file byte for byte, with its segments separated and their segment identifiers intact. That is what makes offline analysis of a program object possible without console access to a machine and without printing a storage dump.
  • Verifying the chunk checksums is a genuine integrity check on an archived save file, and can be done without a machine.
  • The item chain and the catalogue are independent descriptions of the same set of objects. Where they disagree, the file has been damaged or truncated.
  • Because the chunk layer is only present for a save file held in QSYS, a save carved out of an optical or tape image is parsed by the same code with the chunk translation switched off.

Scope of this description

The layout was mapped with reference to jSAVF, an independently developed third-party reader, and then verified field by field against a real V7R4 save file. Where an object in that file also had a printed Display/Alter/Dump available, the bytes extracted here matched the printout exactly. The chunk checksum was reimplemented from scratch and verifies every chunk of the test file.

Not established: the meaning of the 4-byte field at +0x0C of the file header, the layout of the integrated file system catalogue, and the LZ1 and TERSE decompressors.

Weblinks

  • jSAVF — a maintained third-party reader for save files, and the starting point for the layout described here.