ar
Version:
ar - Read Unix archive files.
196 lines (185 loc) • 6.62 kB
text/typescript
/// <reference path="bower_components/DefinitelyTyped/node/node.d.ts" />
import fs = require('fs');
import path = require('path');
/**
* Contains information from a file's header.
*/
export interface ARFile {
name(): string;
date(): Date;
uid(): number;
gid(): number;
mode(): number;
dataSize(): number;
fileSize(): number;
headerSize(): number;
totalSize(): number;
fileData(): NodeBuffer;
}
export class Archive {
private files: ARFile[] = [];
constructor(private data: NodeBuffer) {
// Verify that it begins with "!<arch>\n".
if (data.toString('utf8', 0, 8) !== "!<arch>\n") {
throw new Error("Invalid archive file: Missing magic header '!<arch>\\n'");
}
this.createFiles();
}
/**
* Detects the header type of each file, and creates an ARFile representing
* each.
* Currently only supports BSD-style headers.
*/
private createFiles() {
// Should only be called once.
if (this.files.length > 0) return;
var offset = 8, file: ARFile;
while (offset < this.data.length) {
file = new BSDARFile(this.data.slice(offset));
this.files.push(file);
offset += file.totalSize();
}
}
/**
* Get an array of the files in the archive.
*/
public getFiles(): ARFile[] { return this.files; }
}
/**
* Given something of size *size* bytes that needs to be aligned by *alignment*
* bytes, returns the total number of padding bytes that need to be appended to
* the end of the data.
*/
function getPaddingBytes(size: number, alignment: number): number {
return (alignment - (size % alignment)) % alignment;
}
/**
* Trims trailing whitespace from the given string (both ends, although we
* only really need the RHS).
*/
function trimWhitespace(str: string): string {
return String.prototype.trim ? str.trim() : str.replace(/^\s+|\s+$/gm, '');
}
/**
* Trims trailing NULL characters.
*/
function trimNulls(str: string): string {
return str.replace(/\0/g, '');
}
/**
* All archive variants share this header before files, but the variants differ
* in how they handle odd cases (e.g. files with spaces, long filenames, etc).
*
* char ar_name[16]; File name
* char ar_date[12]; file member date
* char ar_uid[6] file member user identification
* char ar_gid[6] file member group identification
* char ar_mode[8] file member mode (octal)
* char ar_size[10]; file member size
* char ar_fmag[2]; header trailer string
*/
export class ARCommonFile implements ARFile {
constructor(public data: NodeBuffer) {
if (this.fmag() !== "`\n") {
throw new Error("Record is missing header trailer string; instead, it has: " + this.fmag());
}
}
public name(): string {
// The name field is padded by whitespace, so trim any lingering whitespace.
return trimWhitespace(this.data.toString('utf8', 0, 16));
}
public date(): Date { return new Date(parseInt(this.data.toString('ascii', 16, 28), 10)); }
public uid(): number { return parseInt(this.data.toString('ascii', 28, 34), 10); }
public gid(): number { return parseInt(this.data.toString('ascii', 34, 40), 10); }
public mode(): number { return parseInt(this.data.toString('ascii', 40, 48), 8); }
/**
* Total size of the data section in the record. Does not include padding bytes.
*/
public dataSize(): number { return parseInt(this.data.toString('ascii', 48, 58), 10); }
/**
* Total size of the *file* data in the data section of the record. This is
* not always equal to dataSize.
*/
public fileSize(): number { return this.dataSize(); }
private fmag(): string { return this.data.toString('ascii', 58, 60); }
/**
* Total size of the header, including padding bytes.
*/
public headerSize(): number {
// The common header is already two-byte aligned.
return 60;
}
/**
* Total size of this file record (header + header padding + file data +
* padding before next archive member).
*/
public totalSize(): number {
var headerSize = this.headerSize(), dataSize = this.dataSize();
// All archive members are 2-byte aligned, so there's padding bytes after
// the data section.
return headerSize + dataSize + getPaddingBytes(dataSize, 2);
}
/**
* Returns a *slice* of the backing buffer that has all of the file's data.
*/
public fileData(): NodeBuffer {
var headerSize = this.headerSize();
return this.data.slice(headerSize, headerSize + this.dataSize());
}
}
/**
* BSD variant of the file header.
*/
export class BSDARFile extends ARCommonFile implements ARFile {
private appendedFileName: boolean;
constructor(data: NodeBuffer) {
super(data);
// Check if the filename is appended to the header or not.
this.appendedFileName = super.name().substr(0, 3) === "#1/";
}
/**
* Returns the number of bytes that the appended name takes up in the content
* section.
*/
private appendedNameSize(): number {
if (this.appendedFileName) {
return parseInt(super.name().substr(3), 10);
}
return 0;
}
/**
* BSD ar stores extended filenames by placing the string "#1/" followed by
* the file name length in the file name field.
*
* Note that this is unambiguous, as '/' is not a valid filename character.
*/
public name(): string {
var length, name = super.name(), headerSize;
if (this.appendedFileName) {
length = this.appendedNameSize();
// The filename is stored right after the header.
headerSize = super.headerSize();
// Unfortunately, even though they give us the *explicit length*, they add
// NULL bytes and include that in the length, so we must strip them out.
name = trimNulls(this.data.toString('utf8', headerSize, headerSize + length));
}
return name;
}
/**
* dataSize = appendedNameSize + fileSize
*/
public fileSize(): number {
return this.dataSize() - this.appendedNameSize();
}
/**
* Returns a *slice* of the backing buffer that has all of the file's data.
* For BSD archives, we need to add in the size of the file name, which,
* unfortunately, is included in the fileSize number.
*/
public fileData(): NodeBuffer {
var headerSize = this.headerSize(),
appendedNameSize = this.appendedNameSize();
return this.data.slice(headerSize + appendedNameSize,
headerSize + appendedNameSize + this.fileSize());
}
}