Task 016: .ADZ File Format

Task 016: .ADZ File Forma

File Format Specifications for the .ADZ File Format

The .ADZ file format is a compressed Amiga Disk File (ADF) image, where the compression is applied using the GZIP algorithm. The uncompressed content is an ADF file, which is a raw sector-by-sector dump of an Amiga floppy disk, typically 901120 bytes in size for a standard double-density (DD) floppy disk (80 cylinders, 2 heads, 11 sectors per track, 512 bytes per sector). The file system within the ADF is the AmigaDOS Old File System (OFS) or Fast File System (FFS), with optional extensions for international mode (INTL) and directory cache (DIRC).

The .ADZ format itself follows the GZIP specification: it begins with a GZIP header (magic bytes 0x1F 0x8B, compression method 0x08 for deflate, flags, modification time, extra flags, OS type, optional extra fields, filename, comment, header CRC), followed by the compressed ADF data, and ends with a CRC32 and uncompressed size.

  1. List of all the properties of this file format intrinsic to its file system:

Based on the logical structure of the AmigaDOS file system within the uncompressed ADF data, the intrinsic properties are derived from the key blocks (boot block, root block, bitmap block, etc.). These include:

  • DOS Identifier (from boot block: string 'DOS')
  • DOS Flags (from boot block: byte indicating file system type - bit 0: FFS if set, bit 1: INTL if set, bit 2: DIRC if set)
  • Boot Checksum (ulong from boot block)
  • Root Block Pointer (ulong from boot block)
  • Boot Code Present (boolean from boot block: true if non-zero bytes after offset 12)
  • Type (ulong from root block: T_HEADER = 2)
  • Header Key (ulong from root block: unused, 0)
  • Hash Table Size (ulong from root block)
  • First Data (ulong from root block: unused, 0)
  • Root Checksum (ulong from root block)
  • Hash Table (array of ulongs from root block: pointers to entry blocks)
  • Bitmap Flag (ulong from root block: -1 if valid)
  • Bitmap Pages (array of ulongs from root block: bitmap block pointers, up to 25)
  • Bitmap Extension (ulong from root block: first bitmap extension block)
  • Last Root Alteration Date (days, mins, ticks from root block)
  • Volume Name Length (char from root block)
  • Volume Name (string from root block, max 30 chars)
  • Last Volume Alteration Date (days, mins, ticks from root block)
  • Creation Date (days, mins, ticks from root block)
  • First Directory Cache Block (ulong from root block, for FFS)
  • Secondary Type (ulong from root block: ST_ROOT = 1)
  • Bitmap Checksum (long from bitmap block)
  • Free Blocks (integer: count of free blocks from bitmap map)
  • Allocated Blocks (integer: total blocks minus free blocks minus reserved)
  1. Two direct download links for files of format .ADZ:
  1. Ghost blog embedded HTML JavaScript for drag and drop .ADZ file to dump properties:
Drag and drop .ADZ file here
  1. Python class for .ADZ file:
import gzip
import struct
import datetime

