Initial commit

master
Sven Slootweg 8 years ago
commit 652b8d47d5

3
.gitignore vendored

@ -0,0 +1,3 @@
log.txt
test
node_modules

@ -0,0 +1,67 @@
'use strict';
const Promise = require("bluebird");
const streamToPromise = require("stream-to-promise");
const fs = require("fs");
const memoizee = require("memoizee");
const archy = require("archy");
const httpSeekable = require("./lib/seekable/http");
const fsSeekable = require("./lib/seekable/fs");
const iso9660 = require("./lib/iso9660");
let imagePath = process.argv[2];
Promise.try(() => {
if (imagePath.match(/^https?:\/\//)) {
return httpSeekable(imagePath);
} else {
return fsSeekable(imagePath);
}
}).then((seekable) => {
let image = iso9660(seekable);
return Promise.try(() => {
return image.getRootDirectory();
}).then((rootDirectory) => {
return rootDirectory.getTree();
}).then((tree) => {
function makeArchy(node) {
if (node.type === "directory") {
return {
label: node.filename,
nodes: node.children.map(makeArchy)
}
} else {
return node.filename
}
}
let archyNodes = {
label: "(root)",
nodes: tree.map(makeArchy)
}
console.log(archy(archyNodes));
});
/*
let image = iso9660(seekable);
return Promise.try(() => {
return image.getPathTable();
}).then((paths) => {
//console.log(paths);
return image.getPrimaryVolumeDescriptor();
}).then((primaryVolumeDescriptor) => {
console.log(JSON.stringify(primaryVolumeDescriptor, (key, value) => {
if (value != null && value.type === "Buffer") {
return require("util").inspect(Buffer.from(value));
} else {
return value;
}
}, "\t"));
})
*/
});

@ -0,0 +1,43 @@
'use strict';
const types = require("./types");
module.exports = function createBufferReader(buffer) {
let typeSpecs = {
strA: {},
strD: {},
strFilename: {},
int8: { length: 1 },
sint8: { length: 1 },
int16_LSB: { length: 2 },
int16_MSB: { length: 2 },
int16_LSB_MSB: { length: 4 },
sint16_LSB: { length: 2 },
sint16_MSB: { length: 2 },
sint16_LSB_MSB: { length: 4 },
int32_LSB: { length: 4 },
int32_MSB: { length: 4 },
int32_LSB_MSB: { length: 8 },
sint32_LSB: { length: 4 },
sint32_MSB: { length: 4 },
sint32_LSB_MSB: { length: 8 },
decDatetime: { length: 17 },
directoryDatetime: { length: 7 }
}
return Object.keys(typeSpecs).reduce((methods, type) => {
let options = typeSpecs[type];
if (options.length == null) {
methods[type] = function(offset, length) {
return types[type](buffer.slice(offset, offset + length));
}
} else {
methods[type] = function(offset) {
return types[type](buffer.slice(offset, offset + options.length));
}
}
return methods;
}, {});
};

@ -0,0 +1,72 @@
'use strict';
const Promise = require("bluebird");
const streamToPromise = require("stream-to-promise");
const memoizee = require("memoizee");
const parseVolumeDescriptor = require("./parse/volume-descriptor");
const parsePathTable = require("./parse/path-table");
let imageOffset = 32 * 1024; // first 32KB are system area, ref http://wiki.osdev.org/ISO_9660#System_Area
module.exports = function createImage(seekable) {
let image = {
createReadStream: function createReadStream(start, end) {
return Promise.try(() => {
return seekable.createReadStream(start, end);
});
},
readRange: function readRange(start, end) {
return Promise.try(() => {
return this.createReadStream(start, end);
}).then((stream) => {
return streamToPromise(stream);
});
},
getPrimaryVolumeDescriptorBuffer: memoizee(function getPrimaryVolumeDescriptorBuffer() {
return this.readRange(imageOffset, imageOffset + 2047);
}),
getPrimaryVolumeDescriptor: function getPrimaryVolumeDescriptor() {
return Promise.try(() => {
return this.getPrimaryVolumeDescriptorBuffer();
}).then((buffer) => {
return parseVolumeDescriptor(buffer);
});
},
getSectorOffset: function getSectorOffset(sector) {
return Promise.try(() => {
return this.getPrimaryVolumeDescriptor();
}).then((primaryVolumeDescriptor) => {
return primaryVolumeDescriptor.data.sectorSize * sector;
});
},
getPathTable: function getPathTable() {
return Promise.try(() => {
return this.getPrimaryVolumeDescriptor();
}).then((primaryVolumeDescriptor) => {
return Promise.try(() => {
let start = primaryVolumeDescriptor.data.pathTableLocationL * primaryVolumeDescriptor.data.sectorSize;
let end = start + primaryVolumeDescriptor.data.pathTableSize - 1;
return this.readRange(start, end);
}).then((buffer) => {
return parsePathTable(buffer, primaryVolumeDescriptor.data.sectorSize);
});
});
},
getRootDirectory: function getRootDirectory() {
return Promise.try(() => {
return this.getPrimaryVolumeDescriptor();
}).then((primaryVolumeDescriptor) => {
return createDirectory(primaryVolumeDescriptor.data.rootDirectory);
});
}
}
const createDirectory = require("./object/directory")(image);
return image;
}

@ -0,0 +1,87 @@
'use strict';
const Promise = require("bluebird");
const memoizee = require("memoizee");
const debug = require("debug")("iso9660:object:directory");
const parseDirectoryRecord = require("../parse/directory-record");
module.exports = function(image) {
const createFile = require("./file")(image);
return function createDirectory(directoryRecord, parent) {
debug(`Creating new directory object for ${directoryRecord.identifier.filename}`);
return {
type: "directory",
filename: directoryRecord.identifier.filename,
version: directoryRecord.identifier.version,
parent: parent,
recordingDate: directoryRecord.recordingDate,
getChildren: memoizee(function getChildren() {
return Promise.try(() => {
function getDirectoryContents() {
return Promise.try(() => {
return image.getSectorOffset(directoryRecord.extentLocation);
}).then((sectorOffset) => {
return image.readRange(sectorOffset, sectorOffset + directoryRecord.extentSize - 1);
});
}
return Promise.all([
image.getPrimaryVolumeDescriptor(),
getDirectoryContents()
]);
}).spread((primaryVolumeDescriptor, directoryContents) => {
const roundToNextSector = require("../round-to-next-sector")(primaryVolumeDescriptor.data.sectorSize);
let pos = 0;
let records = [];
while (pos < directoryContents.length) {
let recordLength = directoryContents.readUInt8(pos);
if (recordLength === 0) {
/* We ran out of records for this sector, skip to the next. */
pos = roundToNextSector(pos);
} else {
let directoryRecord = parseDirectoryRecord(directoryContents.slice(pos, pos + recordLength));
records.push(directoryRecord);
pos += recordLength;
}
}
return records.slice(2).map((record) => {
if (record.fileFlags.directory) {
debug(`Found directory: ${record.identifier.filename}`);
return createDirectory(record, this);
} else {
debug(`Found file: ${record.identifier.filename}`);
return createFile(record, this);
}
});
});
}),
getTree: function getTree() {
return Promise.try(() => {
return this.getChildren();
}).map((child) => {
if (child.type === "directory") {
return Promise.try(() => {
return child.getTree();
}).then((tree) => {
return Object.assign({
children: tree
}, child);
});
} else {
return child;
}
});
}
}
}
}

@ -0,0 +1,13 @@
'use strict';
module.exports = function(image) {
return function createFile(record, parent) {
return {
type: "file",
filename: record.identifier.filename,
version: record.identifier.version,
parent: parent,
recordingDate: record.recordingDate
}
}
}

@ -0,0 +1,13 @@
'use strict';
const createBufferReader = require("../../buffer-reader");
module.exports = function parseBootRecord(data) {
let bufferReader = createBufferReader(data);
return {
bootSystemIdentifier: bufferReader.strA(0, 32),
bootIdentifier: bufferReader.strA(32, 32),
bootData: data.slice(64, 2041) // FIXME: El Torito
}
};

@ -0,0 +1,5 @@
'use strict';
module.exports = function parseVolumeDescriptorSetTerminator(data) {
return {};
};

@ -0,0 +1,35 @@
'use strict';
const createBufferReader = require("../../buffer-reader");
const parseDirectoryRecord = require("../directory-record");
module.exports = function parsePrimaryVolumeDescriptor(data) {
let bufferReader = createBufferReader(data);
return {
systemIdentifier: bufferReader.strA(1, 32),
volumeIdentifier: bufferReader.strD(33, 32),
sectorCount: bufferReader.int32_LSB_MSB(73),
setSize: bufferReader.int16_LSB_MSB(113),
sequenceNumber: bufferReader.int16_LSB_MSB(117),
sectorSize: bufferReader.int16_LSB_MSB(121),
pathTableSize: bufferReader.int32_LSB_MSB(125),
pathTableLocationL: bufferReader.int32_LSB(133), // FIXME: Pointer? (location is expressed in 'sectors')
optionalPathTableLocationL: bufferReader.int32_LSB(137), // FIXME: Pointer? (location is expressed in 'sectors')
pathTableLocationM: bufferReader.int32_MSB(141), // FIXME: Pointer? (location is expressed in 'sectors')
optionalPathTableLocationM: bufferReader.int32_MSB(145), // FIXME: Pointer? (location is expressed in 'sectors')
rootDirectory: parseDirectoryRecord(data.slice(149, 149 + 34)),
publisherIdentifier: bufferReader.strA(311, 128), // FIXME: null for unspecified & extended publisher information
dataPreparerIdentifier: bufferReader.strA(439, 128), // FIXME: null for unspecified & extended publisher information
applicationIdentifier: bufferReader.strA(567, 128), // FIXME: null for unspecified & extended publisher information
copyrightFile: bufferReader.strD(695, 38), // FIXME: seek for file, optionally?
abstractFile: bufferReader.strD(733, 36), // FIXME: seek for file, optionally?
bibliographicFile: bufferReader.strD(769, 37), // FIXME: seek for file, optionally?
creationDate: bufferReader.decDatetime(806),
modificationDate: bufferReader.decDatetime(823),
expirationDate: bufferReader.decDatetime(840),
effectiveDate: bufferReader.decDatetime(857),
fileStructureVersion: bufferReader.int8(874),
applicationData: data.slice(876, 876 + 512)
}
};

@ -0,0 +1,5 @@
'use strict';
module.exports = function parseSupplementaryVolumeDescriptor(data) {
throw new Error("Not implemented yet");
};

@ -0,0 +1,5 @@
'use strict';
module.exports = function parseVolumePartitionDescriptor(data) {
throw new Error("Not implemented yet");
};

@ -0,0 +1,48 @@
'use strict';
const createBufferReader = require("../buffer-reader");
const roundEven = require("../../round-even");
function parseFileFlags(value) {
/* File flags: (ref http://wiki.osdev.org/ISO_9660#Directories)
* Bit Description
* 0 If set, the existence of this file need not be made known to the user (basically a 'hidden' flag.
* 1 If set, this record describes a directory (in other words, it is a subdirectory extent).
* 2 If set, this file is an "Associated File".
* 3 If set, the extended attribute record contains information about the format of this file.
* 4 If set, owner and group permissions are set in the extended attribute record.
* 5 & 6 Reserved
* 7 If set, this is not the final directory record for this file (for files spanning several extents, for example files over 4GiB long.
*/
return {
hidden: !!(value & 1),
directory: !!(value & 2),
associated: !!(value & 4),
inEAR: !!(value & 8),
permissionsInEAR: !!(value & 16),
moreRecordsFollowing: !!(value & 128)
}
}
module.exports = function parseDirectoryRecord(data) {
let bufferReader = createBufferReader(data);
let identifierLength = bufferReader.int8(32);
let identifierEnd = roundEven(32 + identifierLength);
return {
recordLength: bufferReader.int8(0),
extendedAttributeRecordLength: bufferReader.int8(1),
extentLocation: bufferReader.int32_LSB_MSB(2),
extentSize: bufferReader.int32_LSB_MSB(10),
recordingDate: bufferReader.directoryDatetime(18),
fileFlags: parseFileFlags(bufferReader.int8(25)),
interleavedUnitSize: bufferReader.int8(26),
interleavedGapSize: bufferReader.int8(27),
sequenceNumber: bufferReader.int16_LSB_MSB(28),
identifierLength: identifierLength,
identifier: bufferReader.strFilename(33, identifierLength),
systemUse: data.slice(identifierEnd, 254) // FIXME: Extensions!
}
};

@ -0,0 +1,28 @@
'use strict';
const createBufferReader = require("../buffer-reader");
module.exports = function parsePathTableRecord(data, tableType) {
let bufferReader = createBufferReader(data);
let extentLocation, parentDirectoryIndex;
let identifierLength = bufferReader.int8(0);
if (tableType === "L") {
extentLocation = bufferReader.int32_LSB(2);
parentDirectoryIndex = bufferReader.int16_LSB(6);
} else if (tableType === "M") {
extentLocation = bufferReader.int32_MSB(2);
parentDirectoryIndex = bufferReader.int16_MSB(6);
} else {
throw new Error("Unknown path table type");
}
return {
identifierLength: identifierLength,
earLength: bufferReader.int8(1),
extentLocation: extentLocation,
parentDirectoryIndex: parentDirectoryIndex,
identifier: bufferReader.strFilename(8, identifierLength)
}
};

@ -0,0 +1,34 @@
'use strict';
const debug = require("debug")("iso9660:parse:path-table");
const roundEven = require("../../round-even");
const parsePathTableRecord = require("./path-table-record");
module.exports = function parsePathTable(buffer, sectorSize) {
function roundToNextSector(position) {
return Math.ceil(position / sectorSize) * sectorSize;
}
let records = [];
let pos = 0;
while (pos < buffer.length) {
let recordIdentifierLength = buffer.readUInt8(pos);
let recordLength = roundEven(recordIdentifierLength + 8);
if (recordIdentifierLength === 0) {
/* Reached the end of records for this section, throw away the remainder. */
debug("Ran out of records, skipping to next sector...");
pos = roundToNextSector(pos);
} else {
let parsedRecord = parsePathTableRecord(buffer.slice(pos, pos + recordLength), "L");
debug(`Found: ${parsedRecord.identifier.filename}`);
records.push(parsedRecord);
pos += recordLength;
}
}
return records;
};

@ -0,0 +1,60 @@
'use strict';
const Buffer = require("safe-buffer").Buffer;
const createBufferReader = require("../buffer-reader");
const parseBootRecord = require("./descriptors/boot-record");
const parsePrimaryVolumeDescriptor = require("./descriptors/primary-volume");
const parseSupplementaryVolumeDescriptor = require("./descriptors/supplementary-volume");
const parseVolumePartitionDescriptor = require("./descriptors/volume-partition");
const parseVolumeDescriptorSetTerminator = require("./descriptors/descriptor-set-terminator");
let identifierCD001 = Buffer.from("CD001");
function getVolumeDescriptorType(typeNumber) {
if (typeNumber === 0) {
return "boot";
} else if (typeNumber === 1) {
return "primary";
} else if (typeNumber === 2) {
return "supplementary";
} else if (typeNumber === 3) {
return "partition";
} else if (typeNumber >= 4 && typeNumber <= 254) {
return "reserved";
} else if (typeNumber === 255) {
return "terminator";
} else {
throw new Error("Unknown volume descriptor type");
}
}
function parseVolumeDescriptorData(type, dataBuffer) {
if (type === "boot") {
return parseBootRecord(dataBuffer);
} else if (type === "primary") {
return parsePrimaryVolumeDescriptor(dataBuffer);
} else if (type === "supplementary") {
return parseSupplementaryVolumeDescriptor(dataBuffer);
} else if (type === "partition") {
return parseVolumePartitionDescriptor(dataBuffer);
} else if (type === "terminator") {
return parseVolumeDescriptorSetTerminator(dataBuffer);
}
}
module.exports = function parseVolumeDescriptor(data) {
if (!data.slice(1, 6).equals(identifierCD001)) {
throw new Error("Not a valid volume descriptor - CD001 identifier is missing");
}
let buffer = createBufferReader(data);
let descriptorType = getVolumeDescriptorType(buffer.int8(0));
return {
type: descriptorType,
version: buffer.int8(6),
data: parseVolumeDescriptorData(descriptorType, data.slice(7, 2048))
}
};

@ -0,0 +1,7 @@
'use strict';
module.exports = function(sectorSize) {
return function roundToNextSector(position) {
return Math.ceil(position / sectorSize) * sectorSize;
}
}

@ -0,0 +1,130 @@
'use strict';
const moment = require("moment");
const removeRightPadding = require("../remove-right-padding");
/* Basic ISO 9660 types: (ref http://wiki.osdev.org/ISO_9660#Numerical_formats)
* int8 Unsigned 8-bit integer.
* sint8 Signed 8-bit integer.
* int16_LSB Little-endian encoded unsigned 16-bit integer.
* int16_MSB Big-endian encoded unsigned 16-bit integer.
* int16_LSB-MSB Little-endian followed by big-endian encoded unsigned 16-bit integer.
* sint16_LSB Little-endian encoded signed 16-bit integer.
* sint16_MSB Big-endian encoded signed 16-bit integer.
* sint16_LSB-MSB Little-endian followed by big-endian encoded signed 16-bit integer.
* int32_LSB Little-endian encoded unsigned 32-bit integer.
* int32_MSB Big-endian encoded unsigned 32-bit integer.
* int32_LSB-MSB Little-endian followed by big-endian encoded unsigned 32-bit integer.
* sint32_LSB Little-endian encoded signed 32-bit integer.
* sint32_MSB Big-endian encoded signed 32-bit integer.
* sint32_LSB-MSB Little-endian followed by big-endian encoded signed 32-bit integer.
*
* "Where a both-endian format is present, the x86 architecture makes use of the first little-endian sequence and ignores the big-endian sequence."
*/
function offsetInMinutes(value) {
/* "Time zone offset from GMT in 15 minute intervals, starting at interval -48 (west) and running up to interval 52 (east).
* So value 0 indicates interval -48 which equals GMT-12 hours, and value 100 indicates interval 52 which equals GMT+13 hours."
*
* ref: http://wiki.osdev.org/ISO_9660#Date.2Ftime_format
*/
return (value - 48) * 15;
}
module.exports = {
/* String types */
strA: function parseStrA(buffer) {
return removeRightPadding(buffer.toString("ascii"));
},
strD: function parseStrD(buffer) {
return this.strA(buffer);
},
strFilename: function parseStrFilename(buffer) {
let str = this.strA(buffer);
let [filename, version] = str.split(";");
return {filename, version};
},
/* 8-bit integers */
int8: function parseInt8(buffer) {
return buffer.readUInt8(0);
},
sint8: function parseSInt8(buffer) {
return buffer.readInt8(0);
},
/* 16-bit integers */
int16_LSB: function parseInt16_LSB(buffer) {
return buffer.readUInt16LE(0);
},
int16_MSB: function parseInt16_LSB(buffer) {
return buffer.readUInt16BE(0);
},
int16_LSB_MSB: function parseInt16_LSB(buffer) {
return this.int16_LSB(buffer.slice(0, 2));
},
sint16_LSB: function parseSInt16_LSB(buffer) {
return buffer.readInt16LE(0);
},
sint16_MSB: function parseSInt16_LSB(buffer) {
return buffer.readInt16BE(0);
},
sint16_LSB_MSB: function parseSInt16_LSB(buffer) {
return this.sint16_LSB(buffer.slice(0, 2));
},
/* 32-bit integers */
int32_LSB: function parseInt32_LSB(buffer) {
return buffer.readUInt32LE(0);
},
int32_MSB: function parseInt32_LSB(buffer) {
return buffer.readUInt32BE(0);
},
int32_LSB_MSB: function parseInt32_LSB(buffer) {
return this.int32_LSB(buffer.slice(0, 4));
},
sint32_LSB: function parseSInt32_LSB(buffer) {
return buffer.readInt32LE(0);
},
sint32_MSB: function parseSInt32_LSB(buffer) {
return buffer.readInt32BE(0);
},
sint32_LSB_MSB: function parseSInt32_LSB(buffer) {
return this.sint32_LSB(buffer.slice(0, 4));
},
/* Date/time */
decDatetime: function parseDecDatetime(buffer) {
let year = parseInt(this.strD(buffer.slice(0, 4)));
let month = parseInt(this.strD(buffer.slice(4, 6))) - 1; // "Note that like moment(Array) and new Date(year, month, date), months are 0 indexed."
let day = parseInt(this.strD(buffer.slice(6, 8)));
let hour = parseInt(this.strD(buffer.slice(8, 10)));
let minute = parseInt(this.strD(buffer.slice(10, 12)));
let second = parseInt(this.strD(buffer.slice(12, 14)));
let centisecond = parseInt(this.strD(buffer.slice(14, 16)));
let timezoneOffset = this.int8(buffer.slice(16, 17));
if (year === 0 && month === 0 && day === 0 && hour === 0 && minute === 0 && second === 0 && centisecond === 0 && timezoneOffset === 0) {
return null;
} else {
return moment({
year, month, day, hour, minute, second,
millisecond: centisecond * 10,
}).utcOffset(offsetInMinutes(timezoneOffset));
}
},
directoryDatetime: function parseDirectoryDatetime(buffer) {
return moment({
year: 1900 + this.int8(buffer.slice(0, 0 + 1)),
month: this.int8(buffer.slice(1, 1 + 1)) - 1,
day: this.int8(buffer.slice(2, 2 + 1)),
hour: this.int8(buffer.slice(3, 3 + 1)),
minute: this.int8(buffer.slice(4, 4 + 1)),
second: this.int8(buffer.slice(5, 5 + 1)),
}).utcOffset(offsetInMinutes(this.int8(buffer.slice(6, 6 + 1))));
}
};

@ -0,0 +1,5 @@
'use strict';
module.exports = function removeRightPadding(string) {
return string.replace(/[ ]+$/, "");
};

@ -0,0 +1,5 @@
'use strict';
module.exports = function roundEven(number) {
return Math.ceil((number) / 2) * 2;
};

@ -0,0 +1,17 @@
'use strict';
const fs = require("fs");
const debug = require("debug")("seekable:fs");
module.exports = function getHttpSeekable(targetPath) {
return {
createReadStream: function createReadStream(start, end) {
debug(`${start}-${end} -- ${targetPath}`);
return fs.createReadStream(targetPath, {
start: start,
end: end
});
}
}
};

@ -0,0 +1,34 @@
'use strict';
const Promise = require("bluebird");
const bhttp = require("bhttp");
const debug = require("debug")("seekable:http");
function generateRangeHeader(start, end) {
if (start != null && end != null) {
return `bytes=${start}-${end}`;
} else if (start != null && end == null) {
return `bytes=${start}-`;
} else if (start == null && end != null) {
return `bytes=-${end}`;
} else {
return null;
}
}
module.exports = function getHttpSeekable(targetUrl) {
return {
createReadStream: function createReadStream(start, end) {
return Promise.try(() => {
debug(`${start}-${end} -- ${targetUrl}`);
return bhttp.get(targetUrl, {
stream: true,
headers: {
"range": generateRangeHeader(start, end) // FIXME: What if there is no range specified at all? Is `null` valid then?
}
});
});
}
}
};

@ -0,0 +1,29 @@
{
"name": "iso-spider",
"version": "1.0.0",
"description": "",
"main": "index.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@git.cryto.net:joepie91/iso-spider.git"
},
"author": "",
"license": "ISC",
"dependencies": {
"archy": "^1.0.0",
"bhttp": "^1.2.4",
"bluebird": "^3.4.6",
"debug": "^2.2.0",
"memoizee": "^0.4.1",
"moment-timezone": "^0.5.6",
"promise-while-loop": "^1.0.1",
"safe-buffer": "^5.0.1",
"stream-to-promise": "^2.2.0"
}
}
Loading…
Cancel
Save