Task 035: .ARC File Format

Task 035: .ARC File Format

The .ARC file format, developed by System Enhancement Associates (SEA) in the 1980s, is a lossless data compression and archival format primarily used in early computing environments like MS/PC-DOS and CP/M. It combines multiple files into a single archive, compressing them to save disk space. The format was popular in the dial-up BBS era but was later surpassed by ZIP due to better compression and directory structure support. Note that the .arc extension is also used by other unrelated formats (e.g., FreeArc, Internet Archive ARC, Nintendo ARC), but this response focuses on the SEA ARC format, as it appears to be the intended context.

1. Properties of the .ARC File Format Intrinsic to Its File System

Based on available information, the SEA ARC file format has the following intrinsic properties relevant to its file system structure:

  • File Extension: .arc
  • MIME Type: application/x-arc-compressed or application/octet-stream
  • File Structure: Linear, consisting of a sequence of file headers followed by file data, ending with an end-of-archive marker.
  • File Header:
  • ARCID: A single byte (0x1A) at offset 0, identifying the file as an ARC archive.
  • File Name: Stored as a null-terminated string (up to 12 characters in early versions, per DOS 8.3 naming conventions).
  • Compressed Size: Size of the compressed file data (typically 4 bytes).
  • Uncompressed Size: Size of the file before compression (typically 4 bytes).
  • Timestamp: File modification date and time, stored in DOS format (4 bytes).
  • Compression Method: A byte indicating the compression algorithm (e.g., 0x02 for uncompressed, 0x03 for LZW-based compression).
  • CRC-16: A 2-byte cyclic redundancy check for data integrity.
  • Compression Method: Supports multiple schemes, primarily LZW-based (LZSS variant), with methods like:
  • 0x02: Uncompressed (stored).
  • 0x03–0x09: Various LZW-based compression methods (e.g., packing, squeezing, crunching).
  • Checksum: CRC-32 for archive integrity verification.
  • End-of-Archive Marker: A header with a compression method of 0x00 signals the end of the archive.
  • Multi-File Support: Concatenates multiple files into one archive.
  • Metadata Storage: Limited to file name, size, timestamp, and compression method.
  • Maximum File Size: Limited by the operating system (e.g., 4GB in 32-bit systems).
  • Portability: High across DOS, CP/M, Unix, and Atari ST, but no native directory structure support.
  • Encryption: Not natively supported.
  • Recovery Record: Not supported.
  • Open Format: Yes, with source code released in 1986.

Limitations:

  • Does not support directory structures (unlike ZIP).
  • Limited metadata compared to modern formats.
  • Replaced by ZIP due to inferior compression ratios.

2. Python Class for .ARC File Handling

Below is a Python class to open, decode, read, write, and print ARC file properties. It assumes a basic LZW decompression for reading (simplified, as full LZW implementation is complex and requires external libraries like lzrw for accuracy). For simplicity, this handles uncompressed files and basic LZW.

import struct
import binascii
import os