class ADZFile:
    def __init__(self, filename):
        with open(filename, 'rb') as f:
            compressed = f.read()
        self.data = gzip.decompress(compressed)
        self.properties = self._parse_properties()

    def _parse_properties(self):
        properties = {}
        # Boot block
        dos_id = struct.unpack('>3s', self.data[0:3])[0].decode()
        properties['DOS Identifier'] = dos_id
        dos_flags = self.data[3]
        properties['DOS Flags'] = dos_flags
        properties['File System Type'] = ('FFS' if dos_flags & 1 else 'OFS') + (' INTL' if dos_flags & 2 else '') + (' DIRC' if dos_flags & 4 else '')
        properties['Boot Checksum'] = hex(struct.unpack('>I', self.data[4:8])[0])
        root_block = struct.unpack('>I', self.data[8:12])[0]
        properties['Root Block Pointer'] = root_block
        boot_code_present = any(self.data[i] != 0 for i in range(12, 1024))
        properties['Boot Code Present'] = boot_code_present
        root_offset = root_block * 512
        # Root block
        properties['Type'] = struct.unpack('>I', self.data[root_offset:root_offset+4])[0]
        properties['Header Key'] = struct.unpack('>I', self.data[root_offset+4:root_offset+8])[0]
        properties['Hash Table Size'] = struct.unpack('>I', self.data[root_offset+12:root_offset+16])[0]
        properties['First Data'] = struct.unpack('>I', self.data[root_offset+16:root_offset+20])[0]
        properties['Root Checksum'] = hex(struct.unpack('>I', self.data[root_offset+20:root_offset+24])[0])
        hash_table = []
        for i in range(properties['Hash Table Size']):
            ptr = struct.unpack('>I', self.data[root_offset+24 + i*4 : root_offset+28 + i*4])[0]
            if ptr != 0:
                hash_table.append(ptr)
        properties['Hash Table'] = hash_table
        properties['Bitmap Flag'] = struct.unpack('>i', self.data[root_offset+312:root_offset+316])[0]
        bm_pages = []
        for i in range(25):
            ptr = struct.unpack('>I', self.data[root_offset+316 + i*4 : root_offset+320 + i*4])[0]
            if ptr != 0:
                bm_pages.append(ptr)
        properties['Bitmap Pages'] = bm_pages
        properties['Bitmap Extension'] = struct.unpack('>I', self.data[root_offset+416:root_offset+420])[0]
        r_days, r_mins, r_ticks = struct.unpack('>III', self.data[root_offset+420:root_offset+432])
        r_date = datetime.date(1978, 1, 1) + datetime.timedelta(days=r_days) + datetime.timedelta(minutes=r_mins) + datetime.timedelta(seconds=r_ticks / 50)
        properties['Last Root Alteration Date'] = r_date.isoformat()
        name_len = self.data[root_offset+432]
        volume_name = self.data[root_offset+433:root_offset+433+name_len].decode()
        properties['Volume Name Length'] = name_len
        properties['Volume Name'] = volume_name
        v_days, v_mins, v_ticks = struct.unpack('>III', self.data[root_offset+472:root_offset+484])
        v_date = datetime.date(1978, 1, 1) + datetime.timedelta(days=v_days) + datetime.timedelta(minutes=v_mins) + datetime.timedelta(seconds=v_ticks / 50)
        properties['Last Volume Alteration Date'] = v_date.isoformat()
        c_days, c_mins, c_ticks = struct.unpack('>III', self.data[root_offset+484:root_offset+496])
        c_date = datetime.date(1978, 1, 1) + datetime.timedelta(days=c_days) + datetime.timedelta(minutes=c_mins) + datetime.timedelta(seconds=c_ticks / 50)
        properties['Creation Date'] = c_date.isoformat()
        properties['First Directory Cache Block'] = struct.unpack('>I', self.data[root_offset+496:root_offset+500])[0]
        properties['Secondary Type'] = struct.unpack('>I', self.data[root_offset+508:root_offset+512])[0]
        # Bitmap block
        bitmap_offset = bm_pages[0] * 512
        properties['Bitmap Checksum'] = hex(struct.unpack('>I', self.data[bitmap_offset:bitmap_offset+4])[0])
        free_blocks = 0
        for i in range(127):
            word = struct.unpack('>I', self.data[bitmap_offset+4 + i*4 : bitmap_offset+8 + i*4])[0]
            free_blocks += bin(word).count('1')
        properties['Free Blocks'] = free_blocks
        properties['Allocated Blocks'] = 1760 - free_blocks - 2  # For DD floppy
        return properties

    def print_properties(self):
        for k, v in self.properties.items():
            print(f"{k}: {v}")

    def write(self, filename):
        with gzip.open(filename, 'wb', compresslevel=9) as f:
            f.write(self.data)

# Example usage: adz = ADZFile('example.adz'); adz.print_properties(); adz.write('modified.adz')
  1. Java class for .ADZ file:
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Date;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

public class ADZFile {
    private byte[] data;
    private Map<String, Object> properties;

