xo-vmdk-to-vhd
Version:
JS lib reading and writing .vmdk and .ova files
303 lines (302 loc) • 11 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ensureArray = exports.ParsableFile = void 0;
exports.parseOVAFile = parseOVAFile;
exports.parseOVF = parseOVF;
var _assert = _interopRequireDefault(require("assert"));
var _find = _interopRequireDefault(require("lodash/find"));
var _forEach = _interopRequireDefault(require("lodash/forEach"));
var _pako = _interopRequireDefault(require("pako"));
var _sum = _interopRequireDefault(require("lodash/sum"));
var _xml2js = _interopRequireWildcard(require("xml2js"));
var _ = require(".");
var _util = require("./util");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const MEMORY_UNIT_TO_FACTOR = {
k: 1024,
m: 1048576,
g: 1073741824,
t: 1099511627776
};
const RESOURCE_TYPE_TO_HANDLER = {
3: (data, {
VirtualQuantity: nCpus
}) => {
data.nCpus = +nCpus;
},
4: (data, {
AllocationUnits: unit,
VirtualQuantity: quantity
}) => {
data.memory = quantity * allocationUnitsToFactor(unit);
},
10: ({
networks
}, {
AutomaticAllocation: enabled,
Connection: name
}) => {
if (enabled) {
networks.push(name);
}
},
17: ({
disks
}, {
Address: address,
AddressOnParent: position,
Description: description = 'No description',
ElementName: name,
Caption: caption = name,
HostResource: resource
}) => {
const diskId = resource.match(/^(?:ovf:)?\/disk\/(.+)$/);
const disk = diskId && disks[diskId[1]];
if (position === undefined && address !== undefined) {
let parsed = address.replace(/\s+/g, '');
if (parsed[0] === '{' && parsed[parsed.length - 1] === '}') {
parsed = parsed.substring(1, parsed.length - 1);
parsed = parsed.split(',');
parsed = Object.fromEntries(parsed.map(couple => couple.split('=')));
if ('target' in parsed) {
position = +parsed.target;
}
}
}
if (position === undefined) {
position = 0;
}
if (disk) {
disk.descriptionLabel = description;
disk.nameLabel = caption;
disk.position = +position;
} else {
console.error(`No disk found: '${diskId}'.`);
}
}
};
function parseTarHeader(header, stringDeserializer) {
const fileName = stringDeserializer(header.slice(0, 100), 'ascii').split('\0')[0];
if (fileName.length === 0) {
return null;
}
const sizeBuffer = header.slice(124, 124 + 12);
let fileSize = 0;
if (new Uint8Array(sizeBuffer)[0] === 128) {
for (const byte of new Uint8Array(sizeBuffer.slice(1))) {
fileSize *= 256;
fileSize += byte;
}
} else {
fileSize = parseInt(stringDeserializer(sizeBuffer.slice(0, 11), 'ascii'), 8);
}
return {
fileName,
fileSize
};
}
class ParsableFile {
slice(start, end) {}
async read() {}
}
exports.ParsableFile = ParsableFile;
const ensureArray = value => {
if (value === undefined) {
return [];
}
return Array.isArray(value) ? value : [value];
};
exports.ensureArray = ensureArray;
const allocationUnitsToFactor = unit => {
const intValue = unit.match(/\^([0-9]+)$/);
return intValue != null ? Math.pow(2, intValue[1]) : MEMORY_UNIT_TO_FACTOR[unit.charAt(0).toLowerCase()];
};
const cleanDisks = disks => {
const usedPositions = new Set();
let nextPosition = Object.keys(disks).length;
for (const diskId in disks) {
let position = disks[diskId].position;
if (position == null) {
console.error(`No position specified for '${diskId}'.`);
delete disks[diskId];
} else {
if (usedPositions.has(position)) {
console.warn(`There is at least two disks with position ${position}, we're changing the second one to ${nextPosition}`);
disks[diskId].position = position = nextPosition;
nextPosition++;
}
usedPositions.add(position);
}
}
};
async function parseOVF(fileFragment, stringDeserializer) {
const xmlString = stringDeserializer(await fileFragment.read(), 'utf-8');
return new Promise((resolve, reject) => _xml2js.default.parseString(xmlString, {
mergeAttrs: true,
explicitArray: false,
tagNameProcessors: [_xml2js.processors.stripPrefix],
attrNameProcessors: [_xml2js.processors.stripPrefix]
}, (err, res) => {
if (err) {
reject(err);
return;
}
const {
Envelope: {
DiskSection: {
Disk: disks
},
References: {
File: files
},
VirtualSystem: system
}
} = res;
const data = {
disks: {},
networks: []
};
const hardware = system.VirtualHardwareSection;
data.nameLabel = hardware.System.VirtualSystemIdentifier;
data.descriptionLabel = system.AnnotationSection && system.AnnotationSection.Annotation || system.OperatingSystemSection && system.OperatingSystemSection.Description;
(0, _forEach.default)(ensureArray(disks), disk => {
const file = (0, _find.default)(ensureArray(files), file => file.id === disk.fileRef);
const unit = disk.capacityAllocationUnits;
data.disks[disk.diskId] = {
capacity: disk.capacity * (unit && allocationUnitsToFactor(unit) || 1),
path: file && file.href,
compression: file && file.compression
};
});
const handleItem = item => {
const handler = RESOURCE_TYPE_TO_HANDLER[item.ResourceType];
if (!handler) {
return;
}
handler(data, item);
};
(0, _forEach.default)(ensureArray(hardware.Item), handleItem);
(0, _forEach.default)(ensureArray(hardware.StorageItem), handleItem);
(0, _forEach.default)(ensureArray(hardware.EthernetPortItem), handleItem);
cleanDisks(data.disks);
resolve(data);
}));
}
const GZIP_CHUNK_SIZE = 4 * 1024 * 1024;
async function parseGzipFromEnd(start, end, fileSlice, header) {
const l = end - start;
const chunks = [];
let savedSize = 0;
let currentDeflatedPos = 0;
const inflate = new _pako.default.Inflate();
while (currentDeflatedPos < header.fileSize) {
const slice = fileSlice.slice(currentDeflatedPos, currentDeflatedPos + GZIP_CHUNK_SIZE);
const compressed = await slice.read();
inflate.push(compressed, _pako.default.Z_SYNC_FLUSH);
const chunk = inflate.result.slice();
chunks.push({
pos: currentDeflatedPos,
buffer: chunk
});
savedSize += chunk.length;
if (savedSize - chunks[0].buffer.length >= l) {
savedSize -= chunks[0].buffer.length;
chunks.shift();
}
currentDeflatedPos += GZIP_CHUNK_SIZE;
}
let resultBuffer = new Uint8Array((0, _sum.default)(chunks.map(c => c.buffer.length)));
let index = 0;
chunks.forEach(c => {
resultBuffer.set(c.buffer, index);
index += c.buffer.length;
});
resultBuffer = resultBuffer.slice(start, end);
return resultBuffer.buffer;
}
async function parseOVAFile(parsableFile, stringDeserializer, skipVmdk = false) {
let offset = 0;
const HEADER_SIZE = 512;
let data = {
tables: {}
};
while (true) {
const header = parseTarHeader(await parsableFile.slice(offset, offset + HEADER_SIZE).read(), stringDeserializer);
offset += HEADER_SIZE;
if (header === null) {
break;
}
const fileSlice = parsableFile.slice(offset, offset + header.fileSize);
fileSlice.fileName = header.fileName;
if (!(header.fileName.startsWith('PaxHeader/') || header.fileName.startsWith('.'))) {
if (header.fileName.toLowerCase().endsWith('.ovf')) {
const res = await parseOVF(fileSlice, stringDeserializer);
data = {
...data,
...res
};
}
if (!skipVmdk && header.fileName.toLowerCase().endsWith('.vmdk')) {
const readFile = async (start, end) => fileSlice.slice(start, end).read();
readFile.fileName = header.fileName;
data.tables[header.fileName] = (0, _util.suppressUnhandledRejection)((0, _.readVmdkGrainTable)(readFile));
}
}
if (!skipVmdk && header.fileName.toLowerCase().endsWith('.vmdk.gz')) {
let forwardsInflater = new _pako.default.Inflate();
const readFile = async (start, end) => {
async function parseGzipFromStart(start, end, fileSlice) {
const chunks = [];
const resultStart = () => forwardsInflater.strm.total_out - forwardsInflater.result.length;
if (forwardsInflater.result != null && start < resultStart()) {
forwardsInflater = new _pako.default.Inflate();
}
let isLast = false;
while (true) {
if (forwardsInflater.strm.total_out > start) {
let chunk = forwardsInflater.result;
if (resultStart() < start) {
chunk = chunk.slice(start - resultStart());
}
if (forwardsInflater.strm.total_out > end) {
chunk = chunk.slice(0, -(forwardsInflater.strm.total_out - end));
isLast = true;
}
chunks.push(chunk);
}
if (isLast) {
break;
}
const slice = fileSlice.slice(forwardsInflater.strm.total_in, forwardsInflater.strm.total_in + GZIP_CHUNK_SIZE);
forwardsInflater.push(await slice.read(), _pako.default.Z_SYNC_FLUSH);
}
const resultBuffer = new Uint8Array((0, _sum.default)(chunks.map(c => c.length)));
let index = 0;
chunks.forEach(c => {
resultBuffer.set(c, index);
index += c.length;
});
_assert.default.strictEqual(resultBuffer.buffer.byteLength, end - start);
return resultBuffer.buffer;
}
if (start === end) {
return new Uint8Array(0);
}
if (start >= 0 && end >= 0) {
return parseGzipFromStart(start, end, fileSlice);
} else if (start < 0 && end < 0) {
return parseGzipFromEnd(start, end, fileSlice, header);
}
};
readFile.fileName = header.fileName;
data.tables[header.fileName] = (0, _util.suppressUnhandledRejection)((0, _.readVmdkGrainTable)(readFile));
}
offset += Math.ceil(header.fileSize / 512) * 512;
}
return data;
}
//# sourceMappingURL=ova-read.js.map
;