class ARCFile:
    def __init__(self, filename, mode='r'):
        self.filename = filename
        self.mode = mode
        self.file = None
        self.files = []
        self._open_file()

    def _open_file(self):
        """Open the ARC file and read its structure."""
        self.file = open(self.filename, 'rb' if self.mode == 'r' else 'r+b')
        self._read_headers()

    def _read_headers(self):
        """Read file headers and store properties."""
        self.file.seek(0)
        while True:
            # Read ARC header
            header = self.file.read(1)
            if not header or header == b'\x00':
                break  # End of archive
            if header != b'\x1A':
                raise ValueError("Invalid ARC file: Missing ARCID")
            
            # Read compression method
            method = struct.unpack('B', self.file.read(1))[0]
            if method == 0:
                break  # End of archive
            
            # Read file name (13 bytes, null-terminated)
            name = self.file.read(13).split(b'\x00')[0].decode('ascii')
            
            # Read sizes, timestamp, CRC
            compressed_size = struct.unpack('<I', self.file.read(4))[0]
            timestamp = struct.unpack('<I', self.file.read(4))[0]
            uncompressed_size = struct.unpack('<I', self.file.read(4))[0]
            crc = struct.unpack('<H', self.file.read(2))[0]
            
            # Store file data position
            data_start = self.file.tell()
            self.files.append({
                'name': name,
                'compression_method': method,
                'compressed_size': compressed_size,
                'uncompressed_size': uncompressed_size,
                'timestamp': timestamp,
                'crc': crc,
                'data_start': data_start
            })
            self.file.seek(data_start + compressed_size)  # Skip file data

    def print_properties(self):
        """Print all ARC file properties."""
        print(f"ARC File: {self.filename}")
        print(f"MIME Type: application/x-arc-compressed")
        print(f"File Structure: Linear, multi-file archive")
        print(f"Total Files: {len(self.files)}")
        for i, file_info in enumerate(self.files, 1):
            print(f"\nFile {i}:")
            print(f"  Name: {file_info['name']}")
            print(f"  Compression Method: {file_info['compression_method']} "
                  f"({'Uncompressed' if file_info['compression_method'] == 2 else 'LZW-based'})")
            print(f"  Compressed Size: {file_info['compressed_size']} bytes")
            print(f"  Uncompressed Size: {file_info['uncompressed_size']} bytes")
            print(f"  Timestamp: {file_info['timestamp']}")
            print(f"  CRC-16: {hex(file_info['crc'])}")

    def read_file(self, file_index):
        """Read and decode a file's data."""
        file_info = self.files[file_index]
        self.file.seek(file_info['data_start'])
        data = self.file.read(file_info['compressed_size'])
        
        # Simplified decompression (uncompressed only for demo)
        if file_info['compression_method'] == 2:
            return data
        else:
            print(f"Warning: LZW decompression not fully implemented. Returning raw data.")
            return data  # Requires external LZW library for full support

    def write_file(self, name, data, compression_method=2):
        """Write a new file to the archive (uncompressed only)."""
        if self.mode != 'r+':
            raise ValueError("File not opened in write mode")
        
        # Prepare header
        header = b'\x1A'  # ARCID
        header += struct.pack('B', compression_method)
        header += name.encode('ascii').ljust(13, b'\x00')[:13]
        header += struct.pack('<I', len(data))  # Compressed size
        header += struct.pack('<I', 0)  # Timestamp (simplified)
        header += struct.pack('<I', len(data))  # Uncompressed size
        crc = binascii.crc_hqx(data, 0)  # CRC-16
        header += struct.pack('<H', crc)
        
        # Append to file
        self.file.seek(0, os.SEEK_END)
        self.file.write(header + data)
        self.file.write(b'\x1A\x00')  # End marker
        self._read_headers()  # Refresh headers

    def close(self):
        """Close the file."""
        if self.file:
            self.file.close()

# Example usage
if __name__ == "__main__":
    arc = ARCFile("example.arc")
    arc.print_properties()
    # Read first file's data
    if arc.files:
        data = arc.read_file(0)
        print(f"Data of first file: {data[:50]}...")  # Print first 50 bytes
    arc.close()

Notes:

  • The class reads the ARC file structure, parsing headers and storing properties.
  • LZW decompression is not fully implemented due to complexity; use an external library like lzrw for production.
  • Writing is implemented for uncompressed data (method 2) for simplicity.
  • Timestamp parsing is simplified; DOS format conversion requires additional logic.

