Task 351: .LBR File Format

Task 351: .LBR File Format

.LBR File Format Specifications

The .LBR file format is an uncompressed archive format originating from the CP/M operating system, used to bundle multiple files into a single library file. It was primarily associated with the LU (Library Utility) program. The format organizes data into 128-byte sectors (records). The file begins with a directory consisting of one or more sectors, followed by the archived member files' data. The directory contains 32-byte entries describing each member (including the directory itself as the first entry). The directory ends with an unused entry (starting with 0xFF), and the total directory size is padded to full sectors if needed. Member data follows immediately after the directory, starting on the next sector boundary if necessary. Each member's data is stored sequentially, padded to full sectors with 0x1A (CP/M EOF character) bytes if the last sector is partial. There is no compression; it's a simple container.

The CRC uses the CCITT-16 algorithm (polynomial 0x1021, initial value 0x0000, no bit reversal). The stored CRC for a member is set such that computing the CRC over the member's data (including pads) appended with the stored CRC value (high byte first) results in 0x0000. For the directory member, the CRC calculation includes the directory's own CRC field in its data.

1. List of All Properties Intrinsic to This File Format's File System

The "file system" here refers to the archive's internal structure, where each member (file) is described by properties in the 32-byte directory entries. These are the key properties for each member:

  • Status: 1 byte (0x00 = active, 0xFE = deleted, 0xFF = unused; other values treated as deleted).
  • Name: 8 bytes (ASCII characters, space-padded; follows CP/M file naming rules, no lowercase typically).
  • Extension: 3 bytes (ASCII characters, space-padded).
  • Starting Index: 2 bytes (little-endian uint16; sector number where the member data starts, relative to the file start; sector 0 is the directory start).
  • Length: 2 bytes (little-endian uint16; number of 128-byte sectors for the member; 0 for empty members).
  • CRC: 2 bytes (little-endian uint16; CCITT-16 checksum of the member's data including pads).
  • Creation Date: 2 bytes (little-endian uint16; Julian days since December 31, 1977; 0 if unavailable).
  • Last Change Date: 2 bytes (little-endian uint16; Julian days since December 31, 1977; 0 if unavailable or same as creation).
  • Creation Time: 2 bytes (little-endian uint16; MS-DOS format: bits 15-11 = hour (0-23), bits 10-5 = minutes (0-59), bits 4-0 = seconds/2 (0-29, i.e., 0-58 seconds)).
  • Last Change Time: 2 bytes (little-endian uint16; same format as creation time).
  • Pad Length: 1 byte (0-127; number of 0x1A pad bytes in the last sector; subtract from last sector to get actual data length).
  • Reserved: 1 byte (must be 0x00).
  • Filler: 4 bytes (0x00 padding).

Format-wide properties include:

  • Sector size: 128 bytes.
  • Pad byte: 0x1A (ASCII SUB, CP/M EOF).
  • Directory starts at byte 0, ends with 0xFF entry.
  • First directory entry describes the directory itself (name/extension spaces, index 0, length >0, pad length 0).
  • Members must have unique names/extensions.
  • No holes in the file; sequential access safe.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .LBR File Parser

This is a self-contained HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post (or any HTML page). It allows drag-and-drop of a .LBR file and dumps all properties to the screen in a readable format.

Drag and drop .LBR file here

4. Python Class for .LBR File Handling

This Python class can open, decode/read, write, and print properties of a .LBR file.

import struct
import datetime

class LBRFile:
    SECTOR_SIZE = 128
    ENTRY_SIZE = 32
    PAD_BYTE = 0x1A

    def __init__(self, filename):
        self.filename = filename
        self.members = []  # List of dicts with properties
        self._read()

    def _crc16_ccitt(self, data):
        crc = 0x0000
        for byte in data:
            crc ^= (byte << 8)
            for _ in range(8):
                crc = ((crc << 1) ^ 0x1021) if (crc & 0x8000) else (crc << 1)
            crc &= 0xFFFF
        return crc

    def _parse_date(self, days):
        if days == 0:
            return 'Unavailable'
        base = datetime.date(1977, 12, 31)
        return (base + datetime.timedelta(days=days)).isoformat()

    def _parse_time(self, word):
        hour = (word >> 11) & 0x1F
        minute = (word >> 5) & 0x3F
        sec = (word & 0x1F) * 2
        return f"{hour:02d}:{minute:02d}:{sec:02d}"

    def _read(self):
        with open(self.filename, 'rb') as f:
            data = f.read()
        # Get directory length from first entry
        dir_length_sectors, = struct.unpack_from('<H', data, 14)
        dir_size = dir_length_sectors * self.SECTOR_SIZE
        offset = 0
        while offset < dir_size:
            status = data[offset]
            if status == 0xFF:
                break
            if status in (0x00, 0xFE):
                name = data[offset+1:offset+9].decode('ascii').rstrip()
                ext = data[offset+9:offset+12].decode('ascii').rstrip()
                start_sector, length_sectors, crc, create_date, update_date, create_time, update_time = struct.unpack_from('<HHHHHHH', data, offset+12)
                pad_length = data[offset+26]
                reserved = data[offset+27]
                self.members.append({
                    'status': 'Active' if status == 0x00 else 'Deleted',
                    'name': name,
                    'ext': ext,
                    'start_sector': start_sector,
                    'length_sectors': length_sectors,
                    'crc': crc,
                    'create_date': self._parse_date(create_date),
                    'update_date': self._parse_date(update_date),
                    'create_time': self._parse_time(create_time),
                    'update_time': self._parse_time(update_time),
                    'pad_length': pad_length,
                    'reserved': reserved
                })
            offset += self.ENTRY_SIZE

    def print_properties(self):
        for member in self.members:
            print(f"Member: {member['name']}.{member['ext']} (Status: {member['status']})")
            for key, value in member.items():
                if key not in ('name', 'ext', 'status'):
                    print(f"  {key.capitalize().replace('_', ' ')}: {value}")
            print()

    def write(self, new_filename=None):
        # Simple write: reconstruct the file from current members (assumes data not modified; for demo)
        # In a full impl, you'd need member data stored too. Here, just rewrite directory for existing file.
        filename = new_filename or self.filename
        with open(self.filename, 'rb') as f:
            original_data = f.read()
        # Rebuild directory (simplified; assumes no changes to data positions)
        dir_data = bytearray()
        for member in self.members:
            status = 0x00 if member['status'] == 'Active' else 0xFE
            name = member['name'].ljust(8, ' ').encode('ascii')
            ext = member['ext'].ljust(3, ' ').encode('ascii')
            # Omitted: proper packing of dates/times from strings; assume original for demo
            # In full, parse back to ints
            entry = struct.pack('<B8s3sHHHHHHHB B 4B', status, name, ext,
                                member['start_sector'], member['length_sectors'], member['crc'],
                                0, 0, 0, 0,  # Placeholder for dates/times
                                member['pad_length'], member['reserved'], 0,0,0,0)
            dir_data += entry
        dir_data += b'\xFF' + b'\x00' * (self.ENTRY_SIZE - 1)  # End marker
        # Pad directory to sectors
        dir_sectors = (len(dir_data) + self.SECTOR_SIZE - 1) // self.SECTOR_SIZE
        dir_data += b'\x00' * (dir_sectors * self.SECTOR_SIZE - len(dir_data))
        # Update first entry length
        struct.pack_into('<H', dir_data, 14, dir_sectors)
        # Write (directory + original data after dir)
        with open(filename, 'wb') as f:
            f.write(dir_data + original_data[dir_sectors * self.SECTOR_SIZE:])

# Example usage:
# lbr = LBRFile('example.lbr')
# lbr.print_properties()
# lbr.write('modified.lbr')

5. Java Class for .LBR File Handling

This Java class can open, decode/read, write, and print properties of a .LBR file.

import java.io.*;
import java.nio.*;
import java.nio.file.*;
import java.util.*;

public class LBRFile {
    private static final int SECTOR_SIZE = 128;
    private static final int ENTRY_SIZE = 32;
    private static final byte PAD_BYTE = 0x1A;

    private String filename;
    private List<Map<String, Object>> members = new ArrayList<>();

    public LBRFile(String filename) {
        this.filename = filename;
        read();
    }

    private int crc16CCITT(byte[] data) {
        int crc = 0x0000;
        for (byte b : data) {
            crc ^= (Byte.toUnsignedInt(b) << 8);
            for (int i = 0; i < 8; i++) {
                crc = ((crc & 0x8000) != 0) ? ((crc << 1) ^ 0x1021) : (crc << 1);
            }
            crc &= 0xFFFF;
        }
        return crc;
    }

    private String parseDate(int days) {
        if (days == 0) return "Unavailable";
        Calendar base = Calendar.getInstance();
        base.set(1977, Calendar.DECEMBER, 31, 0, 0, 0);
        base.add(Calendar.DAY_OF_YEAR, days);
        return String.format("%tF", base);
    }

    private String parseTime(int word) {
        int hour = (word >> 11) & 0x1F;
        int min = (word >> 5) & 0x3F;
        int sec = (word & 0x1F) * 2;
        return String.format("%02d:%02d:%02d", hour, min, sec);
    }

    private void read() {
        try {
            byte[] data = Files.readAllBytes(Paths.get(filename));
            ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
            int dirLengthSectors = buffer.getShort(14);
            int dirSize = dirLengthSectors * SECTOR_SIZE;
            int offset = 0;
            while (offset < dirSize) {
                byte status = buffer.get(offset);
                if (status == (byte) 0xFF) break;
                if (status == (byte) 0x00 || status == (byte) 0xFE) {
                    String name = new String(data, offset + 1, 8, "ASCII").trim();
                    String ext = new String(data, offset + 9, 3, "ASCII").trim();
                    int startSector = Short.toUnsignedInt(buffer.getShort(offset + 12));
                    int lengthSectors = Short.toUnsignedInt(buffer.getShort(offset + 14));
                    int crc = Short.toUnsignedInt(buffer.getShort(offset + 16));
                    int createDate = Short.toUnsignedInt(buffer.getShort(offset + 18));
                    int updateDate = Short.toUnsignedInt(buffer.getShort(offset + 20));
                    int createTime = Short.toUnsignedInt(buffer.getShort(offset + 22));
                    int updateTime = Short.toUnsignedInt(buffer.getShort(offset + 24));
                    byte padLength = buffer.get(offset + 26);
                    byte reserved = buffer.get(offset + 27);

                    Map<String, Object> member = new HashMap<>();
                    member.put("status", status == (byte) 0x00 ? "Active" : "Deleted");
                    member.put("name", name);
                    member.put("ext", ext);
                    member.put("start_sector", startSector);
                    member.put("length_sectors", lengthSectors);
                    member.put("crc", crc);
                    member.put("create_date", parseDate(createDate));
                    member.put("update_date", parseDate(updateDate));
                    member.put("create_time", parseTime(createTime));
                    member.put("update_time", parseTime(updateTime));
                    member.put("pad_length", Byte.toUnsignedInt(padLength));
                    member.put("reserved", Byte.toUnsignedInt(reserved));
                    members.add(member);
                }
                offset += ENTRY_SIZE;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void printProperties() {
        for (Map<String, Object> member : members) {
            System.out.println("Member: " + member.get("name") + "." + member.get("ext") + " (Status: " + member.get("status") + ")");
            member.forEach((key, value) -> {
                if (!key.equals("name") && !key.equals("ext") && !key.equals("status")) {
                    System.out.println("  " + key.replace("_", " ") + ": " + value);
                }
            });
            System.out.println();
        }
    }

    public void write(String newFilename) {
        // Simplified: rewrite directory, keep original data
        try {
            byte[] originalData = Files.readAllBytes(Paths.get(filename));
            ByteArrayOutputStream dirStream = new ByteArrayOutputStream();
            ByteBuffer entryBuffer = ByteBuffer.allocate(ENTRY_SIZE).order(ByteOrder.LITTLE_ENDIAN);
            for (Map<String, Object> member : members) {
                entryBuffer.clear();
                byte status = member.get("status").equals("Active") ? (byte) 0x00 : (byte) 0xFE;
                byte[] name = ((String) member.get("name")).getBytes("ASCII");
                byte[] ext = ((String) member.get("ext")).getBytes("ASCII");
                // Pad name and ext
                byte[] namePadded = Arrays.copyOf(name, 8);
                byte[] extPadded = Arrays.copyOf(ext, 3);
                entryBuffer.put(status);
                entryBuffer.put(namePadded);
                entryBuffer.put(extPadded);
                entryBuffer.putShort(((Integer) member.get("start_sector")).shortValue());
                entryBuffer.putShort(((Integer) member.get("length_sectors")).shortValue());
                entryBuffer.putShort(((Integer) member.get("crc")).shortValue());
                entryBuffer.putShort((short) 0); // Placeholder create_date
                entryBuffer.putShort((short) 0); // update_date
                entryBuffer.putShort((short) 0); // create_time
                entryBuffer.putShort((short) 0); // update_time
                entryBuffer.put(((Integer) member.get("pad_length")).byteValue());
                entryBuffer.put(((Integer) member.get("reserved")).byteValue());
                entryBuffer.putInt(0); // filler
                dirStream.write(entryBuffer.array());
            }
            // End marker
            entryBuffer.clear();
            entryBuffer.put((byte) 0xFF);
            entryBuffer.position(ENTRY_SIZE);
            dirStream.write(entryBuffer.array(), 0, ENTRY_SIZE - 1);
            byte[] dirData = dirStream.toByteArray();
            // Pad to sectors
            int dirSectors = (dirData.length + SECTOR_SIZE - 1) / SECTOR_SIZE;
            byte[] paddedDir = Arrays.copyOf(dirData, dirSectors * SECTOR_SIZE);
            // Update first entry length
            ByteBuffer.wrap(paddedDir).order(ByteOrder.LITTLE_ENDIAN).putShort(14, (short) dirSectors);
            // Write file
            try (FileOutputStream fos = new FileOutputStream(newFilename == null ? filename : newFilename)) {
                fos.write(paddedDir);
                fos.write(originalData, dirSectors * SECTOR_SIZE, originalData.length - dirSectors * SECTOR_SIZE);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Example usage:
    // public static void main(String[] args) {
    //     LBRFile lbr = new LBRFile("example.lbr");
    //     lbr.printProperties();
    //     lbr.write("modified.lbr");
    // }
}

6. JavaScript Class for .LBR File Handling

This JavaScript class (Node.js compatible) can open, decode/read, write, and print properties of a .LBR file. Use with fs module.

const fs = require('fs');

class LBRFile {
  constructor(filename) {
    this.SECTOR_SIZE = 128;
    this.ENTRY_SIZE = 32;
    this.PAD_BYTE = 0x1A;
    this.filename = filename;
    this.members = [];
    this.read();
  }

  crc16CCITT(data) {
    let crc = 0x0000;
    for (let byte of data) {
      crc ^= (byte << 8);
      for (let i = 0; i < 8; i++) {
        crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
      }
      crc &= 0xFFFF;
    }
    return crc;
  }

  parseDate(days) {
    if (days === 0) return 'Unavailable';
    const base = new Date(1977, 11, 31);
    base.setDate(base.getDate() + days);
    return base.toISOString().split('T')[0];
  }

  parseTime(word) {
    const hour = (word >> 11) & 0x1F;
    const min = (word >> 5) & 0x3F;
    const sec = (word & 0x1F) * 2;
    return `${hour.toString().padStart(2, '0')}:${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
  }

  read() {
    const data = fs.readFileSync(this.filename);
    const view = new DataView(data.buffer);
    const dirLengthSectors = view.getUint16(14, true);
    const dirSize = dirLengthSectors * this.SECTOR_SIZE;
    let offset = 0;
    while (offset < dirSize) {
      const status = view.getUint8(offset);
      if (status === 0xFF) break;
      if (status === 0x00 || status === 0xFE) {
        const name = data.slice(offset + 1, offset + 9).toString('ascii').trim();
        const ext = data.slice(offset + 9, offset + 12).toString('ascii').trim();
        const startSector = view.getUint16(offset + 12, true);
        const lengthSectors = view.getUint16(offset + 14, true);
        const crc = view.getUint16(offset + 16, true);
        const createDate = view.getUint16(offset + 18, true);
        const updateDate = view.getUint16(offset + 20, true);
        const createTime = view.getUint16(offset + 22, true);
        const updateTime = view.getUint16(offset + 24, true);
        const padLength = view.getUint8(offset + 26);
        const reserved = view.getUint8(offset + 27);
        this.members.push({
          status: status === 0x00 ? 'Active' : 'Deleted',
          name,
          ext,
          start_sector: startSector,
          length_sectors: lengthSectors,
          crc,
          create_date: this.parseDate(createDate),
          update_date: this.parseDate(updateDate),
          create_time: this.parseTime(createTime),
          update_time: this.parseTime(updateTime),
          pad_length: padLength,
          reserved
        });
      }
      offset += this.ENTRY_SIZE;
    }
  }

  printProperties() {
    this.members.forEach(member => {
      console.log(`Member: ${member.name}.${member.ext} (Status: ${member.status})`);
      Object.entries(member).forEach(([key, value]) => {
        if (key !== 'name' && key !== 'ext' && key !== 'status') {
          console.log(`  ${key.replace(/_/g, ' ')}: ${value}`);
        }
      });
      console.log('');
    });
  }

  write(newFilename = this.filename) {
    const originalData = fs.readFileSync(this.filename);
    let dirData = Buffer.alloc(0);
    this.members.forEach(member => {
      const entry = Buffer.alloc(this.ENTRY_SIZE);
      const status = member.status === 'Active' ? 0x00 : 0xFE;
      const name = Buffer.from(member.name.padEnd(8, ' '), 'ascii');
      const ext = Buffer.from(member.ext.padEnd(3, ' '), 'ascii');
      entry.writeUInt8(status, 0);
      name.copy(entry, 1);
      ext.copy(entry, 9);
      entry.writeUInt16LE(member.start_sector, 12);
      entry.writeUInt16LE(member.length_sectors, 14);
      entry.writeUInt16LE(member.crc, 16);
      entry.writeUInt16LE(0, 18); // Placeholder create_date
      entry.writeUInt16LE(0, 20); // update_date
      entry.writeUInt16LE(0, 22); // create_time
      entry.writeUInt16LE(0, 24); // update_time
      entry.writeUInt8(member.pad_length, 26);
      entry.writeUInt8(member.reserved, 27);
      entry.writeUInt32LE(0, 28); // filler
      dirData = Buffer.concat([dirData, entry]);
    });
    // End marker
    const endEntry = Buffer.alloc(this.ENTRY_SIZE).fill(0);
    endEntry.writeUInt8(0xFF, 0);
    dirData = Buffer.concat([dirData, endEntry.slice(0, this.ENTRY_SIZE)]);
    // Pad to sectors
    const dirSectors = Math.ceil(dirData.length / this.SECTOR_SIZE);
    const paddedDir = Buffer.alloc(dirSectors * this.SECTOR_SIZE, 0);
    dirData.copy(paddedDir);
    // Update first entry length
    paddedDir.writeUInt16LE(dirSectors, 14);
    // Write file
    const outputData = Buffer.concat([paddedDir, originalData.slice(dirSectors * this.SECTOR_SIZE)]);
    fs.writeFileSync(newFilename, outputData);
  }
}

// Example usage:
// const lbr = new LBRFile('example.lbr');
// lbr.printProperties();
// lbr.write('modified.lbr');

7. C Class (Using Struct and Functions) for .LBR File Handling

Since C doesn't have classes, this is a struct with associated functions for open, decode/read, write, and print properties of a .LBR file.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>

#define SECTOR_SIZE 128
#define ENTRY_SIZE 32
#define PAD_BYTE 0x1A

typedef struct {
    char *filename;
    struct Member *members;
    int num_members;
} LBRFile;

typedef struct Member {
    char *status;
    char name[9];
    char ext[4];
    uint16_t start_sector;
    uint16_t length_sectors;
    uint16_t crc;
    char create_date[11];
    char update_date[11];
    char create_time[9];
    char update_time[9];
    uint8_t pad_length;
    uint8_t reserved;
    struct Member *next;
} Member;

uint16_t crc16_ccitt(const uint8_t *data, size_t len) {
    uint16_t crc = 0x0000;
    for (size_t i = 0; i < len; i++) {
        crc ^= (data[i] << 8);
        for (int j = 0; j < 8; j++) {
            crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
        }
    }
    return crc;
}

void parse_date(uint16_t days, char *buf) {
    if (days == 0) {
        strcpy(buf, "Unavailable");
        return;
    }
    struct tm base = {.tm_year = 77, .tm_mon = 11, .tm_mday = 31, .tm_hour = 0, .tm_min = 0, .tm_sec = 0};
    time_t base_time = mktime(&base) + days * 86400;
    struct tm *t = localtime(&base_time);
    sprintf(buf, "%04d-%02d-%02d", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday);
}

void parse_time(uint16_t word, char *buf) {
    uint8_t hour = (word >> 11) & 0x1F;
    uint8_t min = (word >> 5) & 0x3F;
    uint8_t sec = (word & 0x1F) * 2;
    sprintf(buf, "%02d:%02d:%02d", hour, min, sec);
}

void lbr_read(LBRFile *lbr) {
    FILE *f = fopen(lbr->filename, "rb");
    if (!f) return;
    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t *data = malloc(size);
    fread(data, 1, size, f);
    fclose(f);

    uint16_t dir_length_sectors = *(uint16_t*)(data + 14);
    int dir_size = dir_length_sectors * SECTOR_SIZE;
    int offset = 0;
    Member *last = NULL;
    while (offset < dir_size) {
        uint8_t status = data[offset];
        if (status == 0xFF) break;
        if (status == 0x00 || status == 0xFE) {
            Member *m = malloc(sizeof(Member));
            memset(m, 0, sizeof(Member));
            m->status = strdup(status == 0x00 ? "Active" : "Deleted");
            strncpy(m->name, (char*)(data + offset + 1), 8);
            m->name[8] = '\0';
            char *trim = strchr(m->name, ' ');
            if (trim) *trim = '\0';
            strncpy(m->ext, (char*)(data + offset + 9), 3);
            m->ext[3] = '\0';
            trim = strchr(m->ext, ' ');
            if (trim) *trim = '\0';
            m->start_sector = *(uint16_t*)(data + offset + 12);
            m->length_sectors = *(uint16_t*)(data + offset + 14);
            m->crc = *(uint16_t*)(data + offset + 16);
            uint16_t create_date_val = *(uint16_t*)(data + offset + 18);
            uint16_t update_date_val = *(uint16_t*)(data + offset + 20);
            uint16_t create_time_val = *(uint16_t*)(data + offset + 22);
            uint16_t update_time_val = *(uint16_t*)(data + offset + 24);
            m->pad_length = data[offset + 26];
            m->reserved = data[offset + 27];
            parse_date(create_date_val, m->create_date);
            parse_date(update_date_val, m->update_date);
            parse_time(create_time_val, m->create_time);
            parse_time(update_time_val, m->update_time);
            m->next = NULL;
            if (last) last->next = m;
            else lbr->members = m;
            last = m;
            lbr->num_members++;
        }
        offset += ENTRY_SIZE;
    }
    free(data);
}

void lbr_print_properties(LBRFile *lbr) {
    Member *m = lbr->members;
    while (m) {
        printf("Member: %s.%s (Status: %s)\n", m->name, m->ext, m->status);
        printf("  Start sector: %u\n", m->start_sector);
        printf("  Length sectors: %u\n", m->length_sectors);
        printf("  Crc: %u\n", m->crc);
        printf("  Create date: %s\n", m->create_date);
        printf("  Update date: %s\n", m->update_date);
        printf("  Create time: %s\n", m->create_time);
        printf("  Update time: %s\n", m->update_time);
        printf("  Pad length: %u\n", m->pad_length);
        printf("  Reserved: %u\n\n", m->reserved);
        m = m->next;
    }
}

void lbr_write(LBRFile *lbr, const char *new_filename) {
    // Simplified: read original, rewrite directory
    FILE *f = fopen(lbr->filename, "rb");
    if (!f) return;
    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t *data = malloc(size);
    fread(data, 1, size, f);
    fclose(f);

    uint8_t *dir_data = malloc(ENTRY_SIZE * (lbr->num_members + 1));
    memset(dir_data, 0, ENTRY_SIZE * (lbr->num_members + 1));
    int offset = 0;
    Member *m = lbr->members;
    while (m) {
        dir_data[offset] = strcmp(m->status, "Active") == 0 ? 0x00 : 0xFE;
        strncpy((char*)(dir_data + offset + 1), m->name, 8);
        for (int i = strlen(m->name); i < 8; i++) dir_data[offset + 1 + i] = ' ';
        strncpy((char*)(dir_data + offset + 9), m->ext, 3);
        for (int i = strlen(m->ext); i < 3; i++) dir_data[offset + 9 + i] = ' ';
        *(uint16_t*)(dir_data + offset + 12) = m->start_sector;
        *(uint16_t*)(dir_data + offset + 14) = m->length_sectors;
        *(uint16_t*)(dir_data + offset + 16) = m->crc;
        // Placeholder 0 for dates/times
        *(uint16_t*)(dir_data + offset + 18) = 0;
        *(uint16_t*)(dir_data + offset + 20) = 0;
        *(uint16_t*)(dir_data + offset + 22) = 0;
        *(uint16_t*)(dir_data + offset + 24) = 0;
        dir_data[offset + 26] = m->pad_length;
        dir_data[offset + 27] = m->reserved;
        offset += ENTRY_SIZE;
        m = m->next;
    }
    dir_data[offset] = 0xFF;
    int dir_length = offset + ENTRY_SIZE;
    int dir_sectors = (dir_length + SECTOR_SIZE - 1) / SECTOR_SIZE;
    uint8_t *padded_dir = calloc(dir_sectors * SECTOR_SIZE, 1);
    memcpy(padded_dir, dir_data, dir_length);
    *(uint16_t*)(padded_dir + 14) = dir_sectors;

    FILE *out = fopen(new_filename ? new_filename : lbr->filename, "wb");
    fwrite(padded_dir, 1, dir_sectors * SECTOR_SIZE, out);
    fwrite(data + dir_sectors * SECTOR_SIZE, 1, size - dir_sectors * SECTOR_SIZE, out);
    fclose(out);

    free(dir_data);
    free(padded_dir);
    free(data);
}

LBRFile *lbr_open(const char *filename) {
    LBRFile *lbr = malloc(sizeof(LBRFile));
    lbr->filename = strdup(filename);
    lbr->members = NULL;
    lbr->num_members = 0;
    lbr_read(lbr);
    return lbr;
}

void lbr_free(LBRFile *lbr) {
    Member *m = lbr->members;
    while (m) {
        Member *next = m->next;
        free(m->status);
        free(m);
        m = next;
    }
    free(lbr->filename);
    free(lbr);
}

// Example usage:
// int main() {
//     LBRFile *lbr = lbr_open("example.lbr");
//     lbr_print_properties(lbr);
//     lbr_write(lbr, "modified.lbr");
//     lbr_free(lbr);
//     return 0;
// }