diff --git a/CMakeLists.txt b/CMakeLists.txt
index 754da45..493877e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -131,7 +131,9 @@ add_library(aaruformat SHARED include/aaruformat/consts.h include/aaruformat/enu
src/checksum/sha1.c
include/sha1.h
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)
diff --git a/include/aaruformat/structs/lisa_tag.h b/include/aaruformat/structs/lisa_tag.h
new file mode 100644
index 0000000..96ebb23
--- /dev/null
+++ b/include/aaruformat/structs/lisa_tag.h
@@ -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 .
+ */
+
+/**
+ * @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
+
+#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
diff --git a/src/lisa_tag.c b/src/lisa_tag.c
new file mode 100644
index 0000000..9f4fa35
--- /dev/null
+++ b/src/lisa_tag.c
@@ -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 .
+ */
+
+/**
+ * @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
+
+#include "aaruformat/structs/lisa_tag.h"
+
+#include
+
+/**
+ * @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;
+}
\ No newline at end of file