3. Java Class for .ARC File Handling

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class ARCFile {
    private String filename;
    private RandomAccessFile file;
    private List<FileEntry> files;

    static class FileEntry {
        String name;
        int compressionMethod;
        long compressedSize;
        long uncompressedSize;
        long timestamp;
        int crc;
        long dataStart;

        FileEntry(String name, int compressionMethod, long compressedSize, long uncompressedSize, long timestamp, int crc, long dataStart) {
            this.name = name;
            this.compressionMethod = compressionMethod;
            this.compressedSize = compressedSize;
            this.uncompressedSize = uncompressedSize;
            this.timestamp = timestamp;
            this.crc = crc;
            this.dataStart = dataStart;
        }
    }

    public ARCFile(String filename, String mode) throws IOException {
        this.filename = filename;
        this.file = new RandomAccessFile(filename, mode);
        this.files = new ArrayList<>();
        readHeaders();
    }

    private void readHeaders() throws IOException {
        file.seek(0);
        while (true) {
            int arcId = file.readByte();
            if (arcId == 0 || file.getFilePointer() >= file.length()) break;
            if (arcId != 0x1A) throw new IOException("Invalid ARC file: Missing ARCID");

            int method = file.readByte();
            if (method == 0) break;

            byte[] nameBytes = new byte[13];
            file.readFully(nameBytes);
            String name = new String(nameBytes).split("\0")[0];

            ByteBuffer buffer = ByteBuffer.allocate(14).order(ByteOrder.LITTLE_ENDIAN);
            file.readFully(buffer.array());
            long compressedSize = Integer.toUnsignedLong(buffer.getInt());
            long timestamp = Integer.toUnsignedLong(buffer.getInt());
            long uncompressedSize = Integer.toUnsignedLong(buffer.getInt());
            int crc = buffer.getShort() & 0xFFFF;

            long dataStart = file.getFilePointer();
            files.add(new FileEntry(name, method, compressedSize, uncompressedSize, timestamp, crc, dataStart));
            file.seek(dataStart + compressedSize);
        }
    }

    public void printProperties() {
        System.out.println("ARC File: " + filename);
        System.out.println("MIME Type: application/x-arc-compressed");
        System.out.println("File Structure: Linear, multi-file archive");
        System.out.println("Total Files: " + files.size());
        for (int i = 0; i < files.size(); i++) {
            FileEntry entry = files.get(i);
            System.out.println("\nFile " + (i + 1) + ":");
            System.out.println("  Name: " + entry.name);
            System.out.println("  Compression Method: " + entry.compressionMethod +
                    (entry.compressionMethod == 2 ? " (Uncompressed)" : " (LZW-based)"));
            System.out.println("  Compressed Size: " + entry.compressedSize + " bytes");
            System.out.println("  Uncompressed Size: " + entry.uncompressedSize + " bytes");
            System.out.println("  Timestamp: " + entry.timestamp);
            System.out.println("  CRC-16: 0x" + Integer.toHexString(entry.crc));
        }
    }

    public byte[] readFile(int index) throws IOException {
        FileEntry entry = files.get(index);
        file.seek(entry.dataStart);
        byte[] data = new byte[(int) entry.compressedSize];
        file.readFully(data);
        if (entry.compressionMethod == 2) {
            return data;
        } else {
            System.out.println("Warning: LZW decompression not implemented. Returning raw data.");
            return data;
        }
    }

    public void writeFile(String name, byte[] data, int compressionMethod) throws IOException {
        if (!file.getFD().valid() || file.getChannel().position() == 0) {
            throw new IOException("File not opened in write mode");
        }
        file.seek(file.length());
        file.writeByte(0x1A);
        file.writeByte(compressionMethod);
        byte[] nameBytes = name.getBytes("ASCII");
        byte[] paddedName = new byte[13];
        System.arraycopy(nameBytes, 0, paddedName, 0, Math.min(nameBytes.length, 13));
        file.write(paddedName);
        ByteBuffer buffer = ByteBuffer.allocate(14).order(ByteOrder.LITTLE_ENDIAN);
        buffer.putInt(data.length).putInt(0).putInt(data.length).putShort((short) calculateCRC(data));
        file.write(buffer.array());
        file.write(data);
        file.write(new byte[]{0x1A, 0x00});
        readHeaders();
    }

    private int calculateCRC(byte[] data) {
        int crc = 0;
        for (byte b : data) {
            crc = (crc ^ (b & 0xFF)) & 0xFFFF;
            for (int i = 0; i < 8; i++) {
                if ((crc & 1) != 0) {
                    crc = (crc >>> 1) ^ 0xA001;
                } else {
                    crc >>>= 1;
                }
            }
        }
        return crc;
    }

    public void close() throws IOException {
        if (file != null) {
            file.close();
        }
    }

    public static void main(String[] args) throws IOException {
        ARCFile arc = new ARCFile("example.arc", "r");
        arc.printProperties();
        if (!arc.files.isEmpty()) {
            byte[] data = arc.readFile(0);
            System.out.println("Data of first file: " + new String(data).substring(0, Math.min(50, data.length)) + "...");
        }
        arc.close();
    }
}

Notes:

  • Uses RandomAccessFile for reading and writing.
  • LZW decompression is not implemented; raw data is returned for compressed files.
  • CRC-16 calculation is included for writing.
  • Timestamp is not fully parsed (DOS format requires additional conversion).

4. JavaScript Class for .ARC File Handling

const fs = require('fs');

class ARCFile {
    constructor(filename, mode = 'r') {
        this.filename = filename;
        this.mode = mode;
        this.file = null;
        this.files = [];
        this._openFile();
    }

    _openFile() {
        this.file = fs.openSync(this.filename, this.mode);
        this._readHeaders();
    }

    _readHeaders() {
        const buffer = fs.readFileSync(this.filename);
        let offset = 0;
        while (offset < buffer.length) {
            if (buffer[offset] !== 0x1A) {
                throw new Error('Invalid ARC file: Missing ARCID');
            }
            const method = buffer.readUInt8(offset + 1);
            if (method === 0) break;

            const name = buffer.slice(offset + 2, offset + 15).toString('ascii').split('\0')[0];
            const compressedSize = buffer.readUInt32LE(offset + 15);
            const timestamp = buffer.readUInt32LE(offset + 19);
            const uncompressedSize = buffer.readUInt32LE(offset + 23);
            const crc = buffer.readUInt16LE(offset + 27);
            const dataStart = offset + 29;

            this.files.push({
                name,
                compressionMethod: method,
                compressedSize,
                uncompressedSize,
                timestamp,
                crc,
                dataStart
            });
            offset = dataStart + compressedSize;
        }
    }

