Add parsing and serialization functions for Lisa family disk tags

This commit is contained in:
2025-10-04 01:29:03 +01:00
parent 27bb4491bf
commit 7313897064
3 changed files with 523 additions and 1 deletions

View File

@@ -131,7 +131,9 @@ add_library(aaruformat SHARED include/aaruformat/consts.h include/aaruformat/enu
src/checksum/sha1.c src/checksum/sha1.c
include/sha1.h include/sha1.h
src/checksum/sha256.c src/checksum/sha256.c
include/sha256.h) include/sha256.h
src/lisa_tag.c
include/aaruformat/structs/lisa_tag.h)
include_directories(include include/aaruformat 3rdparty/uthash/include 3rdparty/uthash/src) include_directories(include include/aaruformat 3rdparty/uthash/include 3rdparty/uthash/src)

View File

@@ -0,0 +1,95 @@
/*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2025 Natalia Portillo.
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/
/**
* @file lisa_tag.h
* @brief Structure definitions and conversion/serialization function declarations for Lisa family disk tags.
*
* Provides compact C representations for on-disk tag metadata used by Sony, Profile and Priam storage devices
* in the Apple Lisa ecosystem. See lisa_tag.c for detailed description of field semantics and conversion rules.
*/
#ifndef LIBAARUFORMAT_LISA_TAG_H
#define LIBAARUFORMAT_LISA_TAG_H
#include <stdint.h>
#pragma pack(push, 1)
typedef struct sony_tag
{
uint16_t version; ///< 0x00, Lisa OS version number
uint8_t kind : 2; ///< 0x02 bits 7 to 6, kind of info in this block
uint8_t reserved : 6; ///< 0x02 bits 5 to 0, reserved
uint8_t volume; ///< 0x03, disk volume number
int16_t file_id; ///< 0x04, file ID
uint16_t rel_page; ///< 0x06, relative page number
uint16_t next_block; ///< 0x08, next block, 0x7FF if it's last block, 0x8000 set if block is valid
uint16_t prev_block; ///< 0x0A, previous block, 0x7FF if it's first block
} sony_tag;
typedef struct profile_tag
{
uint16_t version; ///< 0x00, Lisa OS version number
uint8_t kind : 2; ///< 0x02 bits 7 to 6, kind of info in this block
uint8_t reserved : 6; ///< 0x02 bits 5 to 0, reserved
uint8_t volume; ///< 0x03, disk volume number
int16_t file_id; ///< 0x04, file ID
uint8_t valid_chk : 1; ///< 0x06 bit 7, checksum valid?
uint16_t used_bytes : 15; ///< 0x06 bits 6 to 0, used bytes in block
uint32_t abs_page; ///< 0x08, 3 bytes, absolute page number
uint8_t checksum; ///< 0x0B, checksum of data
uint16_t rel_page; ///< 0x0C, relative page number
uint32_t next_block; ///< 0x0E, 3 bytes, next block, 0xFFFFFF if it's last block
uint32_t prev_block; ///< 0x11, 3 bytes, previous block, 0xFFFFFF if it's first block
} profile_tag;
typedef struct priam_tag
{
uint16_t version; ///< 0x00, Lisa OS version number
uint8_t kind : 2; ///< 0x02 bits 7 to 6, kind of info in this block
uint8_t reserved : 6; ///< 0x02 bits 5 to 0, reserved
uint8_t volume; ///< 0x03, disk volume number
int16_t file_id; ///< 0x04, file ID
uint8_t valid_chk : 1; ///< 0x06 bit 7, checksum valid?
uint16_t used_bytes : 15; ///< 0x06 bits 6 to 0, used bytes in block
uint32_t abs_page; ///< 0x08, 3 bytes, absolute page number
uint8_t checksum; ///< 0x0B, checksum of data
uint16_t rel_page; ///< 0x0C, relative page number
uint32_t next_block; ///< 0x0E, 3 bytes, next block, 0xFFFFFF if it's last block
uint32_t prev_block; ///< 0x11, 3 bytes, previous block, 0xFFFFFF if it's first block
uint32_t disk_size; ///< 0x14, disk size
} priam_tag;
#pragma pack(pop)
sony_tag bytes_to_sony_tag(const uint8_t *bytes);
profile_tag bytes_to_profile_tag(const uint8_t *bytes);
priam_tag bytes_to_priam_tag(const uint8_t *bytes);
uint8_t *sony_tag_to_bytes(sony_tag tag);
uint8_t *profile_tag_to_bytes(profile_tag tag);
uint8_t *priam_tag_to_bytes(priam_tag tag);
profile_tag sony_tag_to_profile(sony_tag tag);
profile_tag priam_tag_to_profile(priam_tag tag);
priam_tag sony_tag_to_priam(sony_tag tag);
priam_tag profile_tag_to_priam(profile_tag tag);
sony_tag profile_tag_to_sony(profile_tag tag);
sony_tag priam_tag_to_sony(priam_tag tag);
#endif // LIBAARUFORMAT_LISA_TAG_H

