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
orapplication/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(×tamp, 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
orPeaZip
(for SEA ARC compatibility). - Dependencies: Python requires
struct
andbinascii
(standard library). JavaScript requires Node.js. C requires standard libraries only. - Limitations: The SEA ARC format is obsolete, and modern tools like
PeaZip
orPowerArchiver
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);
}
};