    printProperties() {
        console.log(`ARC File: ${this.filename}`);
        console.log('MIME Type: application/x-arc-compressed');
        console.log('File Structure: Linear, multi-file archive');
        console.log(`Total Files: ${this.files.length}`);
        this.files.forEach((file, i) => {
            console.log(`\nFile ${i + 1}:`);
            console.log(`  Name: ${file.name}`);
            console.log(`  Compression Method: ${file.compressionMethod} ` +
                `${file.compressionMethod === 2 ? '(Uncompressed)' : '(LZW-based)'}`);
            console.log(`  Compressed Size: ${file.compressedSize} bytes`);
            console.log(`  Uncompressed Size: ${file.uncompressedSize} bytes`);
            console.log(`  Timestamp: ${file.timestamp}`);
            console.log(`  CRC-16: 0x${file.crc.toString(16)}`);
        });
    }

    readFile(index) {
        const fileInfo = this.files[index];
        const buffer = Buffer.alloc(fileInfo.compressedSize);
        fs.readSync(this.file, buffer, 0, fileInfo.compressedSize, fileInfo.dataStart);
        if (fileInfo.compressionMethod === 2) {
            return buffer;
        } else {
            console.log('Warning: LZW decompression not implemented. Returning raw data.');
            return buffer;
        }
    }

    writeFile(name, data, compressionMethod = 2) {
        if (this.mode !== 'r+') throw new Error('File not opened in write mode');
        const header = Buffer.alloc(29);
        header.writeUInt8(0x1A, 0);
        header.writeUInt8(compressionMethod, 1);
        header.write(name.padEnd(13, '\0'), 2, 'ascii');
        header.writeUInt32LE(data.length, 15);
        header.writeUInt32LE(0, 19); // Simplified timestamp
        header.writeUInt32LE(data.length, 23);
        header.writeUInt16LE(this._calculateCRC(data), 27);

        fs.writeSync(this.file, header, 0, header.length, null);
        fs.writeSync(this.file, data, 0, data.length, null);
        fs.writeSync(this.file, Buffer.from([0x1A, 0x00]), 0, 2, null);
        this._readHeaders();
    }

    _calculateCRC(data) {
        let crc = 0;
        for (let b of data) {
            crc ^= b & 0xFF;
            for (let i = 0; i < 8; i++) {
                if (crc & 1) {
                    crc = (crc >>> 1) ^ 0xA001;
                } else {
                    crc >>>= 1;
                }
            }
        }
        return crc;
    }

    close() {
        if (this.file) {
            fs.closeSync(this.file);
        }
    }
}

// Example usage
const arc = new ARCFile('example.arc');
arc.printProperties();
if (arc.files.length > 0) {
    const data = arc.readFile(0);
    console.log(`Data of first file: ${data.toString('ascii', 0, Math.min(50, data.length))}...`);
}
arc.close();

Notes:

  • Uses Node.js fs module for file operations.
  • LZW decompression is not implemented; raw data is returned.
  • CRC-16 calculation is included for writing.
  • Assumes ASCII encoding for file names.

5. C Class for .ARC File Handling

C does not have classes, but we can use a struct and functions to achieve similar functionality.

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

typedef struct {
    char name[13];
    unsigned char compression_method;
    unsigned int compressed_size;
    unsigned int timestamp;
    unsigned int uncompressed_size;
    unsigned short crc;
    long data_start;
} FileEntry;

typedef struct {
    char* filename;
    FILE* file;
    FileEntry* files;
    int file_count;
} ARCFile;

ARCFile* arcfile_open(const char* filename, const char* mode) {
    ARCFile* arc = malloc(sizeof(ARCFile));
    arc->filename = strdup(filename);
    arc->file = fopen(filename, mode);
    arc->files = NULL;
    arc->file_count = 0;

    if (!arc->file) {
        free(arc->filename);
        free(arc);
        return NULL;
    }

    fseek(arc->file, 0, SEEK_SET);
    int capacity = 10;
    arc->files = malloc(capacity * sizeof(FileEntry));
    while (1) {
        unsigned char arc_id;
        if (fread(&arc_id, 1, 1, arc->file) != 1 || arc_id == 0) break;
        if (arc_id != 0x1A) {
            fprintf(stderr, "Invalid ARC file: Missing ARCID\n");
            fclose(arc->file);
            free(arc->filename);
            free(arc->files);
            free(arc);
            return NULL;
        }

        unsigned char method;
        fread(&method, 1, 1, arc->file);
        if (method == 0) break;

        char name[13];
        fread(name, 1, 13, arc->file);
        name[12] = '\0';

        unsigned int compressed_size, timestamp, uncompressed_size;
        unsigned short crc;
        fread(&compressed_size, 4, 1, arc->file);
        fread(&timestamp, 4, 1, arc->file);
        fread(&uncompressed_size, 4, 1, arc->file);
        fread(&crc, 2, 1, arc->file);

        long data_start = ftell(arc->file);
        if (arc->file_count >= capacity) {
            capacity *= 2;
            arc->files = realloc(arc->files, capacity * sizeof(FileEntry));
        }

        FileEntry entry;
        strncpy(entry.name, name, 13);
        entry.compression_method = method;
        entry.compressed_size = compressed_size;
        entry.timestamp = timestamp;
        entry.uncompressed_size = uncompressed_size;
        entry.crc = crc;
        entry.data_start = data_start;
        arc->files[arc->file_count++] = entry;

        fseek(arc->file, compressed_size, SEEK_CUR);
    }
    return arc;
}