    public ADZFile(String filename) throws IOException {
        try (FileInputStream fis = new FileInputStream(filename);
             ByteArrayOutputStream baos = new ByteArrayOutputStream();
             GZIPInputStream gis = new GZIPInputStream(fis)) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = gis.read(buffer)) > 0) {
                baos.write(buffer, 0, len);
            }
            data = baos.toByteArray();
        }
        properties = parseProperties();
    }

    private Map<String, Object> parseProperties() {
        Map<String, Object> props = new HashMap<>();
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
        // Boot block
        props.put("DOS Identifier", new String(data, 0, 3));
        int dosFlags = data[3] & 0xFF;
        props.put("DOS Flags", dosFlags);
        String fsType = (dosFlags & 1) != 0 ? "FFS" : "OFS";
        fsType += (dosFlags & 2) != 0 ? " INTL" : "";
        fsType += (dosFlags & 4) != 0 ? " DIRC" : "";
        props.put("File System Type", fsType);
        props.put("Boot Checksum", Integer.toHexString(bb.getInt(4)));
        int rootBlock = bb.getInt(8);
        props.put("Root Block Pointer", rootBlock);
        boolean bootCodePresent = false;
        for (int i = 12; i < 1024; i++) {
            if (data[i] != 0) {
                bootCodePresent = true;
                break;
            }
        }
        props.put("Boot Code Present", bootCodePresent);
        int rootOffset = rootBlock * 512;
        // Root block
        props.put("Type", bb.getInt(rootOffset));
        props.put("Header Key", bb.getInt(rootOffset + 4));
        int hashTableSize = bb.getInt(rootOffset + 12);
        props.put("Hash Table Size", hashTableSize);
        props.put("First Data", bb.getInt(rootOffset + 16));
        props.put("Root Checksum", Integer.toHexString(bb.getInt(rootOffset + 20)));
        List<Integer> hashTable = new ArrayList<>();
        for (int i = 0; i < hashTableSize; i++) {
            int ptr = bb.getInt(rootOffset + 24 + i * 4);
            if (ptr != 0) hashTable.add(ptr);
        }
        props.put("Hash Table", hashTable);
        props.put("Bitmap Flag", bb.getInt(rootOffset + 312));
        List<Integer> bmPages = new ArrayList<>();
        for (int i = 0; i < 25; i++) {
            int ptr = bb.getInt(rootOffset + 316 + i * 4);
            if (ptr != 0) bmPages.add(ptr);
        }
        props.put("Bitmap Pages", bmPages);
        props.put("Bitmap Extension", bb.getInt(rootOffset + 416));
        int rDays = bb.getInt(rootOffset + 420);
        int rMins = bb.getInt(rootOffset + 424);
        int rTicks = bb.getInt(rootOffset + 428);
        Calendar rCal = Calendar.getInstance();
        rCal.set(1978, 0, 1, 0, 0, 0);
        rCal.add(Calendar.DATE, rDays);
        rCal.add(Calendar.MINUTE, rMins);
        rCal.add(Calendar.MILLISECOND, rTicks * 20);
        props.put("Last Root Alteration Date", rCal.getTime());
        int nameLen = data[rootOffset + 432] & 0xFF;
        props.put("Volume Name Length", nameLen);
        String volumeName = new String(data, rootOffset + 433, nameLen);
        props.put("Volume Name", volumeName);
        int vDays = bb.getInt(rootOffset + 472);
        int vMins = bb.getInt(rootOffset + 476);
        int vTicks = bb.getInt(rootOffset + 480);
        Calendar vCal = Calendar.getInstance();
        vCal.set(1978, 0, 1, 0, 0, 0);
        vCal.add(Calendar.DATE, vDays);
        vCal.add(Calendar.MINUTE, vMins);
        vCal.add(Calendar.MILLISECOND, vTicks * 20);
        props.put("Last Volume Alteration Date", vCal.getTime());
        int cDays = bb.getInt(rootOffset + 484);
        int cMins = bb.getInt(rootOffset + 488);
        int cTicks = bb.getInt(rootOffset + 492);
        Calendar cCal = Calendar.getInstance();
        cCal.set(1978, 0, 1, 0, 0, 0);
        cCal.add(Calendar.DATE, cDays);
        cCal.add(Calendar.MINUTE, cMins);
        cCal.add(Calendar.MILLISECOND, cTicks * 20);
        props.put("Creation Date", cCal.getTime());
        props.put("First Directory Cache Block", bb.getInt(rootOffset + 496));
        props.put("Secondary Type", bb.getInt(rootOffset + 508));
        // Bitmap block
        int bitmapOffset = bmPages.get(0) * 512;
        props.put("Bitmap Checksum", Integer.toHexString(bb.getInt(bitmapOffset)));
        int freeBlocks = 0;
        for (int i = 0; i < 127; i++) {
            int word = bb.getInt(bitmapOffset + 4 + i * 4);
            freeBlocks += Integer.bitCount(word);
        }
        props.put("Free Blocks", freeBlocks);
        props.put("Allocated Blocks", 1760 - freeBlocks - 2);
        return props;
    }

    public void printProperties() {
        properties.forEach((k, v) -> System.out.println(k + ": " + v));
    }

    public void write(String filename) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(filename);
             GZIPOutputStream gos = new GZIPOutputStream(fos)) {
            gos.write(data);
        }
    }

    // Example: ADZFile adz = new ADZFile("example.adz"); adz.printProperties(); adz.write("modified.adz");
}
  1. JavaScript class for .ADZ file:
