UNPKG

@nraynaud/xo-vhdmount

Version:

Node JS utility to expose the content of a VHD file as a raw disk file with FUSE

915 lines (699 loc) 29.9 kB
'use strict'; var _promise = require('babel-runtime/core-js/promise'); var _promise2 = _interopRequireDefault(_promise); var _regenerator = require('babel-runtime/regenerator'); var _regenerator2 = _interopRequireDefault(_regenerator); var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator'); var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); var _createClass2 = require('babel-runtime/helpers/createClass'); var _createClass3 = _interopRequireDefault(_createClass2); var _typeof2 = require('babel-runtime/helpers/typeof'); var _typeof3 = _interopRequireDefault(_typeof2); var _structFu = require('@nraynaud/struct-fu'); var _structFu2 = _interopRequireDefault(_structFu); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var VHD_UTIL_DEBUG = 0; var debug = VHD_UTIL_DEBUG ? function (str) { return console.log('[vhd-util]' + str); } : function () {}; // =================================================================== // // Spec: // https://www.microsoft.com/en-us/download/details.aspx?id=23850 // // C implementation: // https://github.com/rubiojr/vhd-util-convert // // =================================================================== // Sizes in bytes. var VHD_FOOTER_SIZE = 512; var VHD_HEADER_SIZE = 1024; var VHD_SECTOR_SIZE = 512; // Block allocation table entry size. (Block addr) var VHD_ENTRY_SIZE = 4; var VHD_PARENT_LOCATOR_ENTRIES = 8; var VHD_PLATFORM_CODE_NONE = 0; // Types of backup treated. Others are not supported. var HARD_DISK_TYPE_DYNAMIC = 3; // Full backup. var HARD_DISK_TYPE_DIFFERENCING = 4; // Delta backup. // Other. var BLOCK_UNUSED = 0xFFFFFFFF; var BIT_MASK = 0x80; // =================================================================== var fuFooter = _structFu2.default.struct([_structFu2.default.char('cookie', 8), // 0 _structFu2.default.uint32('features'), // 8 _structFu2.default.uint32('fileFormatVersion'), // 12 _structFu2.default.struct('dataOffset', [_structFu2.default.uint32('high'), // 16 _structFu2.default.uint32('low') // 20 ]), _structFu2.default.uint32('timestamp'), // 24 _structFu2.default.char('creatorApplication', 4), // 28 _structFu2.default.uint32('creatorVersion'), // 32 _structFu2.default.uint32('creatorHostOs'), // 36 _structFu2.default.struct('originalSize', [// At the creation, current size of the hard disk. _structFu2.default.uint32('high'), // 40 _structFu2.default.uint32('low') // 44 ]), _structFu2.default.struct('currentSize', [// Current size of the virtual disk. At the creation: currentSize = originalSize. _structFu2.default.uint32('high'), // 48 _structFu2.default.uint32('low') // 52 ]), _structFu2.default.struct('diskGeometry', [_structFu2.default.uint16('cylinders'), // 56 _structFu2.default.uint8('heads'), // 58 _structFu2.default.uint8('sectorsPerTrackCylinder') // 59 ]), _structFu2.default.uint32('diskType'), // 60 Disk type, must be equal to HARD_DISK_TYPE_DYNAMIC/HARD_DISK_TYPE_DIFFERENCING. _structFu2.default.uint32('checksum'), // 64 _structFu2.default.uint8('uuid', 16), // 68 _structFu2.default.char('saved'), // 84 _structFu2.default.char('hidden'), // 85 _structFu2.default.char('reserved', 426) // 86 ]); var fuHeader = _structFu2.default.struct([_structFu2.default.char('cookie', 8), _structFu2.default.struct('dataOffset', [_structFu2.default.uint32('high'), _structFu2.default.uint32('low')]), _structFu2.default.struct('tableOffset', [// Absolute byte offset of the Block Allocation Table. _structFu2.default.uint32('high'), _structFu2.default.uint32('low')]), _structFu2.default.uint32('headerVersion'), _structFu2.default.uint32('maxTableEntries'), // Max entries in the Block Allocation Table. _structFu2.default.uint32('blockSize'), // Block size in bytes. Default (2097152 => 2MB) _structFu2.default.uint32('checksum'), _structFu2.default.uint8('parentUuid', 16), _structFu2.default.uint32('parentTimestamp'), _structFu2.default.uint32('reserved1'), _structFu2.default.char16be('parentUnicodeName', 512), _structFu2.default.struct('parentLocatorEntry', [_structFu2.default.uint32('platformCode'), _structFu2.default.uint32('platformDataSpace'), _structFu2.default.uint32('platformDataLength'), _structFu2.default.uint32('reserved'), _structFu2.default.struct('platformDataOffset', [// Absolute byte offset of the locator data. _structFu2.default.uint32('high'), _structFu2.default.uint32('low')])], VHD_PARENT_LOCATOR_ENTRIES), _structFu2.default.char('reserved2', 256)]); // =================================================================== // Helpers // =================================================================== var SIZE_OF_32_BITS = Math.pow(2, 32); var uint32ToUint64 = function uint32ToUint64(fu) { return fu.high * SIZE_OF_32_BITS + fu.low; }; // Returns a 32 bits integer corresponding to a Vhd version. var getVhdVersion = function getVhdVersion(major, minor) { return major << 16 | minor & 0x0000FFFF; }; // Sectors conversions. var sectorsRoundUp = function sectorsRoundUp(bytes) { return Math.floor((bytes + VHD_SECTOR_SIZE - 1) / VHD_SECTOR_SIZE); }; var sectorsRoundUpNoZero = function sectorsRoundUpNoZero(bytes) { return sectorsRoundUp(bytes) || 1; }; var sectorsToBytes = function sectorsToBytes(sectors) { return sectors * VHD_SECTOR_SIZE; }; // Check/Set a bit on a vhd map. var mapTestBit = function mapTestBit(map, bit) { return (map[bit >> 3] << (bit & 7) & BIT_MASK) !== 0; }; var mapSetBit = function mapSetBit(map, bit) { map[bit >> 3] |= BIT_MASK >> (bit & 7); }; var packField = function packField(field, value, buf) { var offset = field.offset; field.pack(value, buf, (typeof offset === 'undefined' ? 'undefined' : (0, _typeof3.default)(offset)) !== 'object' ? { bytes: offset, bits: 0 } : offset); }; var unpackField = function unpackField(field, buf) { var offset = field.offset; return field.unpack(buf, (typeof offset === 'undefined' ? 'undefined' : (0, _typeof3.default)(offset)) !== 'object' ? { bytes: offset, bits: 0 } : offset); }; // =================================================================== // Returns the checksum of a raw struct. // The raw struct (footer or header) is altered with the new sum. function checksumStruct(rawStruct, checksumField) { var sum = 0; // Reset current sum. packField(checksumField, 0, rawStruct); for (var i = 0; i < VHD_FOOTER_SIZE; i++) { sum = sum + rawStruct[i] & 0xFFFFFFFF; } sum = 0xFFFFFFFF - sum; // Write new sum. packField(checksumField, sum, rawStruct); return sum; } function getParentLocatorSize(parentLocatorEntry) { var platformDataSpace = parentLocatorEntry.platformDataSpace; if (platformDataSpace < VHD_SECTOR_SIZE) { return sectorsToBytes(platformDataSpace); } return platformDataSpace % VHD_SECTOR_SIZE === 0 ? platformDataSpace : 0; } // =================================================================== var Vhd = function () { function Vhd(handler, path) { (0, _classCallCheck3.default)(this, Vhd); this._handler = handler; this._path = path; } // ================================================================= // Read functions. // ================================================================= // Returns the first address after metadata. (In bytes) (0, _createClass3.default)(Vhd, [{ key: 'getEndOfHeaders', value: function getEndOfHeaders() { var header = this.header; var end = uint32ToUint64(this.footer.dataOffset) + VHD_HEADER_SIZE; var blockAllocationTableSize = sectorsToBytes(sectorsRoundUpNoZero(header.maxTableEntries * VHD_ENTRY_SIZE)); // Max(end, block allocation table end) end = Math.max(end, uint32ToUint64(header.tableOffset) + blockAllocationTableSize); for (var i = 0; i < VHD_PARENT_LOCATOR_ENTRIES; i++) { var entry = header.parentLocatorEntry[i]; if (entry.platformCode !== VHD_PLATFORM_CODE_NONE) { var dataOffset = uint32ToUint64(entry.platformDataOffset); // Max(end, locator end) end = Math.max(end, dataOffset + getParentLocatorSize(entry)); } } debug('End of headers: ' + end + '.'); return end; } // Returns the first sector after data. }, { key: 'getEndOfData', value: function getEndOfData() { var end = Math.floor(this.getEndOfHeaders() / VHD_SECTOR_SIZE); var maxTableEntries = this.header.maxTableEntries; for (var i = 0; i < maxTableEntries; i++) { var blockAddr = this.readAllocationTableEntry(i); if (blockAddr !== BLOCK_UNUSED) { // Compute next block address. blockAddr += this.sectorsPerBlock + this.sectorsOfBitmap; end = Math.max(end, blockAddr); } } debug('End of data: ' + end + '.'); return sectorsToBytes(end); } // Returns the start position of the vhd footer. // The real footer, not the copy at the beginning of the vhd file. }, { key: 'getFooterStart', value: function () { var _ref = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee() { var stats; return _regenerator2.default.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return this._handler.getSize(this._path); case 2: stats = _context.sent; return _context.abrupt('return', stats.size - VHD_FOOTER_SIZE); case 4: case 'end': return _context.stop(); } } }, _callee, this); })); function getFooterStart() { return _ref.apply(this, arguments); } return getFooterStart; }() // Get the beginning (footer + header) of a vhd file. }, { key: 'readHeaderAndFooter', value: function () { var _ref2 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee2() { var buf, sum, sumToTest, header, sectorsPerBlock, sectorsOfBitmap; return _regenerator2.default.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: _context2.next = 2; return this._handler.createReadStream(this._path, { start: 0, end: VHD_FOOTER_SIZE + VHD_HEADER_SIZE - 1 }); case 2: _context2.t0 = _context2.sent; _context2.next = 5; return streamToBuffer(_context2.t0); case 5: buf = _context2.sent; sum = unpackField(fuFooter.fields.checksum, buf); sumToTest = checksumStruct(buf, fuFooter.fields.checksum); // Checksum child & parent. if (!(sumToTest !== sum)) { _context2.next = 10; break; } throw new Error('Bad checksum in vhd. Expected: ' + sum + '. Given: ' + sumToTest + '. (data=' + buf.toString('hex') + ')'); case 10: header = this.header = fuHeader.unpack(buf.slice(VHD_FOOTER_SIZE)); this.footer = fuFooter.unpack(buf); // Compute the number of sectors in one block. // Default: One block contains 4096 sectors of 512 bytes. sectorsPerBlock = this.sectorsPerBlock = Math.floor(header.blockSize / VHD_SECTOR_SIZE); // Compute bitmap size in sectors. // Default: 1. sectorsOfBitmap = this.sectorsOfBitmap = sectorsRoundUpNoZero(sectorsPerBlock >> 3); // Full block size => data block size + bitmap size. this.fullBlockSize = sectorsToBytes(sectorsPerBlock + sectorsOfBitmap); // In bytes. // Default: 512. this.bitmapSize = sectorsToBytes(sectorsOfBitmap); case 16: case 'end': return _context2.stop(); } } }, _callee2, this); })); function readHeaderAndFooter() { return _ref2.apply(this, arguments); } return readHeaderAndFooter; }() // Check if a vhd object has a block allocation table. }, { key: 'hasBlockAllocationTableMap', value: function hasBlockAllocationTableMap() { return this.footer.fileFormatVersion > getVhdVersion(1, 0); } // Returns a buffer that contains the block allocation table of a vhd file. }, { key: 'readBlockTable', value: function () { var _ref3 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee3() { var header, offset, size; return _regenerator2.default.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: header = this.header; offset = uint32ToUint64(header.tableOffset); size = sectorsToBytes(sectorsRoundUpNoZero(header.maxTableEntries * VHD_ENTRY_SIZE)); _context3.next = 5; return this._handler.createReadStream(this._path, { start: offset, end: offset + size - 1 }); case 5: _context3.t0 = _context3.sent; _context3.next = 8; return streamToBuffer(_context3.t0); case 8: this.blockTable = _context3.sent; case 9: case 'end': return _context3.stop(); } } }, _callee3, this); })); function readBlockTable() { return _ref3.apply(this, arguments); } return readBlockTable; }() // Returns the address block at the entry location of one table. }, { key: 'readAllocationTableEntry', value: function readAllocationTableEntry(entry) { return this.blockTable.readUInt32BE(entry * VHD_ENTRY_SIZE); } // Returns the data content of a block. (Not the bitmap !) }, { key: 'readBlockData', value: function () { var _ref4 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee4(blockAddr) { var blockSize, handler, path, blockDataAddr, footerStart, isPadded, size, buf; return _regenerator2.default.wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: blockSize = this.header.blockSize; handler = this._handler; path = this._path; blockDataAddr = sectorsToBytes(blockAddr + this.sectorsOfBitmap); _context4.next = 6; return this.getFooterStart(); case 6: footerStart = _context4.sent; isPadded = footerStart < blockDataAddr + blockSize; // Size ot the current block in the vhd file. size = isPadded ? footerStart - blockDataAddr : sectorsToBytes(this.sectorsPerBlock); debug('Read block data at: ' + blockDataAddr + '. (size=' + size + ')'); _context4.next = 12; return handler.createReadStream(path, { start: blockDataAddr, end: blockDataAddr + size - 1 }); case 12: _context4.t0 = _context4.sent; _context4.next = 15; return streamToBuffer(_context4.t0); case 15: buf = _context4.sent; if (!isPadded) { _context4.next = 18; break; } return _context4.abrupt('return', Buffer.concat([buf, new Buffer(blockSize - size).fill(0)])); case 18: return _context4.abrupt('return', buf); case 19: case 'end': return _context4.stop(); } } }, _callee4, this); })); function readBlockData(_x) { return _ref4.apply(this, arguments); } return readBlockData; }() // Returns a buffer that contains the bitmap of a block. // // TODO: merge with readBlockData(). }, { key: 'readBlockBitmap', value: function () { var _ref5 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee5(blockAddr) { var bitmapSize, offset; return _regenerator2.default.wrap(function _callee5$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: bitmapSize = this.bitmapSize; offset = sectorsToBytes(blockAddr); debug('Read bitmap at: ' + offset + '. (size=' + bitmapSize + ')'); _context5.next = 5; return this._handler.createReadStream(this._path, { start: offset, end: offset + bitmapSize - 1 }); case 5: _context5.t0 = _context5.sent; return _context5.abrupt('return', streamToBuffer(_context5.t0)); case 7: case 'end': return _context5.stop(); } } }, _callee5, this); })); function readBlockBitmap(_x2) { return _ref5.apply(this, arguments); } return readBlockBitmap; }() // ================================================================= // Write functions. // ================================================================= // Write a buffer at a given position in a vhd file. }, { key: '_write', value: function () { var _ref6 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee6(buffer, offset) { return _regenerator2.default.wrap(function _callee6$(_context6) { while (1) { switch (_context6.prev = _context6.next) { case 0: return _context6.abrupt('return', this._handler.createOutputStream(this._path, { start: offset, flags: 'r+' }).then(function (stream) { return new _promise2.default(function (resolve, reject) { stream.on('error', reject); stream.write(buffer, function () { stream.end(); resolve(); }); }); })); case 1: case 'end': return _context6.stop(); } } }, _callee6, this); })); function _write(_x3, _x4) { return _ref6.apply(this, arguments); } return _write; }() // Write an entry in the allocation table. }, { key: 'writeAllocationTableEntry', value: function writeAllocationTableEntry(entry, value) { this.blockTable.writeUInt32BE(value, entry * VHD_ENTRY_SIZE); } // Make a new empty block at vhd end. // Update block allocation table in context and in file. }, { key: 'createBlock', value: function () { var _ref7 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee7(blockId) { var offset, blockAddr, blockTable, fullBlockSize, tableOffset, entry; return _regenerator2.default.wrap(function _callee7$(_context7) { while (1) { switch (_context7.prev = _context7.next) { case 0: // End of file ! offset = this.getEndOfData(); // Padded on bound sector. if (offset % VHD_SECTOR_SIZE) { offset += VHD_SECTOR_SIZE - offset % VHD_SECTOR_SIZE; } blockAddr = Math.floor(offset / VHD_SECTOR_SIZE); blockTable = this.blockTable; fullBlockSize = this.fullBlockSize; debug('Create block at ' + blockAddr + '. (size=' + fullBlockSize + ', offset=' + offset + ')'); // New entry in block allocation table. this.writeAllocationTableEntry(blockId, blockAddr); tableOffset = uint32ToUint64(this.header.tableOffset); entry = blockId * VHD_ENTRY_SIZE; // Write an empty block and addr in vhd file. _context7.next = 11; return this._write(new Buffer(fullBlockSize).fill(0), offset); case 11: _context7.next = 13; return this._write(blockTable.slice(entry, entry + VHD_ENTRY_SIZE), tableOffset + entry); case 13: return _context7.abrupt('return', blockAddr); case 14: case 'end': return _context7.stop(); } } }, _callee7, this); })); function createBlock(_x5) { return _ref7.apply(this, arguments); } return createBlock; }() // Write a bitmap at a block address. }, { key: 'writeBlockBitmap', value: function () { var _ref8 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee8(blockAddr, bitmap) { var bitmapSize, offset; return _regenerator2.default.wrap(function _callee8$(_context8) { while (1) { switch (_context8.prev = _context8.next) { case 0: bitmapSize = this.bitmapSize; if (!(bitmap.length !== bitmapSize)) { _context8.next = 3; break; } throw new Error('Bitmap length is not correct ! ' + bitmap.length); case 3: offset = sectorsToBytes(blockAddr); debug('Write bitmap at: ' + offset + '. (size=' + bitmapSize + ', data=' + bitmap.toString('hex') + ')'); _context8.next = 7; return this._write(bitmap, sectorsToBytes(blockAddr)); case 7: case 'end': return _context8.stop(); } } }, _callee8, this); })); function writeBlockBitmap(_x6, _x7) { return _ref8.apply(this, arguments); } return writeBlockBitmap; }() }, { key: 'writeBlockSectors', value: function () { var _ref9 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee9(block, beginSectorId, n) { var blockAddr, endSectorId, offset, bitmap, i; return _regenerator2.default.wrap(function _callee9$(_context9) { while (1) { switch (_context9.prev = _context9.next) { case 0: blockAddr = this.readAllocationTableEntry(block.id); if (!(blockAddr === BLOCK_UNUSED)) { _context9.next = 5; break; } _context9.next = 4; return this.createBlock(block.id); case 4: blockAddr = _context9.sent; case 5: endSectorId = beginSectorId + n; offset = blockAddr + this.sectorsOfBitmap + beginSectorId; debug('Write block data at: ' + offset + '. (counter=' + n + ', blockId=' + block.id + ', blockSector=' + beginSectorId + ')'); _context9.next = 10; return this._write(block.data.slice(sectorsToBytes(beginSectorId), sectorsToBytes(endSectorId)), sectorsToBytes(offset)); case 10: _context9.next = 12; return this.readBlockBitmap(this.bitmapSize, blockAddr); case 12: bitmap = _context9.sent; for (i = beginSectorId; i < endSectorId; ++i) { mapSetBit(bitmap, i); } _context9.next = 16; return this.writeBlockBitmap(blockAddr, bitmap); case 16: case 'end': return _context9.stop(); } } }, _callee9, this); })); function writeBlockSectors(_x8, _x9, _x10) { return _ref9.apply(this, arguments); } return writeBlockSectors; }() // Merge block id (of vhd child) into vhd parent. }, { key: 'coalesceBlock', value: function () { var _ref10 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee10(child, blockAddr, blockId) { var blockData, blockBitmap, sectorsPerBlock, i, sectors; return _regenerator2.default.wrap(function _callee10$(_context10) { while (1) { switch (_context10.prev = _context10.next) { case 0: _context10.next = 2; return child.readBlockData(blockAddr); case 2: blockData = _context10.sent; _context10.next = 5; return child.readBlockBitmap(blockAddr); case 5: blockBitmap = _context10.sent; debug('Coalesce block ' + blockId + ' at ' + blockAddr + '.'); // For each sector of block data... sectorsPerBlock = child.sectorsPerBlock; i = 0; case 9: if (!(i < sectorsPerBlock)) { _context10.next = 26; break; } if (mapTestBit(blockBitmap, i)) { _context10.next = 12; break; } return _context10.abrupt('continue', 23); case 12: sectors = 0; // Count changed sectors. case 13: if (!(sectors + i < sectorsPerBlock)) { _context10.next = 19; break; } if (mapTestBit(blockBitmap, sectors + i)) { _context10.next = 16; break; } return _context10.abrupt('break', 19); case 16: sectors++; _context10.next = 13; break; case 19: // Write n sectors into parent. debug('Coalesce block: write. (offset=' + i + ', sectors=' + sectors + ')'); _context10.next = 22; return this.writeBlockSectors({ id: blockId, data: blockData }, i, sectors); case 22: i += sectors; case 23: i++; _context10.next = 9; break; case 26: case 'end': return _context10.stop(); } } }, _callee10, this); })); function coalesceBlock(_x11, _x12, _x13) { return _ref10.apply(this, arguments); } return coalesceBlock; }() // Write a context footer. (At the end and beginning of a vhd file.) }, { key: 'writeFooter', value: function () { var _ref11 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee11() { var footer, offset, rawFooter; return _regenerator2.default.wrap(function _callee11$(_context11) { while (1) { switch (_context11.prev = _context11.next) { case 0: footer = this.footer; offset = this.getEndOfData(); rawFooter = fuFooter.pack(footer); footer.checksum = checksumStruct(rawFooter, fuFooter.fields.checksum); debug('Write footer at: ' + offset + ' (checksum=' + footer.checksum + '). (data=' + rawFooter.toString('hex') + ')'); _context11.next = 7; return this._write(rawFooter, 0); case 7: _context11.next = 9; return this._write(rawFooter, offset); case 9: case 'end': return _context11.stop(); } } }, _callee11, this); })); function writeFooter() { return _ref11.apply(this, arguments); } return writeFooter; }() }, { key: 'writeHeader', value: function () { var _ref12 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee12() { var header, rawHeader, offset; return _regenerator2.default.wrap(function _callee12$(_context12) { while (1) { switch (_context12.prev = _context12.next) { case 0: header = this.header; rawHeader = fuHeader.pack(header); header.checksum = checksumStruct(rawHeader, fuHeader.fields.checksum); offset = VHD_FOOTER_SIZE; debug('Write header at: ' + offset + ' (checksum=' + header.checksum + '). (data=' + rawHeader.toString('hex') + ')'); _context12.next = 7; return this._write(rawHeader, offset); case 7: case 'end': return _context12.stop(); } } }, _callee12, this); })); function writeHeader() { return _ref12.apply(this, arguments); } return writeHeader; }() }]); return Vhd; }(); //# sourceMappingURL=vhd.js.map