void arcfile_print_properties(ARCFile* arc) {
    printf("ARC File: %s\n", arc->filename);
    printf("MIME Type: application/x-arc-compressed\n");
    printf("File Structure: Linear, multi-file archive\n");
    printf("Total Files: %d\n", arc->file_count);
    for (int i = 0; i < arc->file_count; i++) {
        FileEntry* entry = &arc->files[i];
        printf("\nFile %d:\n", i + 1);
        printf("  Name: %s\n", entry->name);
        printf("  Compression Method: %u (%s)\n", entry->compression_method,
               entry->compression_method == 2 ? "Uncompressed" : "LZW-based");
        printf("  Compressed Size: %u bytes\n", entry->compressed_size);
        printf("  Uncompressed Size: %u bytes\n", entry->uncompressed_size);
        printf("  Timestamp: %u\n", entry->timestamp);
        printf("  CRC-16: 0x%04X\n", entry->crc);
    }
}

unsigned char* arcfile_read_file(ARCFile* arc, int index, size_t* size) {
    FileEntry* entry = &arc->files[index];
    fseek(arc->file, entry->data_start, SEEK_SET);
    unsigned char* data = malloc(entry->compressed_size);
    *size = entry->compressed_size;
    fread(data, 1, entry->compressed_size, arc->file);
    if (entry->compression_method == 2) {
        return data;
    } else {
        printf("Warning: LZW decompression not implemented. Returning raw data.\n");
        return data;
    }
}

