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.
2. Two Direct Download Links for .LBR Files
- https://raw.githubusercontent.com/agn453/UNZIP-CPM-Z80/master/unzip/unzip182.lbr
- https://oldcomputers-ddns.org/public/pub/cdrom/walnut_creek_cdrom/cpm/utils/arc-lbr/pull.lbr
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.
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;
// }