const pako = require('pako'); // Assume Node.js with pako installed, or include in browser

class ADZFile {
  constructor(buffer) {
    this.data = pako.inflate(new Uint8Array(buffer));
    this.properties = this.parseProperties();
  }

  parseProperties() {
    const properties = {};
    const dv = new DataView(this.data.buffer);
    // Boot block
    properties['DOS Identifier'] = String.fromCharCode(dv.getUint8(0), dv.getUint8(1), dv.getUint8(2));
    const dosFlags = dv.getUint8(3);
    properties['DOS Flags'] = dosFlags;
    properties['File System Type'] = (dosFlags & 1 ? 'FFS' : 'OFS') + (dosFlags & 2 ? ' INTL' : '') + (dosFlags & 4 ? ' DIRC' : '');
    properties['Boot Checksum'] = dv.getUint32(4).toString(16);
    const rootBlock = dv.getUint32(8);
    properties['Root Block Pointer'] = rootBlock;
    let bootCodePresent = false;
    for (let i = 12; i < 1024; i++) {
      if (dv.getUint8(i) !== 0) {
        bootCodePresent = true;
        break;
      }
    }
    properties['Boot Code Present'] = bootCodePresent;
    const rootOffset = rootBlock * 512;
    // Root block
    properties['Type'] = dv.getUint32(rootOffset);
    properties['Header Key'] = dv.getUint32(rootOffset + 4);
    const hashTableSize = dv.getUint32(rootOffset + 12);
    properties['Hash Table Size'] = hashTableSize;
    properties['First Data'] = dv.getUint32(rootOffset + 16);
    properties['Root Checksum'] = dv.getUint32(rootOffset + 20).toString(16);
    const hashTable = [];
    for (let i = 0; i < hashTableSize; i++) {
      const ptr = dv.getUint32(rootOffset + 24 + i * 4);
      if (ptr !== 0) hashTable.push(ptr);
    }
    properties['Hash Table'] = hashTable;
    properties['Bitmap Flag'] = dv.getInt32(rootOffset + 312);
    const bmPages = [];
    for (let i = 0; i < 25; i++) {
      const ptr = dv.getUint32(rootOffset + 316 + i * 4);
      if (ptr !== 0) bmPages.push(ptr);
    }
    properties['Bitmap Pages'] = bmPages;
    properties['Bitmap Extension'] = dv.getUint32(rootOffset + 416);
    const rDays = dv.getUint32(rootOffset + 420);
    const rMins = dv.getUint32(rootOffset + 424);
    const rTicks = dv.getUint32(rootOffset + 428);
    const rDate = new Date(1978, 0, 1);
    rDate.setDate(rDate.getDate() + rDays);
    rDate.setMinutes(rDate.getMinutes() + rMins);
    rDate.setMilliseconds(rDate.getMilliseconds() + rTicks * 20);
    properties['Last Root Alteration Date'] = rDate.toISOString();
    const nameLen = dv.getUint8(rootOffset + 432);
    properties['Volume Name Length'] = nameLen;
    let volumeName = '';
    for (let i = 0; i < nameLen; i++) {
      volumeName += String.fromCharCode(dv.getUint8(rootOffset + 433 + i));
    }
    properties['Volume Name'] = volumeName;
    const vDays = dv.getUint32(rootOffset + 472);
    const vMins = dv.getUint32(rootOffset + 476);
    const vTicks = dv.getUint32(rootOffset + 480);
    const vDate = new Date(1978, 0, 1);
    vDate.setDate(vDate.getDate() + vDays);
    vDate.setMinutes(vDate.getMinutes() + vMins);
    vDate.setMilliseconds(vDate.getMilliseconds() + vTicks * 20);
    properties['Last Volume Alteration Date'] = vDate.toISOString();
    const cDays = dv.getUint32(rootOffset + 484);
    const cMins = dv.getUint32(rootOffset + 488);
    const cTicks = dv.getUint32(rootOffset + 492);
    const cDate = new Date(1978, 0, 1);
    cDate.setDate(cDate.getDate() + cDays);
    cDate.setMinutes(cDate.getMinutes() + cMins);
    cDate.setMilliseconds(cDate.getMilliseconds() + cTicks * 20);
    properties['Creation Date'] = cDate.toISOString();
    properties['First Directory Cache Block'] = dv.getUint32(rootOffset + 496);
    properties['Secondary Type'] = dv.getUint32(rootOffset + 508);
    // Bitmap block
    const bitmapOffset = bmPages[0] * 512;
    properties['Bitmap Checksum'] = dv.getUint32(bitmapOffset).toString(16);
    let freeBlocks = 0;
    for (let i = 0; i < 127; i++) {
      let word = dv.getUint32(bitmapOffset + 4 + i * 4);
      while (word) {
        freeBlocks += word & 1;
        word >>>= 1;
      }
    }
    properties['Free Blocks'] = freeBlocks;
    properties['Allocated Blocks'] = 1760 - freeBlocks - 2;
    return properties;
  }

