browserfs
Version:
A filesystem in your browser!
1,389 lines (1,328 loc) • 38.6 kB
text/typescript
import {ApiError, ErrorCode} from '../core/api_error';
import {default as Stats, FileType} from '../core/node_fs_stats';
import {SynchronousFileSystem, FileSystem, BFSCallback, FileSystemOptions} from '../core/file_system';
import {File} from '../core/file';
import {FileFlag, ActionType} from '../core/file_flag';
import {NoSyncFile} from '../generic/preload_file';
import {copyingSlice, deprecationMessage, bufferValidator} from '../core/util';
import * as path from 'path';
/**
* @hidden
*/
const rockRidgeIdentifier = "IEEE_P1282";
/**
* @hidden
*/
const enum VolumeDescriptorTypeCode {
BootRecord = 0,
PrimaryVolumeDescriptor = 1,
SupplementaryVolumeDescriptor = 2,
VolumePartitionDescriptor = 3,
VolumeDescriptorSetTerminator = 255
}
/**
* @hidden
*/
type TGetString = (d: Buffer, i: number, len: number) => string;
/**
* @hidden
*/
function getASCIIString(data: Buffer, startIndex: number, length: number) {
return data.toString('ascii', startIndex, startIndex + length).trim();
}
/**
* @hidden
*/
function getJolietString(data: Buffer, startIndex: number, length: number): string {
if (length === 1) {
// Special: Root, parent, current directory are still a single byte.
return String.fromCharCode(data[startIndex]);
}
// UTF16-BE, which isn't natively supported by NodeJS Buffers.
// Length should be even, but pessimistically floor just in case.
const pairs = Math.floor(length / 2);
const chars = new Array(pairs);
for (let i = 0; i < pairs; i++) {
const pos = startIndex + (i << 1);
chars[i] = String.fromCharCode(data[pos + 1] | (data[pos] << 8));
}
return chars.join('');
}
/**
* @hidden
*/
function getDate(data: Buffer, startIndex: number): Date {
const year = parseInt(getASCIIString(data, startIndex, 4), 10);
const mon = parseInt(getASCIIString(data, startIndex + 4, 2), 10);
const day = parseInt(getASCIIString(data, startIndex + 6, 2), 10);
const hour = parseInt(getASCIIString(data, startIndex + 8, 2), 10);
const min = parseInt(getASCIIString(data, startIndex + 10, 2), 10);
const sec = parseInt(getASCIIString(data, startIndex + 12, 2), 10);
const hundrethsSec = parseInt(getASCIIString(data, startIndex + 14, 2), 10);
// Last is a time-zone offset, but JavaScript dates don't support time zones well.
return new Date(year, mon, day, hour, min, sec, hundrethsSec * 100);
}
/**
* @hidden
*/
function getShortFormDate(data: Buffer, startIndex: number): Date {
const yearsSince1900 = data[startIndex];
const month = data[startIndex + 1];
const day = data[startIndex + 2];
const hour = data[startIndex + 3];
const minute = data[startIndex + 4];
const second = data[startIndex + 5];
// JavaScript's Date support isn't so great; ignore timezone.
// const offsetFromGMT = this._data[24];
return new Date(yearsSince1900, month - 1, day, hour, minute, second);
}
/**
* @hidden
*/
function constructSystemUseEntry(bigData: Buffer, i: number): SystemUseEntry {
const data = bigData.slice(i);
const sue = new SystemUseEntry(data);
switch (sue.signatureWord()) {
case SystemUseEntrySignatures.CE:
return new CEEntry(data);
case SystemUseEntrySignatures.PD:
return new PDEntry(data);
case SystemUseEntrySignatures.SP:
return new SPEntry(data);
case SystemUseEntrySignatures.ST:
return new STEntry(data);
case SystemUseEntrySignatures.ER:
return new EREntry(data);
case SystemUseEntrySignatures.ES:
return new ESEntry(data);
case SystemUseEntrySignatures.PX:
return new PXEntry(data);
case SystemUseEntrySignatures.PN:
return new PNEntry(data);
case SystemUseEntrySignatures.SL:
return new SLEntry(data);
case SystemUseEntrySignatures.NM:
return new NMEntry(data);
case SystemUseEntrySignatures.CL:
return new CLEntry(data);
case SystemUseEntrySignatures.PL:
return new PLEntry(data);
case SystemUseEntrySignatures.RE:
return new REEntry(data);
case SystemUseEntrySignatures.TF:
return new TFEntry(data);
case SystemUseEntrySignatures.SF:
return new SFEntry(data);
case SystemUseEntrySignatures.RR:
return new RREntry(data);
default:
return sue;
}
}
/**
* @hidden
*/
function constructSystemUseEntries(data: Buffer, i: number, len: number, isoData: Buffer): SystemUseEntry[] {
// If the remaining allocated space following the last recorded System Use Entry in a System
// Use field or Continuation Area is less than four bytes long, it cannot contain a System
// Use Entry and shall be ignored
len = len - 4;
let entries = new Array<SystemUseEntry>();
while (i < len) {
const entry = constructSystemUseEntry(data, i);
const length = entry.length();
if (length === 0) {
// Invalid SU section; prevent infinite loop.
return entries;
}
i += length;
if (entry instanceof STEntry) {
// ST indicates the end of entries.
break;
}
if (entry instanceof CEEntry) {
entries = entries.concat(entry.getEntries(isoData));
} else {
entries.push(entry);
}
}
return entries;
}
/**
* @hidden
*/
class VolumeDescriptor {
protected _data: Buffer;
constructor(data: Buffer) {
this._data = data;
}
public type(): VolumeDescriptorTypeCode {
return this._data[0];
}
public standardIdentifier(): string {
return getASCIIString(this._data, 1, 5);
}
public version(): number {
return this._data[6];
}
public data(): Buffer {
return this._data.slice(7, 2048);
}
}
/**
* @hidden
*/
abstract class PrimaryOrSupplementaryVolumeDescriptor extends VolumeDescriptor {
private _root: DirectoryRecord | null = null;
constructor(data: Buffer) {
super(data);
}
public systemIdentifier(): string {
return this._getString32(8);
}
public volumeIdentifier(): string {
return this._getString32(40);
}
public volumeSpaceSize(): number {
return this._data.readUInt32LE(80);
}
public volumeSetSize(): number {
return this._data.readUInt16LE(120);
}
public volumeSequenceNumber(): number {
return this._data.readUInt16LE(124);
}
public logicalBlockSize(): number {
return this._data.readUInt16LE(128);
}
public pathTableSize(): number {
return this._data.readUInt32LE(132);
}
public locationOfTypeLPathTable(): number {
return this._data.readUInt32LE(140);
}
public locationOfOptionalTypeLPathTable(): number {
return this._data.readUInt32LE(144);
}
public locationOfTypeMPathTable(): number {
return this._data.readUInt32BE(148);
}
public locationOfOptionalTypeMPathTable(): number {
return this._data.readUInt32BE(152);
}
public rootDirectoryEntry(isoData: Buffer): DirectoryRecord {
if (this._root === null) {
this._root = this._constructRootDirectoryRecord(this._data.slice(156));
this._root.rootCheckForRockRidge(isoData);
}
return this._root;
}
public volumeSetIdentifier(): string {
return this._getString(190, 128);
}
public publisherIdentifier(): string {
return this._getString(318, 128);
}
public dataPreparerIdentifier(): string {
return this._getString(446, 128);
}
public applicationIdentifier(): string {
return this._getString(574, 128);
}
public copyrightFileIdentifier(): string {
return this._getString(702, 38);
}
public abstractFileIdentifier(): string {
return this._getString(740, 36);
}
public bibliographicFileIdentifier(): string {
return this._getString(776, 37);
}
public volumeCreationDate(): Date {
return getDate(this._data, 813);
}
public volumeModificationDate(): Date {
return getDate(this._data, 830);
}
public volumeExpirationDate(): Date {
return getDate(this._data, 847);
}
public volumeEffectiveDate(): Date {
return getDate(this._data, 864);
}
public fileStructureVersion(): number {
return this._data[881];
}
public applicationUsed(): Buffer {
return this._data.slice(883, 883 + 512);
}
public reserved(): Buffer {
return this._data.slice(1395, 1395 + 653);
}
public abstract name(): string;
protected abstract _constructRootDirectoryRecord(data: Buffer): DirectoryRecord;
protected abstract _getString(idx: number, len: number): string;
protected _getString32(idx: number): string {
return this._getString(idx, 32);
}
}
/**
* @hidden
*/
class PrimaryVolumeDescriptor extends PrimaryOrSupplementaryVolumeDescriptor {
constructor(data: Buffer) {
super(data);
if (this.type() !== VolumeDescriptorTypeCode.PrimaryVolumeDescriptor) {
throw new ApiError(ErrorCode.EIO, `Invalid primary volume descriptor.`);
}
}
public name() {
return "ISO9660";
}
protected _constructRootDirectoryRecord(data: Buffer): DirectoryRecord {
return new ISODirectoryRecord(data, -1);
}
protected _getString(idx: number, len: number): string {
return this._getString(idx, len);
}
}
/**
* @hidden
*/
class SupplementaryVolumeDescriptor extends PrimaryOrSupplementaryVolumeDescriptor {
constructor(data: Buffer) {
super(data);
if (this.type() !== VolumeDescriptorTypeCode.SupplementaryVolumeDescriptor) {
throw new ApiError(ErrorCode.EIO, `Invalid supplementary volume descriptor.`);
}
const escapeSequence = this.escapeSequence();
const third = escapeSequence[2];
// Third character identifies what 'level' of the UCS specification to follow.
// We ignore it.
if (escapeSequence[0] !== 0x25 || escapeSequence[1] !== 0x2F ||
(third !== 0x40 && third !== 0x43 && third !== 0x45)) {
throw new ApiError(ErrorCode.EIO, `Unrecognized escape sequence for SupplementaryVolumeDescriptor: ${escapeSequence.toString()}`);
}
}
public name() {
return "Joliet";
}
public escapeSequence(): Buffer {
return this._data.slice(88, 120);
}
protected _constructRootDirectoryRecord(data: Buffer): DirectoryRecord {
return new JolietDirectoryRecord(data, -1);
}
protected _getString(idx: number, len: number): string {
return getJolietString(this._data, idx, len);
}
}
/**
* @hidden
*/
const enum FileFlags {
Hidden = 1,
Directory = 1 << 1,
AssociatedFile = 1 << 2,
EARContainsInfo = 1 << 3,
EARContainsPerms = 1 << 4,
FinalDirectoryRecordForFile = 1 << 5
}
/**
* @hidden
*/
abstract class DirectoryRecord {
protected _data: Buffer;
// Offset at which system use entries begin. Set to -1 if not enabled.
protected _rockRidgeOffset: number;
protected _suEntries: SystemUseEntry[] | null = null;
private _fileOrDir: Buffer | Directory<DirectoryRecord> | null = null;
constructor(data: Buffer, rockRidgeOffset: number) {
this._data = data;
this._rockRidgeOffset = rockRidgeOffset;
}
public hasRockRidge(): boolean {
return this._rockRidgeOffset > -1;
}
public getRockRidgeOffset(): number {
return this._rockRidgeOffset;
}
/**
* !!ONLY VALID ON ROOT NODE!!
* Checks if Rock Ridge is enabled, and sets the offset.
*/
public rootCheckForRockRidge(isoData: Buffer): void {
const dir = this.getDirectory(isoData);
this._rockRidgeOffset = dir.getDotEntry(isoData)._getRockRidgeOffset(isoData);
if (this._rockRidgeOffset > -1) {
// Wipe out directory. Start over with RR knowledge.
this._fileOrDir = null;
}
}
public length(): number {
return this._data[0];
}
public extendedAttributeRecordLength(): number {
return this._data[1];
}
public lba(): number {
return this._data.readUInt32LE(2) * 2048;
}
public dataLength(): number {
return this._data.readUInt32LE(10);
}
public recordingDate(): Date {
return getShortFormDate(this._data, 18);
}
public fileFlags(): number {
return this._data[25];
}
public fileUnitSize(): number {
return this._data[26];
}
public interleaveGapSize(): number {
return this._data[27];
}
public volumeSequenceNumber(): number {
return this._data.readUInt16LE(28);
}
public identifier(): string {
return this._getString(33, this._data[32]);
}
public fileName(isoData: Buffer): string {
if (this.hasRockRidge()) {
const fn = this._rockRidgeFilename(isoData);
if (fn !== null) {
return fn;
}
}
const ident = this.identifier();
if (this.isDirectory(isoData)) {
return ident;
}
// Files:
// - MUST have 0x2E (.) separating the name from the extension
// - MUST have 0x3B (;) separating the file name and extension from the version
// Gets expanded to two-byte char in Unicode directory records.
const versionSeparator = ident.indexOf(';');
if (versionSeparator === -1) {
// Some Joliet filenames lack the version separator, despite the standard
// specifying that it should be there.
return ident;
} else if (ident[versionSeparator - 1] === '.') {
// Empty extension. Do not include '.' in the filename.
return ident.slice(0, versionSeparator - 1);
} else {
// Include up to version separator.
return ident.slice(0, versionSeparator);
}
}
public isDirectory(isoData: Buffer): boolean {
let rv = !!(this.fileFlags() & FileFlags.Directory);
// If it lacks the Directory flag, it may still be a directory if we've exceeded the directory
// depth limit. Rock Ridge marks these as files and adds a special attribute.
if (!rv && this.hasRockRidge()) {
rv = this.getSUEntries(isoData).filter((e) => e instanceof CLEntry).length > 0;
}
return rv;
}
public isSymlink(isoData: Buffer): boolean {
return this.hasRockRidge() && this.getSUEntries(isoData).filter((e) => e instanceof SLEntry).length > 0;
}
public getSymlinkPath(isoData: Buffer): string {
let p = "";
const entries = this.getSUEntries(isoData);
const getStr = this._getGetString();
for (const entry of entries) {
if (entry instanceof SLEntry) {
const components = entry.componentRecords();
for (const component of components) {
const flags = component.flags();
if (flags & SLComponentFlags.CURRENT) {
p += "./";
} else if (flags & SLComponentFlags.PARENT) {
p += "../";
} else if (flags & SLComponentFlags.ROOT) {
p += "/";
} else {
p += component.content(getStr);
if (!(flags & SLComponentFlags.CONTINUE)) {
p += '/';
}
}
}
if (!entry.continueFlag()) {
// We are done with this link.
break;
}
}
}
if (p.length > 1 && p[p.length - 1] === '/') {
// Trim trailing '/'.
return p.slice(0, p.length - 1);
} else {
return p;
}
}
public getFile(isoData: Buffer): Buffer {
if (this.isDirectory(isoData)) {
throw new Error(`Tried to get a File from a directory.`);
}
if (this._fileOrDir === null) {
this._fileOrDir = isoData.slice(this.lba(), this.lba() + this.dataLength());
}
return <Buffer> this._fileOrDir;
}
public getDirectory(isoData: Buffer): Directory<DirectoryRecord> {
if (!this.isDirectory(isoData)) {
throw new Error(`Tried to get a Directory from a file.`);
}
if (this._fileOrDir === null) {
this._fileOrDir = this._constructDirectory(isoData);
}
return <Directory<this>> this._fileOrDir;
}
public getSUEntries(isoData: Buffer): SystemUseEntry[] {
if (!this._suEntries) {
this._constructSUEntries(isoData);
}
return this._suEntries!;
}
protected abstract _getString(i: number, len: number): string;
protected abstract _getGetString(): TGetString;
protected abstract _constructDirectory(isoData: Buffer): Directory<DirectoryRecord>;
protected _rockRidgeFilename(isoData: Buffer): string | null {
const nmEntries = <NMEntry[]> this.getSUEntries(isoData).filter((e) => e instanceof NMEntry);
if (nmEntries.length === 0 || nmEntries[0].flags() & (NMFlags.CURRENT | NMFlags.PARENT)) {
return null;
}
let str = '';
const getString = this._getGetString();
for (const e of nmEntries) {
str += e.name(getString);
if (!(e.flags() & NMFlags.CONTINUE)) {
break;
}
}
return str;
}
private _constructSUEntries(isoData: Buffer): void {
let i = 33 + this._data[32];
if (i % 2 === 1) {
// Skip padding field.
i++;
}
i += this._rockRidgeOffset;
this._suEntries = constructSystemUseEntries(this._data, i, this.length(), isoData);
}
/**
* !!ONLY VALID ON FIRST ENTRY OF ROOT DIRECTORY!!
* Returns -1 if rock ridge is not enabled. Otherwise, returns the offset
* at which system use fields begin.
*/
private _getRockRidgeOffset(isoData: Buffer): number {
// In the worst case, we get some garbage SU entries.
// Fudge offset to 0 before proceeding.
this._rockRidgeOffset = 0;
const suEntries = this.getSUEntries(isoData);
if (suEntries.length > 0) {
const spEntry = suEntries[0];
if (spEntry instanceof SPEntry && spEntry.checkBytesPass()) {
// SUSP is in use.
for (let i = 1; i < suEntries.length; i++) {
const entry = suEntries[i];
if (entry instanceof RREntry || (entry instanceof EREntry && entry.extensionIdentifier() === rockRidgeIdentifier)) {
// Rock Ridge is in use!
return spEntry.bytesSkipped();
}
}
}
}
// Failed.
this._rockRidgeOffset = -1;
return -1;
}
}
/**
* @hidden
*/
class ISODirectoryRecord extends DirectoryRecord {
constructor(data: Buffer, rockRidgeOffset: number) {
super(data, rockRidgeOffset);
}
protected _getString(i: number, len: number): string {
return getASCIIString(this._data, i, len);
}
protected _constructDirectory(isoData: Buffer): Directory<DirectoryRecord> {
return new ISODirectory(this, isoData);
}
protected _getGetString(): TGetString {
return getASCIIString;
}
}
/**
* @hidden
*/
class JolietDirectoryRecord extends DirectoryRecord {
constructor(data: Buffer, rockRidgeOffset: number) {
super(data, rockRidgeOffset);
}
protected _getString(i: number, len: number): string {
return getJolietString(this._data, i, len);
}
protected _constructDirectory(isoData: Buffer): Directory<DirectoryRecord> {
return new JolietDirectory(this, isoData);
}
protected _getGetString(): TGetString {
return getJolietString;
}
}
/**
* @hidden
*/
const enum SystemUseEntrySignatures {
CE = 0x4345,
PD = 0x5044,
SP = 0x5350,
ST = 0x5354,
ER = 0x4552,
ES = 0x4553,
PX = 0x5058,
PN = 0x504E,
SL = 0x534C,
NM = 0x4E4D,
CL = 0x434C,
PL = 0x504C,
RE = 0x5245,
TF = 0x5446,
SF = 0x5346,
RR = 0x5252
}
/**
* @hidden
*/
class SystemUseEntry {
protected _data: Buffer;
constructor(data: Buffer) {
this._data = data;
}
public signatureWord(): SystemUseEntrySignatures {
return this._data.readUInt16BE(0);
}
public signatureWordString(): string {
return getASCIIString(this._data, 0, 2);
}
public length(): number {
return this._data[2];
}
public suVersion(): number {
return this._data[3];
}
}
/**
* Continuation entry.
* @hidden
*/
class CEEntry extends SystemUseEntry {
private _entries: SystemUseEntry[] | null = null;
constructor(data: Buffer) {
super(data);
}
/**
* Logical block address of the continuation area.
*/
public continuationLba(): number {
return this._data.readUInt32LE(4);
}
/**
* Offset into the logical block.
*/
public continuationLbaOffset(): number {
return this._data.readUInt32LE(12);
}
/**
* Length of the continuation area.
*/
public continuationLength(): number {
return this._data.readUInt32LE(20);
}
public getEntries(isoData: Buffer): SystemUseEntry[] {
if (!this._entries) {
const start = this.continuationLba() * 2048 + this.continuationLbaOffset();
this._entries = constructSystemUseEntries(isoData, start, this.continuationLength(), isoData);
}
return this._entries;
}
}
/**
* Padding entry.
* @hidden
*/
class PDEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
}
/**
* Identifies that SUSP is in-use.
* @hidden
*/
class SPEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public checkBytesPass(): boolean {
return this._data[4] === 0xBE && this._data[5] === 0xEF;
}
public bytesSkipped(): number {
return this._data[6];
}
}
/**
* Identifies the end of the SUSP entries.
* @hidden
*/
class STEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
}
/**
* Specifies system-specific extensions to SUSP.
* @hidden
*/
class EREntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public identifierLength(): number {
return this._data[4];
}
public descriptorLength(): number {
return this._data[5];
}
public sourceLength(): number {
return this._data[6];
}
public extensionVersion(): number {
return this._data[7];
}
public extensionIdentifier(): string {
return getASCIIString(this._data, 8, this.identifierLength());
}
public extensionDescriptor(): string {
return getASCIIString(this._data, 8 + this.identifierLength(), this.descriptorLength());
}
public extensionSource(): string {
return getASCIIString(this._data, 8 + this.identifierLength() + this.descriptorLength(), this.sourceLength());
}
}
/**
* @hidden
*/
class ESEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public extensionSequence(): number {
return this._data[4];
}
}
/**
* RockRidge: Marks that RockRidge is in use [deprecated]
* @hidden
*/
class RREntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
}
/**
* RockRidge: Records POSIX file attributes.
* @hidden
*/
class PXEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public mode(): number {
return this._data.readUInt32LE(4);
}
public fileLinks(): number {
return this._data.readUInt32LE(12);
}
public uid(): number {
return this._data.readUInt32LE(20);
}
public gid(): number {
return this._data.readUInt32LE(28);
}
public inode(): number {
return this._data.readUInt32LE(36);
}
}
/**
* RockRidge: Records POSIX device number.
* @hidden
*/
class PNEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public devTHigh(): number {
return this._data.readUInt32LE(4);
}
public devTLow(): number {
return this._data.readUInt32LE(12);
}
}
/**
* RockRidge: Records symbolic link
* @hidden
*/
class SLEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public flags(): number {
return this._data[4];
}
public continueFlag(): number {
return this.flags() & 0x1;
}
public componentRecords(): SLComponentRecord[] {
const records = new Array<SLComponentRecord>();
let i = 5;
while (i < this.length()) {
const record = new SLComponentRecord(this._data.slice(i));
records.push(record);
i += record.length();
}
return records;
}
}
/**
* @hidden
*/
const enum SLComponentFlags {
CONTINUE = 1,
CURRENT = 1 << 1,
PARENT = 1 << 2,
ROOT = 1 << 3
}
/**
* @hidden
*/
class SLComponentRecord {
private _data: Buffer;
constructor(data: Buffer) {
this._data = data;
}
public flags(): SLComponentFlags {
return this._data[0];
}
public length(): number {
return 2 + this.componentLength();
}
public componentLength(): number {
return this._data[1];
}
public content(getString: TGetString): string {
return getString(this._data, 2, this.componentLength());
}
}
/**
* @hidden
*/
const enum NMFlags {
CONTINUE = 1,
CURRENT = 1 << 1,
PARENT = 1 << 2
}
/**
* RockRidge: Records alternate file name
* @hidden
*/
class NMEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public flags(): NMFlags {
return this._data[4];
}
public name(getString: TGetString): string {
return getString(this._data, 5, this.length() - 5);
}
}
/**
* RockRidge: Records child link
* @hidden
*/
class CLEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public childDirectoryLba(): number {
return this._data.readUInt32LE(4);
}
}
/**
* RockRidge: Records parent link.
* @hidden
*/
class PLEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public parentDirectoryLba(): number {
return this._data.readUInt32LE(4);
}
}
/**
* RockRidge: Records relocated directory.
* @hidden
*/
class REEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
}
/**
* @hidden
*/
const enum TFFlags {
CREATION = 1,
MODIFY = 1 << 1,
ACCESS = 1 << 2,
ATTRIBUTES = 1 << 3,
BACKUP = 1 << 4,
EXPIRATION = 1 << 5,
EFFECTIVE = 1 << 6,
LONG_FORM = 1 << 7
}
/**
* RockRidge: Records file timestamps
* @hidden
*/
class TFEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public flags(): number {
return this._data[4];
}
public creation(): Date | null {
if (this.flags() & TFFlags.CREATION) {
if (this._longFormDates()) {
return getDate(this._data, 5);
} else {
return getShortFormDate(this._data, 5);
}
} else {
return null;
}
}
public modify(): Date | null {
if (this.flags() & TFFlags.MODIFY) {
const previousDates = (this.flags() & TFFlags.CREATION) ? 1 : 0;
if (this._longFormDates) {
return getDate(this._data, 5 + (previousDates * 17));
} else {
return getShortFormDate(this._data, 5 + (previousDates * 7));
}
} else {
return null;
}
}
public access(): Date | null {
if (this.flags() & TFFlags.ACCESS) {
let previousDates = (this.flags() & TFFlags.CREATION) ? 1 : 0;
previousDates += (this.flags() & TFFlags.MODIFY) ? 1 : 0;
if (this._longFormDates) {
return getDate(this._data, 5 + (previousDates * 17));
} else {
return getShortFormDate(this._data, 5 + (previousDates * 7));
}
} else {
return null;
}
}
public backup(): Date | null {
if (this.flags() & TFFlags.BACKUP) {
let previousDates = (this.flags() & TFFlags.CREATION) ? 1 : 0;
previousDates += (this.flags() & TFFlags.MODIFY) ? 1 : 0;
previousDates += (this.flags() & TFFlags.ACCESS) ? 1 : 0;
if (this._longFormDates) {
return getDate(this._data, 5 + (previousDates * 17));
} else {
return getShortFormDate(this._data, 5 + (previousDates * 7));
}
} else {
return null;
}
}
public expiration(): Date | null {
if (this.flags() & TFFlags.EXPIRATION) {
let previousDates = (this.flags() & TFFlags.CREATION) ? 1 : 0;
previousDates += (this.flags() & TFFlags.MODIFY) ? 1 : 0;
previousDates += (this.flags() & TFFlags.ACCESS) ? 1 : 0;
previousDates += (this.flags() & TFFlags.BACKUP) ? 1 : 0;
if (this._longFormDates) {
return getDate(this._data, 5 + (previousDates * 17));
} else {
return getShortFormDate(this._data, 5 + (previousDates * 7));
}
} else {
return null;
}
}
public effective(): Date | null {
if (this.flags() & TFFlags.EFFECTIVE) {
let previousDates = (this.flags() & TFFlags.CREATION) ? 1 : 0;
previousDates += (this.flags() & TFFlags.MODIFY) ? 1 : 0;
previousDates += (this.flags() & TFFlags.ACCESS) ? 1 : 0;
previousDates += (this.flags() & TFFlags.BACKUP) ? 1 : 0;
previousDates += (this.flags() & TFFlags.EXPIRATION) ? 1 : 0;
if (this._longFormDates) {
return getDate(this._data, 5 + (previousDates * 17));
} else {
return getShortFormDate(this._data, 5 + (previousDates * 7));
}
} else {
return null;
}
}
private _longFormDates(): boolean {
return !!(this.flags() && TFFlags.LONG_FORM);
}
}
/**
* RockRidge: File data in sparse format.
* @hidden
*/
class SFEntry extends SystemUseEntry {
constructor(data: Buffer) {
super(data);
}
public virtualSizeHigh(): number {
return this._data.readUInt32LE(4);
}
public virtualSizeLow(): number {
return this._data.readUInt32LE(12);
}
public tableDepth(): number {
return this._data[20];
}
}
/**
* @hidden
*/
abstract class Directory<T extends DirectoryRecord> {
protected _record: T;
private _fileList: string[] = [];
private _fileMap: {[name: string]: T} = {};
constructor(record: T, isoData: Buffer) {
this._record = record;
let i = record.lba();
let iLimit = i + record.dataLength();
if (!(record.fileFlags() & FileFlags.Directory)) {
// Must have a CL entry.
const cl = <CLEntry> record.getSUEntries(isoData).filter((e) => e instanceof CLEntry)[0];
i = cl.childDirectoryLba() * 2048;
iLimit = Infinity;
}
while (i < iLimit) {
const len = isoData[i];
// Zero-padding between sectors.
// TODO: Could optimize this to seek to nearest-sector upon
// seeing a 0.
if (len === 0) {
i++;
continue;
}
const r = this._constructDirectoryRecord(isoData.slice(i));
const fname = r.fileName(isoData);
// Skip '.' and '..' entries.
if (fname !== '\u0000' && fname !== '\u0001') {
// Skip relocated entries.
if (!r.hasRockRidge() || r.getSUEntries(isoData).filter((e) => e instanceof REEntry).length === 0) {
this._fileMap[fname] = r;
this._fileList.push(fname);
}
} else if (iLimit === Infinity) {
// First entry contains needed data.
iLimit = i + r.dataLength();
}
i += r.length();
}
}
/**
* Get the record with the given name.
* Returns undefined if not present.
*/
public getRecord(name: string): DirectoryRecord {
return this._fileMap[name];
}
public getFileList(): string[] {
return this._fileList;
}
public getDotEntry(isoData: Buffer): T {
return this._constructDirectoryRecord(isoData.slice(this._record.lba()));
}
protected abstract _constructDirectoryRecord(data: Buffer): T;
}
/**
* @hidden
*/
class ISODirectory extends Directory<ISODirectoryRecord> {
constructor(record: ISODirectoryRecord, isoData: Buffer) {
super(record, isoData);
}
protected _constructDirectoryRecord(data: Buffer): ISODirectoryRecord {
return new ISODirectoryRecord(data, this._record.getRockRidgeOffset());
}
}
/**
* @hidden
*/
class JolietDirectory extends Directory<JolietDirectoryRecord> {
constructor(record: JolietDirectoryRecord, isoData: Buffer) {
super(record, isoData);
}
protected _constructDirectoryRecord(data: Buffer): JolietDirectoryRecord {
return new JolietDirectoryRecord(data, this._record.getRockRidgeOffset());
}
}
/**
* Options for IsoFS file system instances.
*/
export interface IsoFSOptions {
// The ISO file in a buffer.
data: Buffer;
// The name of the ISO (optional; used for debug messages / identification via getName()).
name?: string;
}
/**
* Mounts an ISO file as a read-only file system.
*
* Supports:
* * Vanilla ISO9660 ISOs
* * Microsoft Joliet and Rock Ridge extensions to the ISO9660 standard
*/
export default class IsoFS extends SynchronousFileSystem implements FileSystem {
public static readonly Name = "IsoFS";
public static readonly Options: FileSystemOptions = {
data: {
type: "object",
description: "The ISO file in a buffer",
validator: bufferValidator
}
};
/**
* Creates an IsoFS instance with the given options.
*/
public static Create(opts: IsoFSOptions, cb: BFSCallback<IsoFS>): void {
let fs: IsoFS | undefined;
let e: ApiError | undefined;
try {
fs = new IsoFS(opts.data, opts.name, false);
} catch (e) {
e = e;
} finally {
cb(e, fs);
}
}
public static isAvailable(): boolean {
return true;
}
private _data: Buffer;
private _pvd: PrimaryOrSupplementaryVolumeDescriptor;
private _root: DirectoryRecord;
private _name: string;
/**
* **Deprecated. Please use IsoFS.Create() method instead.**
*
* Constructs a read-only file system from the given ISO.
* @param data The ISO file in a buffer.
* @param name The name of the ISO (optional; used for debug messages / identification via getName()).
*/
constructor(data: Buffer, name: string = "", deprecateMsg = true) {
super();
this._data = data;
deprecationMessage(deprecateMsg, IsoFS.Name, {data: "ISO data as a Buffer", name: name});
// Skip first 16 sectors.
let vdTerminatorFound = false;
let i = 16 * 2048;
const candidateVDs = new Array<PrimaryOrSupplementaryVolumeDescriptor>();
while (!vdTerminatorFound) {
const slice = data.slice(i);
const vd = new VolumeDescriptor(slice);
switch (vd.type()) {
case VolumeDescriptorTypeCode.PrimaryVolumeDescriptor:
candidateVDs.push(new PrimaryVolumeDescriptor(slice));
break;
case VolumeDescriptorTypeCode.SupplementaryVolumeDescriptor:
candidateVDs.push(new SupplementaryVolumeDescriptor(slice));
break;
case VolumeDescriptorTypeCode.VolumeDescriptorSetTerminator:
vdTerminatorFound = true;
break;
}
i += 2048;
}
if (candidateVDs.length === 0) {
throw new ApiError(ErrorCode.EIO, `Unable to find a suitable volume descriptor.`);
}
candidateVDs.forEach((v) => {
// Take an SVD over a PVD.
if (!this._pvd || this._pvd.type() !== VolumeDescriptorTypeCode.SupplementaryVolumeDescriptor) {
this._pvd = v;
}
});
this._root = this._pvd.rootDirectoryEntry(data);
this._name = name;
}
public getName(): string {
let name = `IsoFS${this._name}${this._pvd ? `-${this._pvd.name()}` : ''}`;
if (this._root && this._root.hasRockRidge()) {
name += `-RockRidge`;
}
return name;
}
public diskSpace(path: string, cb: (total: number, free: number) => void): void {
// Read-only file system.
cb(this._data.length, 0);
}
public isReadOnly(): boolean {
return true;
}
public supportsLinks(): boolean {
return false;
}
public supportsProps(): boolean {
return false;
}
public supportsSynch(): boolean {
return true;
}
public statSync(p: string, isLstat: boolean): Stats {
const record = this._getDirectoryRecord(p);
if (record === null) {
throw ApiError.ENOENT(p);
}
return this._getStats(p, record)!;
}
public openSync(p: string, flags: FileFlag, mode: number): File {
// INVARIANT: Cannot write to RO file systems.
if (flags.isWriteable()) {
throw new ApiError(ErrorCode.EPERM, p);
}
// Check if the path exists, and is a file.
const record = this._getDirectoryRecord(p);
if (!record) {
throw ApiError.ENOENT(p);
} else if (record.isSymlink(this._data)) {
return this.openSync(path.resolve(p, record.getSymlinkPath(this._data)), flags, mode);
} else if (!record.isDirectory(this._data)) {
const data = record.getFile(this._data);
const stats = this._getStats(p, record)!;
switch (flags.pathExistsAction()) {
case ActionType.THROW_EXCEPTION:
case ActionType.TRUNCATE_FILE:
throw ApiError.EEXIST(p);
case ActionType.NOP:
return new NoSyncFile(this, p, flags, stats, data);
default:
throw new ApiError(ErrorCode.EINVAL, 'Invalid FileMode object.');
}
} else {
throw ApiError.EISDIR(p);
}
}
public readdirSync(path: string): string[] {
// Check if it exists.
const record = this._getDirectoryRecord(path);
if (!record) {
throw ApiError.ENOENT(path);
} else if (record.isDirectory(this._data)) {
return record.getDirectory(this._data).getFileList().slice(0);
} else {
throw ApiError.ENOTDIR(path);
}
}
/**
* Specially-optimized readfile.
*/
public readFileSync(fname: string, encoding: string, flag: FileFlag): any {
// Get file.
const fd = this.openSync(fname, flag, 0x1a4);
try {
const fdCast = <NoSyncFile<IsoFS>> fd;
const fdBuff = <Buffer> fdCast.getBuffer();
if (encoding === null) {
return copyingSlice(fdBuff);
}
return fdBuff.toString(encoding);
} finally {
fd.closeSync();
}
}
private _getDirectoryRecord(path: string): DirectoryRecord | null {
// Special case.
if (path === '/') {
return this._root;
}
const components = path.split('/').slice(1);
let dir = this._root;
for (const component of components) {
if (dir.isDirectory(this._data)) {
dir = dir.getDirectory(this._data).getRecord(component);
if (!dir) {
return null;
}
} else {
return null;
}
}
return dir;
}
private _getStats(p: string, record: DirectoryRecord): Stats | null {
if (record.isSymlink(this._data)) {
const newP = path.resolve(p, record.getSymlinkPath(this._data));
const dirRec = this._getDirectoryRecord(newP);
if (!dirRec) {
return null;
}
return this._getStats(newP, dirRec);
} else {
const len = record.dataLength();
let mode = 0x16D;
const date = record.recordingDate();
let atime = date;
let mtime = date;
let ctime = date;
if (record.hasRockRidge()) {
const entries = record.getSUEntries(this._data);
for (const entry of entries) {
if (entry instanceof PXEntry) {
mode = entry.mode();
} else if (entry instanceof TFEntry) {
const flags = entry.flags();
if (flags & TFFlags.ACCESS) {
atime = entry.access()!;
}
if (flags & TFFlags.MODIFY) {
mtime = entry.modify()!;
}
if (flags & TFFlags.CREATION) {
ctime = entry.creation()!;
}
}
}
}
// Mask out writeable flags. This is a RO file system.
mode = mode & 0x16D;
return new Stats(record.isDirectory(this._data) ? FileType.DIRECTORY : FileType.FILE, len, mode, atime, mtime, ctime);
}
}
}