unsigned short calculate_crc(const unsigned char* data, size_t length) {
    unsigned short crc = 0;
    for (size_t i = 0; i < length; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 1) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

void arcfile_write_file(ARCFile* arc, const char* name, const unsigned char* data, size_t length, unsigned char compression_method) {
    fseek(arc->file, 0, SEEK_END);
    fwrite("\x1A", 1, 1, arc->file);
    fwrite(&compression_method, 1, 1, arc->file);
    char padded_name[13] = {0};
    strncpy(padded_name, name, 12);
    fwrite(padded_name, 1, 13, arc->file);
    fwrite(&length, 4, 1, arc->file);
    unsigned int zero = 0;
    fwrite(&zero, 4, 1, arc->file); // Simplified timestamp
    fwrite(&length, 4, 1, arc->file);
    unsigned short crc = calculate_crc(data, length);
    fwrite(&crc, 2, 1, arc->file);
    fwrite(data, 1, length, arc->file);
    fwrite("\x1A\x00", 1, 2, arc->file);
    arcfile_open(arc->filename, "r+"); // Refresh headers
}

void arcfile_close(ARCFile* arc) {
    if (arc->file) fclose(arc->file);
    free(arc->filename);
    free(arc->files);
    free(arc);
}

int main() {
    ARCFile* arc = arcfile_open("example.arc", "r");
    if (arc) {
        arcfile_print_properties(arc);
        if (arc->file_count > 0) {
            size_t size;
            unsigned char* data = arcfile_read_file(arc, 0, &size);
            printf("Data of first file: ");
            for (size_t i = 0; i < size && i < 50; i++) {
                printf("%c", data[i]);
            }
            printf("...\n");
            free(data);
        }
        arcfile_close(arc);
    }
    return 0;
}

Notes:

  • Uses standard C file I/O with FILE*.
  • LZW decompression is not implemented; raw data is returned.
  • CRC-16 calculation is included for writing.
  • Memory management is handled explicitly.

General Notes

  • LZW Decompression: Full LZW decompression requires a dedicated library or complex implementation beyond the scope of this response. The code handles uncompressed data (method 2) and returns raw data for compressed files with a warning.
  • Timestamp: DOS timestamp format (32-bit, packed date/time) is not fully parsed for simplicity. Production code should convert it to a human-readable format.
  • Error Handling: Basic error checking is included; enhance for robustness in production.
  • Writing: Only uncompressed data writing is fully implemented due to LZW complexity.
  • Testing: The code assumes a valid ARC file. Test with real ARC files created by tools like arc or PeaZip (for SEA ARC compatibility).
  • Dependencies: Python requires struct and binascii (standard library). JavaScript requires Node.js. C requires standard libraries only.
  • Limitations: The SEA ARC format is obsolete, and modern tools like PeaZip or PowerArchiver may be needed to create/test ARC files. The code does not handle FreeArc or other .arc variants.

If you need specific enhancements (e.g., full LZW support, directory handling, or support for other .arc variants), please clarify, and I can provide guidance or extend the code.

1. List of Properties Intrinsic to the .ARC File Format

The .ARC file format (SEA ARC) is an archive format that stores multiple files with associated metadata. It has no global header; instead, it consists of concatenated per-file entries, each with a fixed-size header followed by compressed data, terminated by an end-of-archive marker (0x1A 0x00). The properties intrinsic to the format (i.e., the metadata fields stored for each archived file, analogous to file system attributes) are:

  • Filename: A null-terminated ASCII string, up to 12 characters (plus null terminator, fixed 13-byte field).
  • Compression method: A single byte indicating the compression algorithm used (e.g., 2 for unpacked, 3 for packed/RLE, 8 for crunched/LZW+RLE).
  • Compressed size: The size of the file's compressed data in bytes (4-byte unsigned integer, little-endian).
  • Original size: The uncompressed size of the file in bytes (4-byte unsigned integer, little-endian; absent in obsolete method 1 headers, where it equals compressed size).
  • Modification date: The file's last modification date in MS-DOS format (2-byte unsigned integer, little-endian: bits 15-9 = year - 1980, 8-5 = month (1-12), 4-0 = day (1-31)).
  • Modification time: The file's last modification time in MS-DOS format (2-byte unsigned integer, little-endian: bits 15-11 = hour (0-23), 10-5 = minute (0-59), 4-0 = second / 2 (0-29, for 0-58 seconds)).
  • CRC-16 checksum: A 16-bit cyclic redundancy check of the uncompressed file data (2-byte unsigned integer, little-endian; using polynomial x^16 + x^15 + x^2 + 1).

These properties are per-entry. The format supports special entries (e.g., informational blocks for methods 20-39), but the above are the core properties for standard file entries. Compression/decompression of data is not a property but an operation; the classes below focus on reading/writing the properties and associated data blocks (without implementing decompression).

2. Python Class

import struct
import os

class ArcEntry:
    def __init__(self):
        self.filename = ''
        self.compression_method = 0
        self.compressed_size = 0
        self.original_size = 0
        self.mod_date = 0  # Raw MS-DOS date uint16
        self.mod_time = 0  # Raw MS-DOS time uint16
        self.crc = 0
        self.data = b''  # Compressed data

class ArcArchive:
    def __init__(self):
        self.entries = []

    def open(self, filepath):
        with open(filepath, 'rb') as f:
            data = f.read()
        offset = 0
        while offset < len(data):
            if data[offset] != 0x1A:
                raise ValueError("Invalid ARC marker")
            method = data[offset + 1]
            if method == 0:
                break  # End marker
            entry = ArcEntry()
            entry.compression_method = method
            entry.filename = data[offset + 2:offset + 15].decode('ascii').rstrip('\x00')
            entry.compressed_size, = struct.unpack('<I', data[offset + 15:offset + 19])
            entry.mod_date, = struct.unpack('<H', data[offset + 19:offset + 21])
            entry.mod_time, = struct.unpack('<H', data[offset + 21:offset + 23])
            entry.crc, = struct.unpack('<H', data[offset + 23:offset + 25])
            if method == 1:  # Old header, no orig size
                entry.original_size = entry.compressed_size
                header_size = 25
            else:
                entry.original_size, = struct.unpack('<I', data[offset + 25:offset + 29])
                header_size = 29
            entry.data = data[offset + header_size:offset + header_size + entry.compressed_size]
            self.entries.append(entry)
            offset += header_size + entry.compressed_size

    def write(self, filepath):
        with open(filepath, 'wb') as f:
            for entry in self.entries:
                header = bytearray(29)
                header[0] = 0x1A
                header[1] = entry.compression_method
                fname_bytes = entry.filename.encode('ascii')[:12] + b'\x00'
                header[2:15] = fname_bytes.ljust(13, b'\x00')
                struct.pack_into('<I', header, 15, entry.compressed_size)
                struct.pack_into('<H', header, 19, entry.mod_date)
                struct.pack_into('<H', header, 21, entry.mod_time)
                struct.pack_into('<H', header, 23, entry.crc)
                if entry.compression_method == 1:
                    f.write(header[:25])  # Old header
                else:
                    struct.pack_into('<I', header, 25, entry.original_size)
                    f.write(header)
                f.write(entry.data)
            f.write(b'\x1A\x00')  # End marker

3. Java Class

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;

public class ArcArchive {
    static class ArcEntry {
        String filename;
        int compressionMethod;
        long compressedSize;
        long originalSize;
        int modDate;  // Raw MS-DOS date
        int modTime;  // Raw MS-DOS time
        int crc;
        byte[] data;  // Compressed data
    }

    private ArcEntry[] entries = new ArcEntry[0];

    public void open(String filepath) throws IOException {
        byte[] data = Files.readAllBytes(Paths.get(filepath));
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        int offset = 0;
        java.util.List<ArcEntry> entryList = new java.util.ArrayList<>();
        while (offset < data.length) {
            if (bb.get(offset) != 0x1A) {
                throw new IOException("Invalid ARC marker");
            }
            int method = bb.get(offset + 1) & 0xFF;
            if (method == 0) {
                break;
            }
            ArcEntry entry = new ArcEntry();
            entry.compressionMethod = method;
            byte[] fnameBytes = new byte[13];
            bb.position(offset + 2);
            bb.get(fnameBytes);
            entry.filename = new String(fnameBytes, "ASCII").trim();
            entry.compressedSize = bb.getInt(offset + 15) & 0xFFFFFFFFL;
            entry.modDate = bb.getShort(offset + 19) & 0xFFFF;
            entry.modTime = bb.getShort(offset + 21) & 0xFFFF;
            entry.crc = bb.getShort(offset + 23) & 0xFFFF;
            int headerSize;
            if (method == 1) {
                entry.originalSize = entry.compressedSize;
                headerSize = 25;
            } else {
                entry.originalSize = bb.getInt(offset + 25) & 0xFFFFFFFFL;
                headerSize = 29;
            }
            entry.data = new byte[(int) entry.compressedSize];
            System.arraycopy(data, offset + headerSize, entry.data, 0, (int) entry.compressedSize);
            entryList.add(entry);
            offset += headerSize + (int) entry.compressedSize;
        }
        entries = entryList.toArray(new ArcEntry[0]);
    }

    public void write(String filepath) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(filepath);
             FileChannel fc = fos.getChannel()) {
            for (ArcEntry entry : entries) {
                ByteBuffer header = ByteBuffer.allocate(29).order(ByteOrder.LITTLE_ENDIAN);
                header.put((byte) 0x1A);
                header.put((byte) entry.compressionMethod);
                byte[] fnameBytes = entry.filename.getBytes("ASCII");
                byte[] padded = new byte[13];
                System.arraycopy(fnameBytes, 0, padded, 0, Math.min(12, fnameBytes.length));
                header.put(padded);
                header.putInt((int) entry.compressedSize);
                header.putShort((short) entry.modDate);
                header.putShort((short) entry.modTime);
                header.putShort((short) entry.crc);
                if (entry.compressionMethod == 1) {
                    header.flip();
                    header.limit(25);
                    fc.write(header);
                } else {
                    header.putInt((int) entry.originalSize);
                    header.flip();
                    fc.write(header);
                }
                fc.write(ByteBuffer.wrap(entry.data));
            }
            fc.write(ByteBuffer.wrap(new byte[] {0x1A, 0x00}));
        }
    }
}