  printProperties() {
    console.log(this.properties);
  }

  write() {
    return pako.deflate(this.data);
  }
}

// Example (Node.js): const fs = require('fs'); const buffer = fs.readFileSync('example.adz'); const adz = new ADZFile(buffer); adz.printProperties(); fs.writeFileSync('modified.adz', adz.write());
  1. C "class" (struct with functions) for .ADZ file:
#include <stdio.h>
#include <stdlib.h>
#include <zlib.h>
#include <string.h>
#include <time.h>

typedef struct {
    unsigned char *data;
    size_t size;
    // Properties map simulation (array of key-value)
    char *keys[50];
    char *values[50];
    int prop_count;
} ADZFile;

void adz_init(ADZFile *adz, const char *filename) {
    FILE *f = fopen(filename, "rb");
    fseek(f, 0, SEEK_END);
    size_t compressed_size = ftell(f);
    fseek(f, 0, SEEK_SET);
    unsigned char *compressed = malloc(compressed_size);
    fread(compressed, 1, compressed_size, f);
    fclose(f);

    uLongf dest_len = 901120; // Typical ADF size
    adz->data = malloc(dest_len);
    if (uncompress(adz->data, &dest_len, compressed, compressed_size) != Z_OK) {
        fprintf(stderr, "Decompression failed\n");
        free(compressed);
        return;
    }
    adz->size = dest_len;
    free(compressed);
    adz->prop_count = 0;

    // Parse properties
    unsigned int *dv = (unsigned int *)adz->data;
    char buf[256];
    // Boot block
    snprintf(buf, 256, "%c%c%c", adz->data[0], adz->data[1], adz->data[2]);
    adz->keys[adz->prop_count] = strdup("DOS Identifier");
    adz->values[adz->prop_count++] = strdup(buf);
    unsigned char dos_flags = adz->data[3];
    snprintf(buf, 256, "%u", dos_flags);
    adz->keys[adz->prop_count] = strdup("DOS Flags");
    adz->values[adz->prop_count++] = strdup(buf);
    char fs_type[32] = "";
    strcat(fs_type, (dos_flags & 1) ? "FFS" : "OFS");
    if (dos_flags & 2) strcat(fs_type, " INTL");
    if (dos_flags & 4) strcat(fs_type, " DIRC");
    adz->keys[adz->prop_count] = strdup("File System Type");
    adz->values[adz->prop_count++] = strdup(fs_type);
    snprintf(buf, 256, "%08x", dv[1]);
    adz->keys[adz->prop_count] = strdup("Boot Checksum");
    adz->values[adz->prop_count++] = strdup(buf);
    unsigned int root_block = dv[2];
    snprintf(buf, 256, "%u", root_block);
    adz->keys[adz->prop_count] = strdup("Root Block Pointer");
    adz->values[adz->prop_count++] = strdup(buf);
    int boot_code_present = 0;
    for (int i = 12; i < 1024; i++) {
        if (adz->data[i] != 0) {
            boot_code_present = 1;
            break;
        }
    }
    snprintf(buf, 256, "%s", boot_code_present ? "true" : "false");
    adz->keys[adz->prop_count] = strdup("Boot Code Present");
    adz->values[adz->prop_count++] = strdup(buf);
    unsigned int *root_dv = (unsigned int *)(adz->data + root_block * 512);
    snprintf(buf, 256, "%u", root_dv[0]);
    adz->keys[adz->prop_count] = strdup("Type");
    adz->values[adz->prop_count++] = strdup(buf);
    snprintf(buf, 256, "%u", root_dv[1]);
    adz->keys[adz->prop_count] = strdup("Header Key");
    adz->values[adz->prop_count++] = strdup(buf);
    snprintf(buf, 256, "%u", root_dv[3]);
    adz->keys[adz->prop_count] = strdup("Hash Table Size");
    adz->values[adz->prop_count++] = strdup(buf);
    snprintf(buf, 256, "%u", root_dv[4]);
    adz->keys[adz->prop_count] = strdup("First Data");
    adz->values[adz->prop_count++] = strdup(buf);
    snprintf(buf, 256, "%08x", root_dv[5]);
    adz->keys[adz->prop_count] = strdup("Root Checksum");
    adz->values[adz->prop_count++] = strdup(buf);
    char ht_buf[1024] = "";
    for (int i = 0; i < root_dv[3]; i++) {
        if (root_dv[6 + i] != 0) {
            char temp[16];
            snprintf(temp, 16, "%u ", root_dv[6 + i]);
            strcat(ht_buf, temp);
        }
    }
    adz->keys[adz->prop_count] = strdup("Hash Table");
    adz->values[adz->prop_count++] = strdup(ht_buf);
    int bitmap_flag = (int)root_dv[78 -1]; // Adjust for index
    snprintf(buf, 256, "%d", bitmap_flag);
    adz->keys[adz->prop_count] = strdup("Bitmap Flag");
    adz->values[adz->prop_count++] = strdup(buf);
    char bm_pages_buf[1024] = "";
    unsigned int bm_pages[25];
    for (int i = 0; i < 25; i++) {
        bm_pages[i] = root_dv[78 + i];
        if (bm_pages[i] != 0) {
            char temp[16];
            snprintf(temp, 16, "%u ", bm_pages[i]);
            strcat(bm_pages_buf, temp);
        }
    }
    adz->keys[adz->prop_count] = strdup("Bitmap Pages");
    adz->values[adz->prop_count++] = strdup(bm_pages_buf);
    snprintf(buf, 256, "%u", root_dv[103]);
    adz->keys[adz->prop_count] = strdup("Bitmap Extension");
    adz->values[adz->prop_count++] = strdup(buf);
    unsigned int r_days = root_dv[105];
    unsigned int r_mins = root_dv[106];
    unsigned int r_ticks = root_dv[107];
    struct tm tm = {0};
    tm.tm_year = 78;
    tm.tm_mon = 0;
    tm.tm_mday = 1;
    time_t r_time = mktime(&tm) + r_days * 86400 + r_mins * 60 + r_ticks / 50;
    strftime(buf, 64, "%Y-%m-%d", localtime(&r_time));
    adz->keys[adz->prop_count] = strdup("Last Root Alteration Date");
    adz->values[adz->prop_count++] = strdup(buf);
    unsigned char name_len = adz->data[root_block * 512 + 432];
    snprintf(buf, 256, "%u", name_len);
    adz->keys[adz->prop_count] = strdup("Volume Name Length");
    adz->values[adz->prop_count++] = strdup(buf);
    char volume_name[31];
    memcpy(volume_name, adz->data + root_block * 512 + 433, name_len);
    volume_name[name_len] = '\0';
    adz->keys[adz->prop_count] = strdup("Volume Name");
    adz->values[adz->prop_count++] = strdup(volume_name);
    unsigned int v_days = root_dv[118];
    unsigned int v_mins = root_dv[119];
    unsigned int v_ticks = root_dv[120];
    time_t v_time = mktime(&tm) + v_days * 86400 + v_mins * 60 + v_ticks / 50;
    strftime(buf, 64, "%Y-%m-%d", localtime(&v_time));
    adz->keys[adz->prop_count] = strdup("Last Volume Alteration Date");
    adz->values[adz->prop_count++] = strdup(buf);
    unsigned int c_days = root_dv[121];
    unsigned int c_mins = root_dv[122];
    unsigned int c_ticks = root_dv[123];
    time_t c_time = mktime(&tm) + c_days * 86400 + c_mins * 60 + c_ticks / 50;
    strftime(buf, 64, "%Y-%m-%d", localtime(&c_time));
    adz->keys[adz->prop_count] = strdup("Creation Date");
    adz->values[adz->prop_count++] = strdup(buf);
    snprintf(buf, 256, "%u", root_dv[124]);
    adz->keys[adz->prop_count] = strdup("First Directory Cache Block");
    adz->values[adz->prop_count++] = strdup(buf);
    snprintf(buf, 256, "%u", root_dv[127]);
    adz->keys[adz->prop_count] = strdup("Secondary Type");
    adz->values[adz->prop_count++] = strdup(buf);
    // Bitmap block
    unsigned int bitmap_block = bm_pages[0];
    unsigned int *bitmap_dv = (unsigned int *)(adz->data + bitmap_block * 512);
    snprintf(buf, 256, "%08x", bitmap_dv[0]);
    adz->keys[adz->prop_count] = strdup("Bitmap Checksum");
    adz->values[adz->prop_count++] = strdup(buf);
    int free_blocks = 0;
    for (int i = 1; i < 128; i++) { // 127 words
        unsigned int word = bitmap_dv[i];
        while (word) {
            free_blocks += word & 1;
            word >>= 1;
        }
    }
    snprintf(buf, 256, "%d", free_blocks);
    adz->keys[adz->prop_count] = strdup("Free Blocks");
    adz->values[adz->prop_count++] = strdup(buf);
    snprintf(buf, 256, "%d", 1760 - free_blocks - 2);
    adz->keys[adz->prop_count] = strdup("Allocated Blocks");
    adz->values[adz->prop_count++] = strdup(buf);
}

void adz_print_properties(ADZFile *adz) {
    for (int i = 0; i < adz->prop_count; i++) {
        printf("%s: %s\n", adz->keys[i], adz->values[i]);
    }
}

void adz_write(ADZFile *adz, const char *filename) {
    uLongf compressed_len = compressBound(adz->size);
    unsigned char *compressed = malloc(compressed_len);
    if (compress(compressed, &compressed_len, adz->data, adz->size) != Z_OK) {
        fprintf(stderr, "Compression failed\n");
        free(compressed);
        return;
    }
    FILE *f = fopen(filename, "wb");
    fwrite(compressed, 1, compressed_len, f);
    fclose(f);
    free(compressed);
}

void adz_free(ADZFile *adz) {
    free(adz->data);
    for (int i = 0; i < adz->prop_count; i++) {
        free(adz->keys[i]);
        free(adz->values[i]);
    }
}

// Example: ADZFile adz; adz_init(&adz, "example.adz"); adz_print_properties(&adz); adz_write(&adz, "modified.adz"); adz_free(&adz);