Task 562: .PMA File Format
Task 562: .PMA File Format
1. List of Properties Intrinsic to the .PMA File Format
The .PMA file format is a variant of the LHA (LZH) archiving format, primarily used in older systems like MSX and CPM for compressed archives. It shares the same basic structure as LHA but uses distinct compression method identifiers: -pm0- (uncompressed), -pm1- (unknown compression, possibly LZ-based), and -pm2- (unknown, likely improved compression). The format consists of a stream of records (each representing a file or directory entry), with no global header and terminated by a 0x00 byte (end-of-archive marker). Headers can be at different "levels" (0, 1, 2, or 3), affecting field order and extensions, but level 0 is common for .PMA.
Based on the formal specification, the intrinsic properties (fields and metadata) of the format are:
- Header Length (u1, 1 byte): Size of the header (excluding the length byte itself; if 0, indicates end-of-archive).
- Header Checksum (u1, 1 byte): Checksum for header integrity verification.
- Compression Method ID (string, 5 bytes, ASCII): Identifies the compression type (e.g., "-pm0-", "-pm1-", "-pm2-", or "-lhd-" for directories).
- Compressed File Size (u4, 4 bytes): Size of the compressed data body.
- Uncompressed File Size (u4, 4 bytes): Original size of the file data.
- File Timestamp (4 bytes, DOS datetime format): Original file modification date and time (16-bit date + 16-bit time; date: year-1980<<9 | month<<5 | day; time: hour<<11 | minute<<5 | second/2).
- File Attribute (u1, 1 byte): File or directory attributes (e.g., read-only, hidden; MS-DOS style).
- LHA Level (u1, 1 byte): Header level (0-3), determining additional fields and parsing rules.
- Filename Length (u1, 1 byte; conditional on level 0): Length of the filename string.
- Filename (string, variable bytes, ASCII; conditional on level 0): Name of the archived file or directory.
- Uncompressed File CRC16 (u2, 2 bytes; conditional on level 0 or 2): CRC-16 checksum of the uncompressed file data.
- OS Type (u1, 1 byte; conditional on level 2 or higher): Operating system identifier (e.g., 'M' for MS-DOS, 'U' for Unix).
- Extended Header Size (u2, 2 bytes; conditional on level 2 or higher): Size of optional extended headers for platform-specific metadata (e.g., permissions, timestamps).
- Extended Headers (variable; conditional on level 1+): List of tagged extended fields (e.g., for Unix permissions, long filenames).
- Body (variable bytes): Compressed or uncompressed data payload (size matches compressed file size).
These properties repeat for each entry in the archive. For directories, the compression method is typically "-lhd-", with zero sizes and no body.
2. Two Direct Download Links for .PMA Files
- https://www.zimmers.net/anonftp/pub/cpm/archivers/lt31.pma
- https://www.zimmers.net/anonftp/pub/cpm/archivers/unarc16.pma
3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .PMA File Dumper
4. Python Class for .PMA File Handling
import struct
import datetime
class PMAFile:
def __init__(self, filepath):
self.filepath = filepath
self.entries = []
self._parse()
def _parse(self):
with open(self.filepath, 'rb') as f:
data = f.read()
offset = 0
while offset < len(data):
(header_len,) = struct.unpack('<B', data[offset:offset+1])
if header_len == 0:
break
offset += 1
# Header1
(checksum, method_id_raw, compr_size, uncompr_size, timestamp_raw, attr, level) = struct.unpack('<B5sIIIBB', data[offset:offset+20])
method_id = method_id_raw.decode('ascii')
dos_date = (timestamp_raw >> 16) & 0xFFFF
dos_time = timestamp_raw & 0xFFFF
year = 1980 + ((dos_date >> 9) & 0x7F)
month = (dos_date >> 5) & 0x0F
day = dos_date & 0x1F
hour = (dos_time >> 11) & 0x1F
minute = (dos_time >> 5) & 0x3F
second = (dos_time & 0x1F) * 2
timestamp = datetime.datetime(year, month, day, hour, minute, second)
entry = {
'header_len': header_len,
'checksum': checksum,
'method_id': method_id,
'compr_size': compr_size,
'uncompr_size': uncompr_size,
'timestamp': timestamp,
'attr': attr,
'level': level
}
offset += 20
if level == 0:
(fn_len,) = struct.unpack('<B', data[offset:offset+1])
filename = data[offset+1:offset+1+fn_len].decode('ascii')
entry['fn_len'] = fn_len
entry['filename'] = filename
offset += 1 + fn_len
(crc16,) = struct.unpack('<H', data[offset:offset+2])
entry['crc16'] = crc16
offset += 2
if level == 2:
(os_type, ext_size) = struct.unpack('<BH', data[offset:offset+3])
entry['os_type'] = chr(os_type)
entry['ext_size'] = ext_size
offset += 3 + ext_size # Skip extended data
# Skip body (could decompress if -pm0-)
offset += compr_size
self.entries.append(entry)
def print_properties(self):
for i, entry in enumerate(self.entries, 1):
print(f"Entry {i}:")
for key, value in entry.items():
print(f" {key}: {value}")
def write(self, new_filepath, new_entries):
# Simple write for uncompressed (-pm0-) entries; assumes level 0
with open(new_filepath, 'wb') as f:
for entry in new_entries:
# Assume minimal: no filename for simplicity, add body as is
header_len = 22 # Base for level 0 without filename
checksum = 0 # Placeholder; compute real if needed
method_id = b'-pm0-'
compr_size = len(entry['body']) # Assume uncompressed
uncompr_size = compr_size
now = datetime.datetime.now()
dos_date = ((now.year - 1980) << 9) | (now.month << 5) | now.day
dos_time = (now.hour << 11) | (now.minute << 5) | (now.second // 2)
timestamp_raw = (dos_date << 16) | dos_time
attr = 0
level = 0
crc16 = 0 # Placeholder
header1 = struct.pack('<B5sIIIBB', checksum, method_id, compr_size, uncompr_size, timestamp_raw, attr, level)
f.write(struct.pack('<B', header_len))
f.write(header1)
f.write(struct.pack('<H', crc16))
f.write(entry['body'])
f.write(b'\x00') # End marker
# Example usage:
# pma = PMAFile('example.pma')
# pma.print_properties()
# pma.write('new.pma', [{'body': b'test data'}])
5. Java Class for .PMA File Handling
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Date;
public class PMAFile {
private String filepath;
private List<Entry> entries = new ArrayList<>();
public PMAFile(String filepath) {
this.filepath = filepath;
parse();
}
private void parse() {
try {
byte[] data = Files.readAllBytes(Paths.get(filepath));
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
int offset = 0;
while (offset < data.length) {
int headerLen = bb.get(offset) & 0xFF;
if (headerLen == 0) break;
offset += 1;
int checksum = bb.get(offset) & 0xFF;
byte[] methodIdBytes = new byte[5];
bb.position(offset + 1);
bb.get(methodIdBytes);
String methodId = new String(methodIdBytes, "ASCII");
int comprSize = bb.getInt(offset + 6);
int uncomprSize = bb.getInt(offset + 10);
int timestampRaw = bb.getInt(offset + 14);
int dosDate = (timestampRaw >> 16) & 0xFFFF;
int dosTime = timestampRaw & 0xFFFF;
int year = 1980 + ((dosDate >> 9) & 0x7F);
int month = (dosDate >> 5) & 0x0F;
int day = dosDate & 0x1F;
int hour = (dosTime >> 11) & 0x1F;
int minute = (dosTime >> 5) & 0x3F;
int second = (dosTime & 0x1F) * 2;
@SuppressWarnings("deprecation")
Date timestamp = new Date(year - 1900, month - 1, day, hour, minute, second);
int attr = bb.get(offset + 18) & 0xFF;
int level = bb.get(offset + 19) & 0xFF;
offset += 20;
Entry entry = new Entry();
entry.headerLen = headerLen;
entry.checksum = checksum;
entry.methodId = methodId;
entry.comprSize = comprSize;
entry.uncomprSize = uncomprSize;
entry.timestamp = timestamp;
entry.attr = attr;
entry.level = level;
if (level == 0) {
int fnLen = bb.get(offset) & 0xFF;
byte[] fnBytes = new byte[fnLen];
bb.position(offset + 1);
bb.get(fnBytes);
entry.filename = new String(fnBytes, "ASCII");
offset += 1 + fnLen;
}
entry.crc16 = bb.getShort(offset) & 0xFFFF;
offset += 2;
if (level == 2) {
int osType = bb.get(offset) & 0xFF;
int extSize = bb.getShort(offset + 1) & 0xFFFF;
entry.osType = (char) osType;
entry.extSize = extSize;
offset += 3 + extSize;
}
offset += comprSize; // Skip body
entries.add(entry);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void printProperties() {
for (int i = 0; i < entries.size(); i++) {
System.out.println("Entry " + (i + 1) + ":");
Entry e = entries.get(i);
System.out.println(" Header Length: " + e.headerLen);
System.out.println(" Header Checksum: " + e.checksum);
System.out.println(" Compression Method: " + e.methodId);
System.out.println(" Compressed Size: " + e.comprSize);
System.out.println(" Uncompressed Size: " + e.uncomprSize);
System.out.println(" Timestamp: " + e.timestamp);
System.out.println(" Attribute: " + e.attr);
System.out.println(" Level: " + e.level);
if (e.filename != null) {
System.out.println(" Filename: " + e.filename);
}
System.out.println(" Uncompressed CRC16: " + e.crc16);
if (e.osType != '\0') {
System.out.println(" OS Type: " + e.osType);
System.out.println(" Extended Header Size: " + e.extSize);
}
}
}
public void write(String newFilepath, List<byte[]> newBodies) throws IOException {
// Simple write for -pm0- level 0
try (FileOutputStream fos = new FileOutputStream(newFilepath)) {
for (byte[] body : newBodies) {
int headerLen = 22; // Base
int checksum = 0; // Placeholder
String methodId = "-pm0-";
int comprSize = body.length;
int uncomprSize = comprSize;
Date now = new Date();
@SuppressWarnings("deprecation")
int dosDate = ((now.getYear() + 1900 - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
@SuppressWarnings("deprecation")
int dosTime = (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() / 2);
int timestampRaw = (dosDate << 16) | dosTime;
int attr = 0;
int level = 0;
int crc16 = 0; // Placeholder
ByteBuffer bb = ByteBuffer.allocate(headerLen).order(ByteOrder.LITTLE_ENDIAN);
bb.put((byte) headerLen);
bb.put((byte) checksum);
bb.put(methodId.getBytes("ASCII"));
bb.putInt(comprSize);
bb.putInt(uncomprSize);
bb.putInt(timestampRaw);
bb.put((byte) attr);
bb.put((byte) level);
bb.putShort((short) crc16);
fos.write(bb.array());
fos.write(body);
}
fos.write(0); // End
}
}
static class Entry {
int headerLen, checksum, comprSize, uncomprSize, attr, level, crc16, extSize;
String methodId, filename;
Date timestamp;
char osType = '\0';
}
// Example usage:
// PMAFile pma = new PMAFile("example.pma");
// pma.printProperties();
// pma.write("new.pma", List.of("test data".getBytes()));
}
6. JavaScript Class for .PMA File Handling
class PMAFile {
constructor(buffer) {
this.entries = [];
this._parse(buffer);
}
_parse(buffer) {
const view = new DataView(buffer);
let offset = 0;
while (offset < buffer.byteLength) {
let headerLen = view.getUint8(offset);
if (headerLen === 0) break;
offset += 1;
let checksum = view.getUint8(offset);
let methodId = '';
for (let i = 1; i <= 5; i++) {
methodId += String.fromCharCode(view.getUint8(offset + i));
}
let comprSize = view.getUint32(offset + 6, true);
let uncomprSize = view.getUint32(offset + 10, true);
let timestampRaw = view.getUint32(offset + 14, true);
let dosDate = (timestampRaw >> 16) & 0xFFFF;
let dosTime = timestampRaw & 0xFFFF;
let year = 1980 + ((dosDate >> 9) & 0x7F);
let month = (dosDate >> 5) & 0x0F;
let day = dosDate & 0x1F;
let hour = (dosTime >> 11) & 0x1F;
let minute = (dosTime >> 5) & 0x3F;
let second = (dosTime & 0x1F) * 2;
let timestamp = new Date(year, month - 1, day, hour, minute, second);
let attr = view.getUint8(offset + 18);
let level = view.getUint8(offset + 19);
offset += 20;
let entry = {
headerLen,
checksum,
methodId,
comprSize,
uncomprSize,
timestamp,
attr,
level
};
if (level === 0) {
let fnLen = view.getUint8(offset);
let filename = '';
for (let i = 0; i < fnLen; i++) {
filename += String.fromCharCode(view.getUint8(offset + 1 + i));
}
entry.fnLen = fnLen;
entry.filename = filename;
offset += 1 + fnLen;
}
entry.crc16 = view.getUint16(offset, true);
offset += 2;
if (level === 2) {
let osType = view.getUint8(offset);
let extSize = view.getUint16(offset + 1, true);
entry.osType = String.fromCharCode(osType);
entry.extSize = extSize;
offset += 3 + extSize;
}
offset += comprSize; // Skip body
this.entries.push(entry);
}
}
printProperties() {
this.entries.forEach((entry, i) => {
console.log(`Entry ${i + 1}:`);
Object.entries(entry).forEach(([key, value]) => {
console.log(` ${key}: ${value}`);
});
});
}
write(newBodies) {
// Simple write for -pm0- level 0; returns ArrayBuffer
let totalSize = newBodies.reduce((acc, body) => acc + 23 + body.byteLength, 1); // Headers + bodies + end
let buffer = new ArrayBuffer(totalSize);
let view = new DataView(buffer);
let offset = 0;
newBodies.forEach((body) => {
let headerLen = 22;
view.setUint8(offset++, headerLen);
view.setUint8(offset++, 0); // checksum placeholder
let methodId = '-pm0-'.split('').forEach((c, i) => view.setUint8(offset + i, c.charCodeAt(0)));
offset += 5;
let comprSize = body.byteLength;
view.setUint32(offset, comprSize, true); offset += 4;
view.setUint32(offset, comprSize, true); offset += 4; // uncomprSize
let now = new Date();
let dosDate = ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
let dosTime = (now.getHours() << 11) | (now.getMinutes() << 5) | (Math.floor(now.getSeconds() / 2));
view.setUint32(offset, (dosDate << 16) | dosTime, true); offset += 4;
view.setUint8(offset++, 0); // attr
view.setUint8(offset++, 0); // level
view.setUint16(offset, 0, true); offset += 2; // crc16 placeholder
new Uint8Array(buffer, offset, body.byteLength).set(new Uint8Array(body));
offset += body.byteLength;
});
view.setUint8(offset, 0); // End
return buffer;
}
}
// Example usage:
// fetch('example.pma').then(res => res.arrayBuffer()).then(buffer => {
// const pma = new PMAFile(buffer);
// pma.printProperties();
// const newBuffer = pma.write([new TextEncoder().encode('test data')]);
// });
7. C Class (Implemented as C++ Class for Object-Oriented Support)
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <ctime>
#include <cstdint>
struct Entry {
uint8_t headerLen;
uint8_t checksum;
std::string methodId;
uint32_t comprSize;
uint32_t uncomprSize;
std::tm timestamp;
uint8_t attr;
uint8_t level;
uint16_t crc16;
std::string filename;
char osType = '\0';
uint16_t extSize = 0;
};
class PMAFile {
private:
std::string filepath;
std::vector<Entry> entries;
void parse() {
std::ifstream file(filepath, std::ios::binary);
if (!file) return;
std::vector<char> data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
size_t offset = 0;
while (offset < data.size()) {
uint8_t headerLen = static_cast<uint8_t>(data[offset]);
if (headerLen == 0) break;
offset += 1;
uint8_t checksum = static_cast<uint8_t>(data[offset]);
std::string methodId(&data[offset + 1], &data[offset + 6]);
uint32_t comprSize = *reinterpret_cast<uint32_t*>(&data[offset + 6]);
uint32_t uncomprSize = *reinterpret_cast<uint32_t*>(&data[offset + 10]);
uint32_t timestampRaw = *reinterpret_cast<uint32_t*>(&data[offset + 14]);
uint16_t dosDate = (timestampRaw >> 16) & 0xFFFF;
uint16_t dosTime = timestampRaw & 0xFFFF;
std::tm ts{};
ts.tm_year = 1980 + ((dosDate >> 9) & 0x7F) - 1900;
ts.tm_mon = ((dosDate >> 5) & 0x0F) - 1;
ts.tm_mday = dosDate & 0x1F;
ts.tm_hour = (dosTime >> 11) & 0x1F;
ts.tm_min = (dosTime >> 5) & 0x3F;
ts.tm_sec = (dosTime & 0x1F) * 2;
uint8_t attr = static_cast<uint8_t>(data[offset + 18]);
uint8_t level = static_cast<uint8_t>(data[offset + 19]);
offset += 20;
Entry entry;
entry.headerLen = headerLen;
entry.checksum = checksum;
entry.methodId = methodId;
entry.comprSize = comprSize;
entry.uncomprSize = uncomprSize;
entry.timestamp = ts;
entry.attr = attr;
entry.level = level;
if (level == 0) {
uint8_t fnLen = static_cast<uint8_t>(data[offset]);
entry.filename = std::string(&data[offset + 1], &data[offset + 1 + fnLen]);
offset += 1 + fnLen;
}
entry.crc16 = *reinterpret_cast<uint16_t*>(&data[offset]);
offset += 2;
if (level == 2) {
entry.osType = data[offset];
entry.extSize = *reinterpret_cast<uint16_t*>(&data[offset + 1]);
offset += 3 + entry.extSize;
}
offset += comprSize; // Skip body
entries.push_back(entry);
}
}
public:
PMAFile(const std::string& filepath) : filepath(filepath) {
parse();
}
void printProperties() {
for (size_t i = 0; i < entries.size(); ++i) {
auto& e = entries[i];
std::cout << "Entry " << (i + 1) << ":" << std::endl;
std::cout << " Header Length: " << static_cast<int>(e.headerLen) << std::endl;
std::cout << " Header Checksum: " << static_cast<int>(e.checksum) << std::endl;
std::cout << " Compression Method: " << e.methodId << std::endl;
std::cout << " Compressed Size: " << e.comprSize << std::endl;
std::cout << " Uncompressed Size: " << e.uncomprSize << std::endl;
char timeBuf[20];
std::strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", &e.timestamp);
std::cout << " Timestamp: " << timeBuf << std::endl;
std::cout << " Attribute: " << static_cast<int>(e.attr) << std::endl;
std::cout << " Level: " << static_cast<int>(e.level) << std::endl;
if (!e.filename.empty()) {
std::cout << " Filename: " << e.filename << std::endl;
}
std::cout << " Uncompressed CRC16: " << e.crc16 << std::endl;
if (e.osType != '\0') {
std::cout << " OS Type: " << e.osType << std::endl;
std::cout << " Extended Header Size: " << e.extSize << std::endl;
}
}
}
void write(const std::string& newFilepath, const std::vector<std::vector<char>>& newBodies) {
std::ofstream file(newFilepath, std::ios::binary);
for (const auto& body : newBodies) {
uint8_t headerLen = 22;
file.put(static_cast<char>(headerLen));
file.put(0); // checksum
std::string methodId = "-pm0-";
file.write(methodId.data(), 5);
uint32_t comprSize = static_cast<uint32_t>(body.size());
file.write(reinterpret_cast<const char*>(&comprSize), 4);
file.write(reinterpret_cast<const char*>(&comprSize), 4); // uncomprSize
std::time_t now = std::time(nullptr);
std::tm* local = std::localtime(&now);
uint16_t dosDate = ((local->tm_year + 1900 - 1980) << 9) | ((local->tm_mon + 1) << 5) | local->tm_mday;
uint16_t dosTime = (local->tm_hour << 11) | (local->tm_min << 5) | (local->tm_sec / 2);
uint32_t timestampRaw = (static_cast<uint32_t>(dosDate) << 16) | dosTime;
file.write(reinterpret_cast<const char*>(×tampRaw), 4);
file.put(0); // attr
file.put(0); // level
uint16_t crc16 = 0;
file.write(reinterpret_cast<const char*>(&crc16), 2);
file.write(body.data(), body.size());
}
file.put(0); // End
}
};
// Example usage:
// PMAFile pma("example.pma");
// pma.printProperties();
// pma.write("new.pma", {std::vector<char>{'t', 'e', 's', 't'}});