4. JavaScript Class

const fs = require('fs');

class ArcEntry {
    constructor() {
        this.filename = '';
        this.compressionMethod = 0;
        this.compressedSize = 0;
        this.originalSize = 0;
        this.modDate = 0; // Raw MS-DOS date uint16
        this.modTime = 0; // Raw MS-DOS time uint16
        this.crc = 0;
        this.data = Buffer.alloc(0); // Compressed data
    }
}

class ArcArchive {
    constructor() {
        this.entries = [];
    }

    open(filepath) {
        const data = fs.readFileSync(filepath);
        let offset = 0;
        while (offset < data.length) {
            if (data[offset] !== 0x1A) {
                throw new Error('Invalid ARC marker');
            }
            const method = data[offset + 1];
            if (method === 0) {
                break;
            }
            const entry = new ArcEntry();
            entry.compressionMethod = method;
            entry.filename = data.slice(offset + 2, offset + 15).toString('ascii').replace(/\x00.*$/, '');
            entry.compressedSize = data.readUInt32LE(offset + 15);
            entry.modDate = data.readUInt16LE(offset + 19);
            entry.modTime = data.readUInt16LE(offset + 21);
            entry.crc = data.readUInt16LE(offset + 23);
            let headerSize;
            if (method === 1) {
                entry.originalSize = entry.compressedSize;
                headerSize = 25;
            } else {
                entry.originalSize = data.readUInt32LE(offset + 25);
                headerSize = 29;
            }
            entry.data = data.slice(offset + headerSize, offset + headerSize + entry.compressedSize);
            this.entries.push(entry);
            offset += headerSize + entry.compressedSize;
        }
    }