425
src/lisa_tag.c Normal file
View File

@@ -0,0 +1,425 @@
/*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2025 Natalia Portillo.
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/
/**
* @file lisa_tag.c
* @brief Parsing, conversion and serialization helpers for Lisa (Apple Lisa / Profile / Priam / Sony)
* disk block tags.
*
* The Lisa operating system and related peripheral disk formats (Sony 3.5" diskettes, ProFile hard disks,
* Priam drives) store per-block (or per-page) metadata in compact on-disk "tag" records. These records use
* mixed-size big-endian integer fields and bit fields to describe allocation chains, relative/absolute page
* numbering, volume numbers, versions, and (for some variants) checksums and disk sizing.
*
* This module provides:
* - Parsing raw big-endian on-disk tag byte sequences into the in-memory structures: ::sony_tag,
* ::profile_tag, ::priam_tag.
* - Lossless (within each format's limits) conversion between the different tag structure variants,
* mapping sentinel values (e.g. 0x7FF <-> 0xFFFFFF for end-of-chain markers) appropriately.
* - Serialization of the in-memory structures back into their canonical on-disk packed byte layouts.
*
* Endianness: All multi-byte numeric fields in the on-disk tag formats are stored big-endian. The helper
* routines perform the necessary byte shifting and masking; the resulting structure fields are in host
* endianness.
*
* Memory management: The serialization functions ( *_tag_to_bytes ) allocate new byte buffers using
* calloc(). The caller owns the returned pointer and must free() it when no longer needed. Parsing
* functions do not allocate dynamic memory; they return structures by value.
*
* Validation: For performance and low-level usage reasons, the parsing routines do not perform NULL
* pointer checks nor length validation on the input byte arrays. Supplying a NULL pointer or insufficient
* bytes is undefined behavior. If such safety is required, perform validation before calling these
* functions.
*
* Sentinel mapping: Sony tag next/prev block fields are 11-bit values (masked with 0x7FF) with the value
* 0x7FF meaning end-of-chain (or no previous). In the wider Profile/Priam formats these become 24-bit
* fields, with 0xFFFFFF used for the same semantic meaning. Conversion helpers transparently translate
* between these sentinel values.
*
* Bit fields: The structures define C bit fields for compact representation (e.g., kind, reserved,
* valid_chk, used_bytes). The parsing and serialization routines reconstruct / pack these manually through
* masking and shifting because on-disk layout may not match compiler bit-field packing. This provides
* portability irrespective of platform-specific bit-field ordering.
*/
#include <stdint.h>
#include "aaruformat/structs/lisa_tag.h"
#include <stdlib.h>
/**
* @brief Parse a 12-byte Sony tag record into a ::sony_tag structure.
*
* Field extraction follows the documented on-disk layout (big-endian). The next/prev block numbers are
* masked to 11 bits (0x7FF) since only those bits are defined in the Sony tag format. The reserved bits
* are preserved in the structure's reserved member for round-tripping if needed.
*
* @param bytes Pointer to at least 12 consecutive bytes containing a raw Sony tag as read from disk.
* @return A populated ::sony_tag structure with host-endian integer fields.
* @warning Behavior is undefined if bytes is NULL or does not point to at least 12 readable bytes.
* @note No validation of version, kind, or chain linkage consistency is performed.
*/
sony_tag bytes_to_sony_tag(const uint8_t *bytes)
{
sony_tag tag = {0};
tag.version = (uint16_t)(bytes[0] << 8 | bytes[1]);
tag.kind = (bytes[2] & 0xC0) >> 6;
tag.reserved = bytes[2] & 0x3F;
tag.volume = bytes[3];
tag.file_id = (int16_t)(bytes[4] << 8 | bytes[5]);
tag.rel_page = (uint16_t)(bytes[6] << 8 | bytes[7]);
tag.next_block = (uint16_t)(bytes[8] << 8 | bytes[9]) & 0x7FF;
tag.prev_block = (uint16_t)(bytes[10] << 8 | bytes[11]) & 0x7FF;
return tag;
}
/**
* @brief Parse a 24-byte Priam tag record into a ::priam_tag structure.
*
* Priam tags extend the Profile tag with an additional 4-byte disk_size field. This routine decodes all bit fields,
* separating the valid checksum flag (bit 7 of byte 6) from the 15-bit used_bytes value.
*
* @param bytes Pointer to at least 24 consecutive bytes containing a raw Priam tag.
* @return A populated ::priam_tag structure.
* @warning Undefined behavior if bytes is NULL or shorter than 24 bytes.
* @note The abs_page, next_block and prev_block 24-bit values are expanded into 32-bit integers in host memory.
*/
priam_tag bytes_to_priam_tag(const uint8_t *bytes)
{
priam_tag tag = {0};
tag.version = (uint16_t)(bytes[0] << 8 | bytes[1]);
tag.kind = (bytes[2] & 0xC0) >> 6;
tag.reserved = bytes[2] & 0x3F;
tag.volume = bytes[3];
tag.file_id = (int16_t)(bytes[4] << 8 | bytes[5]);
tag.used_bytes = (uint16_t)((bytes[6] & 0x7F) << 8 | bytes[7]);
tag.valid_chk = (bytes[6] & 0x80) != 0;
tag.abs_page = (uint32_t)(bytes[8] << 16 | bytes[9] << 8 | bytes[10]);
tag.checksum = bytes[11];
tag.rel_page = (uint16_t)(bytes[12] << 8 | bytes[13]);
tag.next_block = (uint32_t)(bytes[14] << 16 | bytes[15] << 8 | bytes[16]);
tag.prev_block = (uint32_t)(bytes[17] << 16 | bytes[18] << 8 | bytes[19]);
tag.disk_size = (uint32_t)(bytes[20] << 24 | bytes[21] << 16 | bytes[22] << 8 | bytes[23]);
return tag;
}
/**
* @brief Parse a 20-byte Profile tag record into a ::profile_tag structure.
*
* A Profile tag is similar to Priam but without the trailing disk_size field. Multi-byte numeric values are big-endian.
*
* @param bytes Pointer to at least 20 consecutive bytes containing a raw Profile tag.
* @return A populated ::profile_tag structure.
* @warning Undefined behavior if bytes is NULL or shorter than 20 bytes.
*/
profile_tag bytes_to_profile_tag(const uint8_t *bytes)
{
profile_tag tag = {0};
tag.version = (uint16_t)(bytes[0] << 8 | bytes[1]);
tag.kind = (bytes[2] & 0xC0) >> 6;
tag.reserved = bytes[2] & 0x3F;
tag.volume = bytes[3];
tag.file_id = (int16_t)(bytes[4] << 8 | bytes[5]);
tag.used_bytes = (uint16_t)((bytes[6] & 0x7F) << 8 | bytes[7]);
tag.valid_chk = (bytes[6] & 0x80) != 0;
tag.abs_page = (uint32_t)(bytes[8] << 16 | bytes[9] << 8 | bytes[10]);
tag.checksum = bytes[11];
tag.rel_page = (uint16_t)(bytes[12] << 8 | bytes[13]);
tag.next_block = (uint32_t)(bytes[14] << 16 | bytes[15] << 8 | bytes[16]);
tag.prev_block = (uint32_t)(bytes[17] << 16 | bytes[18] << 8 | bytes[19]);
return tag;
}
/**
* @brief Convert a ::sony_tag to a ::profile_tag representation.
*
* Sony tags contain a reduced-width (11-bit) next/prev block. To preserve chain termination semantics, values of
* 0x7FF are translated to 0xFFFFFF (24-bit "no next/prev" sentinel). Other values are copied verbatim into the wider
* fields. Only fields common to both formats are populated; Profile-specific fields (abs_page, checksum, used_bytes,
* valid_chk, reserved) remain zero-initialized.
*
* @param tag Source Sony tag.
* @return Profile tag with mapped values.
*/
profile_tag sony_tag_to_profile(const sony_tag tag)
{
profile_tag profile = {0};
profile.file_id = tag.file_id;
profile.kind = tag.kind;
profile.next_block = tag.next_block == 0x7FF ? 0xFFFFFF : tag.next_block;
profile.prev_block = tag.prev_block == 0x7FF ? 0xFFFFFF : tag.prev_block;
profile.rel_page = tag.rel_page;
profile.version = tag.version;
profile.volume = tag.volume;
return profile;
}
/**
* @brief Convert a ::sony_tag to a ::priam_tag representation.
*
* Similar to sony_tag_to_profile() but returns a Priam tag. Priam-specific fields (abs_page, checksum, used_bytes,
* valid_chk, disk_size, reserved) are zero-initialized. 0x7FF next/prev sentinels expand to 0xFFFFFF.
*
* @param tag Source Sony tag.
* @return Priam tag with mapped values.
*/
priam_tag sony_tag_to_priam(const sony_tag tag)
{
priam_tag priam = {0};
priam.file_id = tag.file_id;
priam.kind = tag.kind;
priam.next_block = tag.next_block == 0x7FF ? 0xFFFFFF : tag.next_block;
priam.prev_block = tag.prev_block == 0x7FF ? 0xFFFFFF : tag.prev_block;
priam.rel_page = tag.rel_page;
priam.version = tag.version;
priam.volume = tag.volume;
return priam;
}
/**
* @brief Convert a ::profile_tag to a ::priam_tag.
*
* Copies all overlapping fields directly. The Priam-only disk_size field remains zero unless modified later.
*
* @param tag Source Profile tag.
* @return Priam tag with copied values.
*/
priam_tag profile_tag_to_priam(const profile_tag tag)
{
priam_tag priam = {0};
priam.abs_page = tag.abs_page;
priam.checksum = tag.checksum;
priam.file_id = tag.file_id;
priam.kind = tag.kind;
priam.next_block = tag.next_block;
priam.prev_block = tag.prev_block;
priam.rel_page = tag.rel_page;
priam.used_bytes = tag.used_bytes;
priam.valid_chk = tag.valid_chk;
priam.version = tag.version;
priam.volume = tag.volume;
return priam;
}
/**
* @brief Convert a ::profile_tag to a ::sony_tag.
*
* Narrows 24-bit next/prev block numbers to 11 bits. A value of 0xFFFFFF (end-of-chain sentinel) is mapped back to
* 0x7FF. Any non-sentinel value is truncated to its lower 11 bits. Fields without Sony equivalents are dropped.
*
* @param tag Source Profile tag.
* @return Sony tag with mapped/narrowed values.
*/
sony_tag profile_tag_to_sony(const profile_tag tag)
{
sony_tag sony = {0};
sony.file_id = tag.file_id;
sony.kind = tag.kind;
sony.next_block = tag.next_block == 0xFFFFFF ? 0x7FF : (uint16_t)tag.next_block & 0x7FF;
sony.prev_block = tag.prev_block == 0xFFFFFF ? 0x7FF : (uint16_t)tag.prev_block & 0x7FF;
sony.rel_page = tag.rel_page;
sony.version = tag.version;
sony.volume = tag.volume;
return sony;
}
/**
* @brief Convert a ::priam_tag to a ::sony_tag.
*
* See profile_tag_to_sony(); Priam-only fields (abs_page, checksum, used_bytes, valid_chk, disk_size) are discarded.
* 0xFFFFFF chain terminators become 0x7FF; other values are truncated to 11 bits.
*
* @param tag Source Priam tag.
* @return Sony tag with mapped values.
*/
sony_tag priam_tag_to_sony(const priam_tag tag)
{
sony_tag sony = {0};
sony.file_id = tag.file_id;
sony.kind = tag.kind;
sony.next_block = tag.next_block == 0xFFFFFF ? 0x7FF : (uint16_t)tag.next_block & 0x7FF;
sony.prev_block = tag.prev_block == 0xFFFFFF ? 0x7FF : (uint16_t)tag.prev_block & 0x7FF;
sony.rel_page = tag.rel_page;
sony.version = tag.version;
sony.volume = tag.volume;
return sony;
}
/**
* @brief Convert a ::priam_tag to a ::profile_tag.
*
* Copies overlapping fields, omitting the Priam-specific disk_size. Useful when reusing Priam metadata in a context
* that expects Profile tag semantics.
*
* @param tag Source Priam tag.
* @return Profile tag with copied values.
*/
profile_tag priam_tag_to_profile(const priam_tag tag)
{
profile_tag profile = {0};
profile.abs_page = tag.abs_page;
profile.checksum = tag.checksum;
profile.file_id = tag.file_id;
profile.kind = tag.kind;
profile.next_block = tag.next_block;
profile.prev_block = tag.prev_block;
profile.rel_page = tag.rel_page;
profile.used_bytes = tag.used_bytes;
profile.valid_chk = tag.valid_chk;
profile.version = tag.version;
profile.volume = tag.volume;
return profile;
}
/**
* @brief Serialize a ::profile_tag into a newly allocated 20-byte big-endian on-disk representation.
*
* Bit/byte packing mirrors bytes_to_profile_tag(). The 24-bit next_block, prev_block and abs_page values are written
* most-significant byte first. The used_bytes high 7 bits and valid_chk flag are packed into the first byte of the
* 16-bit used_bytes field as per on-disk layout.
*
* @param tag Profile tag to serialize.
* @return Pointer to a 20-byte buffer containing the serialized tag, or NULL on allocation failure.
* @retval NULL Allocation failure (caller should check before use).
* @note Caller must free() the returned buffer.
*/
uint8_t *profile_tag_to_bytes(const profile_tag tag)
{
uint8_t *bytes = calloc(1, 20);
if(!bytes) return NULL;
bytes[0] = tag.version >> 8 & 0xFF;
bytes[1] = tag.version & 0xFF;
bytes[2] = (uint8_t)(tag.kind << 6);
bytes[3] = tag.volume;
bytes[4] = tag.file_id >> 8 & 0xFF;
bytes[5] = tag.file_id & 0xFF;
bytes[6] = tag.used_bytes >> 8 & 0x7F;
if(tag.valid_chk) bytes[6] += 0x80;
bytes[7] = tag.used_bytes & 0xFF;
bytes[8] = tag.abs_page >> 16 & 0xFF;
bytes[9] = tag.abs_page >> 8 & 0xFF;
bytes[10] = tag.abs_page & 0xFF;
bytes[11] = tag.checksum;
bytes[12] = tag.rel_page >> 8 & 0xFF;
bytes[13] = tag.rel_page & 0xFF;
bytes[14] = tag.next_block >> 16 & 0xFF;
bytes[15] = tag.next_block >> 8 & 0xFF;
bytes[16] = tag.next_block & 0xFF;
bytes[17] = tag.prev_block >> 16 & 0xFF;
bytes[18] = tag.prev_block >> 8 & 0xFF;
bytes[19] = tag.prev_block & 0xFF;
return bytes;
}
/**
* @brief Serialize a ::priam_tag into a newly allocated 24-byte big-endian on-disk representation.
*
* Layout matches bytes_to_priam_tag(). Includes the final 4-byte disk_size field. Packing rules for used_bytes and
* valid_chk mirror those used in profile_tag_to_bytes().
*
* @param tag Priam tag to serialize.
* @return Pointer to a 24-byte buffer, or NULL on allocation failure.
* @retval NULL Allocation failure.
* @note Caller must free() the returned buffer.
*/
uint8_t *priam_tag_to_bytes(const priam_tag tag)
{
uint8_t *bytes = calloc(1, 24);
if(!bytes) return NULL;
bytes[0] = tag.version >> 8 & 0xFF;
bytes[1] = tag.version & 0xFF;
bytes[2] = (uint8_t)(tag.kind << 6);
bytes[3] = tag.volume;
bytes[4] = tag.file_id >> 8 & 0xFF;
bytes[5] = tag.file_id & 0xFF;
bytes[6] = tag.used_bytes >> 8 & 0x7F;
if(tag.valid_chk) bytes[6] += 0x80;
bytes[7] = tag.used_bytes & 0xFF;
bytes[8] = tag.abs_page >> 16 & 0xFF;
bytes[9] = tag.abs_page >> 8 & 0xFF;
bytes[10] = tag.abs_page & 0xFF;
bytes[11] = tag.checksum;
bytes[12] = tag.rel_page >> 8 & 0xFF;
bytes[13] = tag.rel_page & 0xFF;
bytes[14] = tag.next_block >> 16 & 0xFF;
bytes[15] = tag.next_block >> 8 & 0xFF;
bytes[16] = tag.next_block & 0xFF;
bytes[17] = tag.prev_block >> 16 & 0xFF;
bytes[18] = tag.prev_block >> 8 & 0xFF;
bytes[19] = tag.prev_block & 0xFF;
bytes[20] = tag.disk_size >> 24 & 0xFF;
bytes[21] = tag.disk_size >> 16 & 0xFF;
bytes[22] = tag.disk_size >> 8 & 0xFF;
bytes[23] = tag.disk_size & 0xFF;
return bytes;
}
/**
* @brief Serialize a ::sony_tag into a newly allocated 12-byte big-endian on-disk representation.
*
* The next_block and prev_block fields are masked to 11 bits (0x7FF) during packing. Caller should ensure sentinel
* semantics are already encoded (i.e., use 0x7FF for end-of-chain) prior to calling.
*
* @param tag Sony tag to serialize.
* @return Pointer to a 12-byte buffer, or NULL on allocation failure.
* @retval NULL Allocation failure.
* @note Caller must free() the returned buffer.
*/
uint8_t *sony_tag_to_bytes(sony_tag tag)
{
uint8_t *bytes = calloc(1, 12);
if(!bytes) return NULL;
bytes[0] = tag.version >> 8 & 0xFF;
bytes[1] = tag.version & 0xFF;
bytes[2] = (uint8_t)(tag.kind << 6);
bytes[3] = tag.volume;
bytes[4] = tag.file_id >> 8 & 0xFF;
bytes[5] = tag.file_id & 0xFF;
bytes[6] = tag.rel_page >> 8 & 0xFF;
bytes[7] = tag.rel_page & 0xFF;
bytes[8] = (uint8_t)((tag.next_block & 0x7FF) >> 8);
bytes[9] = (uint8_t)(tag.next_block & 0xFF);
bytes[10] = (uint8_t)((tag.prev_block & 0x7FF) >> 8);
bytes[11] = (uint8_t)(tag.prev_block & 0xFF);
return bytes;
}