-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathar.ts
195 lines (185 loc) · 6.47 KB
/
ar.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/// <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());
}
}