    write(filepath) {
        let buffers = [];
        for (const entry of this.entries) {
            const header = Buffer.alloc(29);
            header[0] = 0x1A;
            header[1] = entry.compressionMethod;
            const fnameBuf = Buffer.from(entry.filename, 'ascii').slice(0, 12);
            fnameBuf.copy(header, 2);
            header.writeUInt32LE(entry.compressedSize, 15);
            header.writeUInt16LE(entry.modDate, 19);
            header.writeUInt16LE(entry.modTime, 21);
            header.writeUInt16LE(entry.crc, 23);
            if (entry.compressionMethod === 1) {
                buffers.push(header.slice(0, 25));
            } else {
                header.writeUInt32LE(entry.originalSize, 25);
                buffers.push(header);
            }
            buffers.push(entry.data);
        }
        buffers.push(Buffer.from([0x1A, 0x00]));
        fs.writeFileSync(filepath, Buffer.concat(buffers));
    }
}

5. C Class (Implemented as C++ Class for "Class" Support)

#include <fstream>
#include <vector>
#include <string>
#include <stdexcept>
#include <cstring>

struct ArcEntry {
    std::string filename;
    unsigned char compression_method;
    uint32_t compressed_size;
    uint32_t original_size;
    uint16_t mod_date; // Raw MS-DOS date
    uint16_t mod_time; // Raw MS-DOS time
    uint16_t crc;
    std::vector<char> data; // Compressed data
};

class ArcArchive {
private:
    std::vector<ArcEntry> entries;

public:
    void open(const std::string& filepath) {
        std::ifstream file(filepath, std::ios::binary | std::ios::ate);
        if (!file) throw std::runtime_error("Cannot open file");
        size_t size = file.tellg();
        file.seekg(0);
        std::vector<char> data(size);
        file.read(data.data(), size);
        size_t offset = 0;
        while (offset < size) {
            if (static_cast<unsigned char>(data[offset]) != 0x1A) {
                throw std::runtime_error("Invalid ARC marker");
            }
            unsigned char method = static_cast<unsigned char>(data[offset + 1]);
            if (method == 0) break;
            ArcEntry entry;
            entry.compression_method = method;
            char fname[14];
            std::memcpy(fname, &data[offset + 2], 13);
            fname[13] = '\0';
            entry.filename = std::string(fname);
            entry.filename = entry.filename.substr(0, entry.filename.find('\0'));
            std::memcpy(&entry.compressed_size, &data[offset + 15], 4);
            std::memcpy(&entry.mod_date, &data[offset + 19], 2);
            std::memcpy(&entry.mod_time, &data[offset + 21], 2);
            std::memcpy(&entry.crc, &data[offset + 23], 2);
            size_t header_size;
            if (method == 1) {
                entry.original_size = entry.compressed_size;
                header_size = 25;
            } else {
                std::memcpy(&entry.original_size, &data[offset + 25], 4);
                header_size = 29;
            }
            entry.data.resize(entry.compressed_size);
            std::memcpy(entry.data.data(), &data[offset + header_size], entry.compressed_size);
            entries.push_back(entry);
            offset += header_size + entry.compressed_size;
        }
    }

    void write(const std::string& filepath) {
        std::ofstream file(filepath, std::ios::binary);
        if (!file) throw std::runtime_error("Cannot write file");
        for (const auto& entry : entries) {
            char header[29];
            std::memset(header, 0, 29);
            header[0] = 0x1A;
            header[1] = entry.compression_method;
            std::string fname = entry.filename.substr(0, 12);
            std::memcpy(&header[2], fname.c_str(), fname.length());
            std::memcpy(&header[15], &entry.compressed_size, 4);
            std::memcpy(&header[19], &entry.mod_date, 2);
            std::memcpy(&header[21], &entry.mod_time, 2);
            std::memcpy(&header[23], &entry.crc, 2);
            if (entry.compression_method == 1) {
                file.write(header, 25);
            } else {
                std::memcpy(&header[25], &entry.original_size, 4);
                file.write(header, 29);
            }
            file.write(entry.data.data(), entry.data.size());
        }
        const char end_marker[2] = {0x1A, 0x00};
        file.write(end_marker, 2);
    }
};