chore(xo-server/vhd-merge): various updates (#2767)
Fixes #2746 - implement parent locators - tests - remove `@nraynaud/struct-fu`
This commit is contained in:
		
				
					committed by
					
						
						Julien Fontanet
					
				
			
			
				
	
			
			
			
						parent
						
							0b9d031965
						
					
				
				
					commit
					7e689076d8
				
			@@ -7,6 +7,11 @@ node_js:
 | 
				
			|||||||
# Use containers.
 | 
					# Use containers.
 | 
				
			||||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
 | 
					# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
 | 
				
			||||||
sudo: false
 | 
					sudo: false
 | 
				
			||||||
 | 
					addons:
 | 
				
			||||||
 | 
					  apt:
 | 
				
			||||||
 | 
					    packages:
 | 
				
			||||||
 | 
					    - qemu-utils
 | 
				
			||||||
 | 
					    - blktap-utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
before_install:
 | 
					before_install:
 | 
				
			||||||
  - curl -o- -L https://yarnpkg.com/install.sh | bash
 | 
					  - curl -o- -L https://yarnpkg.com/install.sh | bash
 | 
				
			||||||
@@ -14,3 +19,7 @@ before_install:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
cache:
 | 
					cache:
 | 
				
			||||||
  yarn: true
 | 
					  yarn: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					script:
 | 
				
			||||||
 | 
					  - yarn run test
 | 
				
			||||||
 | 
					  - yarn run test-integration
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,12 +52,13 @@
 | 
				
			|||||||
    "build": "scripts/run-script --parallel build",
 | 
					    "build": "scripts/run-script --parallel build",
 | 
				
			||||||
    "clean": "scripts/run-script --parallel clean",
 | 
					    "clean": "scripts/run-script --parallel clean",
 | 
				
			||||||
    "dev": "scripts/run-script --parallel dev",
 | 
					    "dev": "scripts/run-script --parallel dev",
 | 
				
			||||||
    "dev-test": "jest --bail --watch",
 | 
					    "dev-test": "jest --bail --watch \"^(?!.*\\.integ\\.spec\\.js$)\"",
 | 
				
			||||||
    "posttest": "scripts/run-script test",
 | 
					    "posttest": "scripts/run-script test",
 | 
				
			||||||
    "precommit": "scripts/lint-staged",
 | 
					    "precommit": "scripts/lint-staged",
 | 
				
			||||||
    "prepare": "scripts/run-script prepare",
 | 
					    "prepare": "scripts/run-script prepare",
 | 
				
			||||||
    "pretest": "eslint --ignore-path .gitignore .",
 | 
					    "pretest": "eslint --ignore-path .gitignore .",
 | 
				
			||||||
    "test": "jest"
 | 
					    "test": "jest \"^(?!.*\\.integ\\.spec\\.js$)\"",
 | 
				
			||||||
 | 
					    "test-integration": "jest \".integ\\.spec\\.js$\""
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "workspaces": [
 | 
					  "workspaces": [
 | 
				
			||||||
    "@xen-orchestra/*",
 | 
					    "@xen-orchestra/*",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,7 @@
 | 
				
			|||||||
    "node": ">=4"
 | 
					    "node": ">=4"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@nraynaud/struct-fu": "^1.0.1",
 | 
					    "struct-fu": "^1.2.0",
 | 
				
			||||||
    "@nraynaud/xo-fs": "^0.0.5",
 | 
					    "@nraynaud/xo-fs": "^0.0.5",
 | 
				
			||||||
    "babel-runtime": "^6.22.0",
 | 
					    "babel-runtime": "^6.22.0",
 | 
				
			||||||
    "exec-promise": "^0.7.0"
 | 
					    "exec-promise": "^0.7.0"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import assert from 'assert'
 | 
					import assert from 'assert'
 | 
				
			||||||
import fu from '@nraynaud/struct-fu'
 | 
					import fu from 'struct-fu'
 | 
				
			||||||
import { dirname } from 'path'
 | 
					import { dirname } from 'path'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ===================================================================
 | 
					// ===================================================================
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,7 +40,7 @@
 | 
				
			|||||||
    "dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
 | 
					    "dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
 | 
				
			||||||
    "prebuild": "rimraf dist/",
 | 
					    "prebuild": "rimraf dist/",
 | 
				
			||||||
    "predev": "yarn run prebuild",
 | 
					    "predev": "yarn run prebuild",
 | 
				
			||||||
    "prepublishOnly": "yarn run build"
 | 
					    "prepare": "yarn run build"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "babel": {
 | 
					  "babel": {
 | 
				
			||||||
    "plugins": [
 | 
					    "plugins": [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,7 +33,6 @@
 | 
				
			|||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@babel/polyfill": "7.0.0-beta.42",
 | 
					    "@babel/polyfill": "7.0.0-beta.42",
 | 
				
			||||||
    "@marsaud/smb2-promise": "^0.2.1",
 | 
					    "@marsaud/smb2-promise": "^0.2.1",
 | 
				
			||||||
    "@nraynaud/struct-fu": "^1.0.1",
 | 
					 | 
				
			||||||
    "@xen-orchestra/cron": "^1.0.2",
 | 
					    "@xen-orchestra/cron": "^1.0.2",
 | 
				
			||||||
    "ajv": "^6.1.1",
 | 
					    "ajv": "^6.1.1",
 | 
				
			||||||
    "app-conf": "^0.5.0",
 | 
					    "app-conf": "^0.5.0",
 | 
				
			||||||
@@ -104,6 +103,7 @@
 | 
				
			|||||||
    "split-lines": "^1.1.0",
 | 
					    "split-lines": "^1.1.0",
 | 
				
			||||||
    "stack-chain": "^2.0.0",
 | 
					    "stack-chain": "^2.0.0",
 | 
				
			||||||
    "stoppable": "^1.0.5",
 | 
					    "stoppable": "^1.0.5",
 | 
				
			||||||
 | 
					    "struct-fu": "^1.2.0",
 | 
				
			||||||
    "tar-stream": "^1.5.5",
 | 
					    "tar-stream": "^1.5.5",
 | 
				
			||||||
    "through2": "^2.0.3",
 | 
					    "through2": "^2.0.3",
 | 
				
			||||||
    "tmp": "^0.0.33",
 | 
					    "tmp": "^0.0.33",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										284
									
								
								packages/xo-server/src/vhd-merge.integ.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								packages/xo-server/src/vhd-merge.integ.spec.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,284 @@
 | 
				
			|||||||
 | 
					/* eslint-env jest */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import execa from 'execa'
 | 
				
			||||||
 | 
					import fs from 'fs-extra'
 | 
				
			||||||
 | 
					import rimraf from 'rimraf'
 | 
				
			||||||
 | 
					import { randomBytes } from 'crypto'
 | 
				
			||||||
 | 
					import { fromEvent } from 'promise-toolbox'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import LocalHandler from './remote-handlers/local'
 | 
				
			||||||
 | 
					import vhdMerge, {
 | 
				
			||||||
 | 
					  chainVhd,
 | 
				
			||||||
 | 
					  createReadStream,
 | 
				
			||||||
 | 
					  Vhd,
 | 
				
			||||||
 | 
					  VHD_SECTOR_SIZE,
 | 
				
			||||||
 | 
					} from './vhd-merge'
 | 
				
			||||||
 | 
					import { pFromCallback, streamToBuffer, tmpDir } from './utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const initialDir = process.cwd()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jest.setTimeout(10000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					beforeEach(async () => {
 | 
				
			||||||
 | 
					  const dir = await tmpDir()
 | 
				
			||||||
 | 
					  process.chdir(dir)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					afterEach(async () => {
 | 
				
			||||||
 | 
					  const tmpDir = process.cwd()
 | 
				
			||||||
 | 
					  process.chdir(initialDir)
 | 
				
			||||||
 | 
					  await pFromCallback(cb => rimraf(tmpDir, cb))
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function createRandomFile (name, sizeMb) {
 | 
				
			||||||
 | 
					  await execa('bash', [
 | 
				
			||||||
 | 
					    '-c',
 | 
				
			||||||
 | 
					    `< /dev/urandom tr -dc "\\t\\n [:alnum:]" | head -c ${sizeMb}M >${name}`,
 | 
				
			||||||
 | 
					  ])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function checkFile (vhdName) {
 | 
				
			||||||
 | 
					  await execa('vhd-util', ['check', '-p', '-b', '-t', '-n', vhdName])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function recoverRawContent (vhdName, rawName, originalSize) {
 | 
				
			||||||
 | 
					  await checkFile(vhdName)
 | 
				
			||||||
 | 
					  await execa('qemu-img', ['convert', '-fvpc', '-Oraw', vhdName, rawName])
 | 
				
			||||||
 | 
					  if (originalSize !== undefined) {
 | 
				
			||||||
 | 
					    await execa('truncate', ['-s', originalSize, rawName])
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function convertFromRawToVhd (rawName, vhdName) {
 | 
				
			||||||
 | 
					  await execa('qemu-img', ['convert', '-f', 'raw', '-Ovpc', rawName, vhdName])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('blocks can be moved', async () => {
 | 
				
			||||||
 | 
					  const initalSize = 4
 | 
				
			||||||
 | 
					  await createRandomFile('randomfile', initalSize)
 | 
				
			||||||
 | 
					  await convertFromRawToVhd('randomfile', 'randomfile.vhd')
 | 
				
			||||||
 | 
					  const handler = new LocalHandler({ url: 'file://' + process.cwd() })
 | 
				
			||||||
 | 
					  const originalSize = await handler.getSize('randomfile')
 | 
				
			||||||
 | 
					  const newVhd = new Vhd(handler, 'randomfile.vhd')
 | 
				
			||||||
 | 
					  await newVhd.readHeaderAndFooter()
 | 
				
			||||||
 | 
					  await newVhd.readBlockTable()
 | 
				
			||||||
 | 
					  await newVhd._freeFirstBlockSpace(8000000)
 | 
				
			||||||
 | 
					  await recoverRawContent('randomfile.vhd', 'recovered', originalSize)
 | 
				
			||||||
 | 
					  expect(await fs.readFile('recovered')).toEqual(
 | 
				
			||||||
 | 
					    await fs.readFile('randomfile')
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('the BAT MSB is not used for sign', async () => {
 | 
				
			||||||
 | 
					  const randomBuffer = await pFromCallback(cb =>
 | 
				
			||||||
 | 
					    randomBytes(VHD_SECTOR_SIZE, cb)
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  await execa('qemu-img', ['create', '-fvpc', 'empty.vhd', '1.8T'])
 | 
				
			||||||
 | 
					  const handler = new LocalHandler({ url: 'file://' + process.cwd() })
 | 
				
			||||||
 | 
					  const vhd = new Vhd(handler, 'empty.vhd')
 | 
				
			||||||
 | 
					  await vhd.readHeaderAndFooter()
 | 
				
			||||||
 | 
					  await vhd.readBlockTable()
 | 
				
			||||||
 | 
					  // we want the bit 31 to be on, to prove it's not been used for sign
 | 
				
			||||||
 | 
					  const hugeWritePositionSectors = Math.pow(2, 31) + 200
 | 
				
			||||||
 | 
					  await vhd.writeData(hugeWritePositionSectors, randomBuffer)
 | 
				
			||||||
 | 
					  await checkFile('empty.vhd')
 | 
				
			||||||
 | 
					  // here we are moving the first sector very far in the VHD to prove the BAT doesn't use signed int32
 | 
				
			||||||
 | 
					  const hugePositionBytes = hugeWritePositionSectors * VHD_SECTOR_SIZE
 | 
				
			||||||
 | 
					  await vhd._freeFirstBlockSpace(hugePositionBytes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // we recover the data manually for speed reasons.
 | 
				
			||||||
 | 
					  // fs.write() with offset is way faster than qemu-img when there is a 1.5To
 | 
				
			||||||
 | 
					  // hole before the block of data
 | 
				
			||||||
 | 
					  const recoveredFile = await fs.open('recovered', 'w')
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const vhd2 = new Vhd(handler, 'empty.vhd')
 | 
				
			||||||
 | 
					    await vhd2.readHeaderAndFooter()
 | 
				
			||||||
 | 
					    await vhd2.readBlockTable()
 | 
				
			||||||
 | 
					    for (let i = 0; i < vhd.header.maxTableEntries; i++) {
 | 
				
			||||||
 | 
					      const entry = vhd._getBatEntry(i)
 | 
				
			||||||
 | 
					      if (entry !== 0xffffffff) {
 | 
				
			||||||
 | 
					        const block = (await vhd2._readBlock(i)).data
 | 
				
			||||||
 | 
					        await fs.write(
 | 
				
			||||||
 | 
					          recoveredFile,
 | 
				
			||||||
 | 
					          block,
 | 
				
			||||||
 | 
					          0,
 | 
				
			||||||
 | 
					          block.length,
 | 
				
			||||||
 | 
					          vhd2.header.blockSize * i
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    fs.close(recoveredFile)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const recovered = await streamToBuffer(
 | 
				
			||||||
 | 
					    await fs.createReadStream('recovered', {
 | 
				
			||||||
 | 
					      start: hugePositionBytes,
 | 
				
			||||||
 | 
					      end: hugePositionBytes + randomBuffer.length - 1,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  expect(recovered).toEqual(randomBuffer)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('writeData on empty file', async () => {
 | 
				
			||||||
 | 
					  const mbOfRandom = 3
 | 
				
			||||||
 | 
					  await createRandomFile('randomfile', mbOfRandom)
 | 
				
			||||||
 | 
					  await execa('qemu-img', ['create', '-fvpc', 'empty.vhd', mbOfRandom + 'M'])
 | 
				
			||||||
 | 
					  const randomData = await fs.readFile('randomfile')
 | 
				
			||||||
 | 
					  const handler = new LocalHandler({ url: 'file://' + process.cwd() })
 | 
				
			||||||
 | 
					  const originalSize = await handler.getSize('randomfile')
 | 
				
			||||||
 | 
					  const newVhd = new Vhd(handler, 'empty.vhd')
 | 
				
			||||||
 | 
					  await newVhd.readHeaderAndFooter()
 | 
				
			||||||
 | 
					  await newVhd.readBlockTable()
 | 
				
			||||||
 | 
					  await newVhd.writeData(0, randomData)
 | 
				
			||||||
 | 
					  await recoverRawContent('empty.vhd', 'recovered', originalSize)
 | 
				
			||||||
 | 
					  expect(await fs.readFile('recovered')).toEqual(randomData)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('writeData in 2 non-overlaping operations', async () => {
 | 
				
			||||||
 | 
					  const mbOfRandom = 3
 | 
				
			||||||
 | 
					  await createRandomFile('randomfile', mbOfRandom)
 | 
				
			||||||
 | 
					  await execa('qemu-img', ['create', '-fvpc', 'empty.vhd', mbOfRandom + 'M'])
 | 
				
			||||||
 | 
					  const randomData = await fs.readFile('randomfile')
 | 
				
			||||||
 | 
					  const handler = new LocalHandler({ url: 'file://' + process.cwd() })
 | 
				
			||||||
 | 
					  const originalSize = await handler.getSize('randomfile')
 | 
				
			||||||
 | 
					  const newVhd = new Vhd(handler, 'empty.vhd')
 | 
				
			||||||
 | 
					  await newVhd.readHeaderAndFooter()
 | 
				
			||||||
 | 
					  await newVhd.readBlockTable()
 | 
				
			||||||
 | 
					  const splitPointSectors = 2
 | 
				
			||||||
 | 
					  await newVhd.writeData(0, randomData.slice(0, splitPointSectors * 512))
 | 
				
			||||||
 | 
					  await newVhd.writeData(
 | 
				
			||||||
 | 
					    splitPointSectors,
 | 
				
			||||||
 | 
					    randomData.slice(splitPointSectors * 512)
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  await recoverRawContent('empty.vhd', 'recovered', originalSize)
 | 
				
			||||||
 | 
					  expect(await fs.readFile('recovered')).toEqual(randomData)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('writeData in 2 overlaping operations', async () => {
 | 
				
			||||||
 | 
					  const mbOfRandom = 3
 | 
				
			||||||
 | 
					  await createRandomFile('randomfile', mbOfRandom)
 | 
				
			||||||
 | 
					  await execa('qemu-img', ['create', '-fvpc', 'empty.vhd', mbOfRandom + 'M'])
 | 
				
			||||||
 | 
					  const randomData = await fs.readFile('randomfile')
 | 
				
			||||||
 | 
					  const handler = new LocalHandler({ url: 'file://' + process.cwd() })
 | 
				
			||||||
 | 
					  const originalSize = await handler.getSize('randomfile')
 | 
				
			||||||
 | 
					  const newVhd = new Vhd(handler, 'empty.vhd')
 | 
				
			||||||
 | 
					  await newVhd.readHeaderAndFooter()
 | 
				
			||||||
 | 
					  await newVhd.readBlockTable()
 | 
				
			||||||
 | 
					  const endFirstWrite = 3
 | 
				
			||||||
 | 
					  const startSecondWrite = 2
 | 
				
			||||||
 | 
					  await newVhd.writeData(0, randomData.slice(0, endFirstWrite * 512))
 | 
				
			||||||
 | 
					  await newVhd.writeData(
 | 
				
			||||||
 | 
					    startSecondWrite,
 | 
				
			||||||
 | 
					    randomData.slice(startSecondWrite * 512)
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  await recoverRawContent('empty.vhd', 'recovered', originalSize)
 | 
				
			||||||
 | 
					  expect(await fs.readFile('recovered')).toEqual(randomData)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('BAT can be extended and blocks moved', async () => {
 | 
				
			||||||
 | 
					  const initalSize = 4
 | 
				
			||||||
 | 
					  await createRandomFile('randomfile', initalSize)
 | 
				
			||||||
 | 
					  await convertFromRawToVhd('randomfile', 'randomfile.vhd')
 | 
				
			||||||
 | 
					  const handler = new LocalHandler({ url: 'file://' + process.cwd() })
 | 
				
			||||||
 | 
					  const originalSize = await handler.getSize('randomfile')
 | 
				
			||||||
 | 
					  const newVhd = new Vhd(handler, 'randomfile.vhd')
 | 
				
			||||||
 | 
					  await newVhd.readHeaderAndFooter()
 | 
				
			||||||
 | 
					  await newVhd.readBlockTable()
 | 
				
			||||||
 | 
					  await newVhd.ensureBatSize(2000)
 | 
				
			||||||
 | 
					  await recoverRawContent('randomfile.vhd', 'recovered', originalSize)
 | 
				
			||||||
 | 
					  expect(await fs.readFile('recovered')).toEqual(
 | 
				
			||||||
 | 
					    await fs.readFile('randomfile')
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('coalesce works with empty parent files', async () => {
 | 
				
			||||||
 | 
					  const mbOfRandom = 2
 | 
				
			||||||
 | 
					  await createRandomFile('randomfile', mbOfRandom)
 | 
				
			||||||
 | 
					  await convertFromRawToVhd('randomfile', 'randomfile.vhd')
 | 
				
			||||||
 | 
					  await execa('qemu-img', [
 | 
				
			||||||
 | 
					    'create',
 | 
				
			||||||
 | 
					    '-fvpc',
 | 
				
			||||||
 | 
					    'empty.vhd',
 | 
				
			||||||
 | 
					    mbOfRandom + 1 + 'M',
 | 
				
			||||||
 | 
					  ])
 | 
				
			||||||
 | 
					  await checkFile('randomfile.vhd')
 | 
				
			||||||
 | 
					  await checkFile('empty.vhd')
 | 
				
			||||||
 | 
					  const handler = new LocalHandler({ url: 'file://' + process.cwd() })
 | 
				
			||||||
 | 
					  const originalSize = await handler._getSize('randomfile')
 | 
				
			||||||
 | 
					  await chainVhd(handler, 'empty.vhd', handler, 'randomfile.vhd', true)
 | 
				
			||||||
 | 
					  await checkFile('randomfile.vhd')
 | 
				
			||||||
 | 
					  await checkFile('empty.vhd')
 | 
				
			||||||
 | 
					  await vhdMerge(handler, 'empty.vhd', handler, 'randomfile.vhd')
 | 
				
			||||||
 | 
					  await recoverRawContent('empty.vhd', 'recovered', originalSize)
 | 
				
			||||||
 | 
					  expect(await fs.readFile('recovered')).toEqual(
 | 
				
			||||||
 | 
					    await fs.readFile('randomfile')
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('coalesce works in normal cases', async () => {
 | 
				
			||||||
 | 
					  const mbOfRandom = 5
 | 
				
			||||||
 | 
					  await createRandomFile('randomfile', mbOfRandom)
 | 
				
			||||||
 | 
					  await createRandomFile('small_randomfile', Math.ceil(mbOfRandom / 2))
 | 
				
			||||||
 | 
					  await execa('qemu-img', [
 | 
				
			||||||
 | 
					    'create',
 | 
				
			||||||
 | 
					    '-fvpc',
 | 
				
			||||||
 | 
					    'parent.vhd',
 | 
				
			||||||
 | 
					    mbOfRandom + 1 + 'M',
 | 
				
			||||||
 | 
					  ])
 | 
				
			||||||
 | 
					  await convertFromRawToVhd('randomfile', 'child1.vhd')
 | 
				
			||||||
 | 
					  const handler = new LocalHandler({ url: 'file://' + process.cwd() })
 | 
				
			||||||
 | 
					  await execa('vhd-util', ['snapshot', '-n', 'child2.vhd', '-p', 'child1.vhd'])
 | 
				
			||||||
 | 
					  const vhd = new Vhd(handler, 'child2.vhd')
 | 
				
			||||||
 | 
					  await vhd.readHeaderAndFooter()
 | 
				
			||||||
 | 
					  await vhd.readBlockTable()
 | 
				
			||||||
 | 
					  vhd.footer.creatorApplication = 'xoa'
 | 
				
			||||||
 | 
					  await vhd.writeFooter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const originalSize = await handler._getSize('randomfile')
 | 
				
			||||||
 | 
					  await chainVhd(handler, 'parent.vhd', handler, 'child1.vhd', true)
 | 
				
			||||||
 | 
					  await execa('vhd-util', ['check', '-t', '-n', 'child1.vhd'])
 | 
				
			||||||
 | 
					  await chainVhd(handler, 'child1.vhd', handler, 'child2.vhd', true)
 | 
				
			||||||
 | 
					  await execa('vhd-util', ['check', '-t', '-n', 'child2.vhd'])
 | 
				
			||||||
 | 
					  const smallRandom = await fs.readFile('small_randomfile')
 | 
				
			||||||
 | 
					  const newVhd = new Vhd(handler, 'child2.vhd')
 | 
				
			||||||
 | 
					  await newVhd.readHeaderAndFooter()
 | 
				
			||||||
 | 
					  await newVhd.readBlockTable()
 | 
				
			||||||
 | 
					  await newVhd.writeData(5, smallRandom)
 | 
				
			||||||
 | 
					  await checkFile('child2.vhd')
 | 
				
			||||||
 | 
					  await checkFile('child1.vhd')
 | 
				
			||||||
 | 
					  await checkFile('parent.vhd')
 | 
				
			||||||
 | 
					  await vhdMerge(handler, 'parent.vhd', handler, 'child1.vhd')
 | 
				
			||||||
 | 
					  await checkFile('parent.vhd')
 | 
				
			||||||
 | 
					  await chainVhd(handler, 'parent.vhd', handler, 'child2.vhd', true)
 | 
				
			||||||
 | 
					  await checkFile('child2.vhd')
 | 
				
			||||||
 | 
					  await vhdMerge(handler, 'parent.vhd', handler, 'child2.vhd')
 | 
				
			||||||
 | 
					  await checkFile('parent.vhd')
 | 
				
			||||||
 | 
					  await recoverRawContent(
 | 
				
			||||||
 | 
					    'parent.vhd',
 | 
				
			||||||
 | 
					    'recovered_from_coalescing',
 | 
				
			||||||
 | 
					    originalSize
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  await execa('cp', ['randomfile', 'randomfile2'])
 | 
				
			||||||
 | 
					  const fd = await fs.open('randomfile2', 'r+')
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    await fs.write(fd, smallRandom, 0, smallRandom.length, 5 * VHD_SECTOR_SIZE)
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    await fs.close(fd)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  expect(await fs.readFile('recovered_from_coalescing')).toEqual(
 | 
				
			||||||
 | 
					    await fs.readFile('randomfile2')
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('createReadStream passes vhd-util check', async () => {
 | 
				
			||||||
 | 
					  const initalSize = 4
 | 
				
			||||||
 | 
					  await createRandomFile('randomfile', initalSize)
 | 
				
			||||||
 | 
					  await convertFromRawToVhd('randomfile', 'randomfile.vhd')
 | 
				
			||||||
 | 
					  const handler = new LocalHandler({ url: 'file://' + process.cwd() })
 | 
				
			||||||
 | 
					  const stream = createReadStream(handler, 'randomfile.vhd')
 | 
				
			||||||
 | 
					  await fromEvent(
 | 
				
			||||||
 | 
					    stream.pipe(await fs.createWriteStream('recovered.vhd')),
 | 
				
			||||||
 | 
					    'finish'
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  await checkFile('recovered.vhd')
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
@@ -3,8 +3,7 @@
 | 
				
			|||||||
import assert from 'assert'
 | 
					import assert from 'assert'
 | 
				
			||||||
import asyncIteratorToStream from 'async-iterator-to-stream'
 | 
					import asyncIteratorToStream from 'async-iterator-to-stream'
 | 
				
			||||||
import concurrency from 'limit-concurrency-decorator'
 | 
					import concurrency from 'limit-concurrency-decorator'
 | 
				
			||||||
import fu from '@nraynaud/struct-fu'
 | 
					import fu from 'struct-fu'
 | 
				
			||||||
import isEqual from 'lodash/isEqual'
 | 
					 | 
				
			||||||
import { dirname, relative } from 'path'
 | 
					import { dirname, relative } from 'path'
 | 
				
			||||||
import { fromEvent } from 'promise-toolbox'
 | 
					import { fromEvent } from 'promise-toolbox'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -13,7 +12,7 @@ import constantStream from './constant-stream'
 | 
				
			|||||||
import { noop, resolveRelativeFromFile, streamToBuffer } from './utils'
 | 
					import { noop, resolveRelativeFromFile, streamToBuffer } from './utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const VHD_UTIL_DEBUG = 0
 | 
					const VHD_UTIL_DEBUG = 0
 | 
				
			||||||
const debug = VHD_UTIL_DEBUG ? str => console.log(`[vhd-util]${str}`) : noop
 | 
					const debug = VHD_UTIL_DEBUG ? str => console.log(`[vhd-merge]${str}`) : noop
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ===================================================================
 | 
					// ===================================================================
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
@@ -28,7 +27,7 @@ const debug = VHD_UTIL_DEBUG ? str => console.log(`[vhd-util]${str}`) : noop
 | 
				
			|||||||
// Sizes in bytes.
 | 
					// Sizes in bytes.
 | 
				
			||||||
const VHD_FOOTER_SIZE = 512
 | 
					const VHD_FOOTER_SIZE = 512
 | 
				
			||||||
const VHD_HEADER_SIZE = 1024
 | 
					const VHD_HEADER_SIZE = 1024
 | 
				
			||||||
const VHD_SECTOR_SIZE = 512
 | 
					export const VHD_SECTOR_SIZE = 512
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Block allocation table entry size. (Block addr)
 | 
					// Block allocation table entry size. (Block addr)
 | 
				
			||||||
const VHD_ENTRY_SIZE = 4
 | 
					const VHD_ENTRY_SIZE = 4
 | 
				
			||||||
@@ -40,6 +39,12 @@ const VHD_PLATFORM_CODE_NONE = 0
 | 
				
			|||||||
export const HARD_DISK_TYPE_DYNAMIC = 3 // Full backup.
 | 
					export const HARD_DISK_TYPE_DYNAMIC = 3 // Full backup.
 | 
				
			||||||
export const HARD_DISK_TYPE_DIFFERENCING = 4 // Delta backup.
 | 
					export const HARD_DISK_TYPE_DIFFERENCING = 4 // Delta backup.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PLATFORM_NONE = 0
 | 
				
			||||||
 | 
					export const PLATFORM_W2RU = 0x57327275
 | 
				
			||||||
 | 
					export const PLATFORM_W2KU = 0x57326b75
 | 
				
			||||||
 | 
					export const PLATFORM_MAC = 0x4d616320
 | 
				
			||||||
 | 
					export const PLATFORM_MACX = 0x4d616358
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Other.
 | 
					// Other.
 | 
				
			||||||
const BLOCK_UNUSED = 0xffffffff
 | 
					const BLOCK_UNUSED = 0xffffffff
 | 
				
			||||||
const BIT_MASK = 0x80
 | 
					const BIT_MASK = 0x80
 | 
				
			||||||
@@ -50,28 +55,24 @@ BUF_BLOCK_UNUSED.writeUInt32BE(BLOCK_UNUSED, 0)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// ===================================================================
 | 
					// ===================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SIZE_OF_32_BITS = Math.pow(2, 32)
 | 
				
			||||||
 | 
					const uint64 = fu.derive(
 | 
				
			||||||
 | 
					  fu.uint32(2),
 | 
				
			||||||
 | 
					  number => [Math.floor(number / SIZE_OF_32_BITS), number % SIZE_OF_32_BITS],
 | 
				
			||||||
 | 
					  _ => _[0] * SIZE_OF_32_BITS + _[1]
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fuFooter = fu.struct([
 | 
					const fuFooter = fu.struct([
 | 
				
			||||||
  fu.char('cookie', 8), // 0
 | 
					  fu.char('cookie', 8), // 0
 | 
				
			||||||
  fu.uint32('features'), // 8
 | 
					  fu.uint32('features'), // 8
 | 
				
			||||||
  fu.uint32('fileFormatVersion'), // 12
 | 
					  fu.uint32('fileFormatVersion'), // 12
 | 
				
			||||||
  fu.struct('dataOffset', [
 | 
					  uint64('dataOffset'), // offset of the header, should always be 512
 | 
				
			||||||
    fu.uint32('high'), // 16
 | 
					 | 
				
			||||||
    fu.uint32('low'), // 20
 | 
					 | 
				
			||||||
  ]),
 | 
					 | 
				
			||||||
  fu.uint32('timestamp'), // 24
 | 
					  fu.uint32('timestamp'), // 24
 | 
				
			||||||
  fu.char('creatorApplication', 4), // 28
 | 
					  fu.char('creatorApplication', 4), // 28
 | 
				
			||||||
  fu.uint32('creatorVersion'), // 32
 | 
					  fu.uint32('creatorVersion'), // 32
 | 
				
			||||||
  fu.uint32('creatorHostOs'), // 36
 | 
					  fu.uint32('creatorHostOs'), // 36
 | 
				
			||||||
  fu.struct('originalSize', [
 | 
					  uint64('originalSize'),
 | 
				
			||||||
    // At the creation, current size of the hard disk.
 | 
					  uint64('currentSize'),
 | 
				
			||||||
    fu.uint32('high'), // 40
 | 
					 | 
				
			||||||
    fu.uint32('low'), // 44
 | 
					 | 
				
			||||||
  ]),
 | 
					 | 
				
			||||||
  fu.struct('currentSize', [
 | 
					 | 
				
			||||||
    // Current size of the virtual disk. At the creation: currentSize = originalSize.
 | 
					 | 
				
			||||||
    fu.uint32('high'), // 48
 | 
					 | 
				
			||||||
    fu.uint32('low'), // 52
 | 
					 | 
				
			||||||
  ]),
 | 
					 | 
				
			||||||
  fu.struct('diskGeometry', [
 | 
					  fu.struct('diskGeometry', [
 | 
				
			||||||
    fu.uint16('cylinders'), // 56
 | 
					    fu.uint16('cylinders'), // 56
 | 
				
			||||||
    fu.uint8('heads'), // 58
 | 
					    fu.uint8('heads'), // 58
 | 
				
			||||||
@@ -87,12 +88,8 @@ const fuFooter = fu.struct([
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const fuHeader = fu.struct([
 | 
					const fuHeader = fu.struct([
 | 
				
			||||||
  fu.char('cookie', 8),
 | 
					  fu.char('cookie', 8),
 | 
				
			||||||
  fu.struct('dataOffset', [fu.uint32('high'), fu.uint32('low')]),
 | 
					  fu.uint8('dataOffsetUnused', 8),
 | 
				
			||||||
  fu.struct('tableOffset', [
 | 
					  uint64('tableOffset'),
 | 
				
			||||||
    // Absolute byte offset of the Block Allocation Table.
 | 
					 | 
				
			||||||
    fu.uint32('high'),
 | 
					 | 
				
			||||||
    fu.uint32('low'),
 | 
					 | 
				
			||||||
  ]),
 | 
					 | 
				
			||||||
  fu.uint32('headerVersion'),
 | 
					  fu.uint32('headerVersion'),
 | 
				
			||||||
  fu.uint32('maxTableEntries'), // Max entries in the Block Allocation Table.
 | 
					  fu.uint32('maxTableEntries'), // Max entries in the Block Allocation Table.
 | 
				
			||||||
  fu.uint32('blockSize'), // Block size in bytes. Default (2097152 => 2MB)
 | 
					  fu.uint32('blockSize'), // Block size in bytes. Default (2097152 => 2MB)
 | 
				
			||||||
@@ -108,11 +105,7 @@ const fuHeader = fu.struct([
 | 
				
			|||||||
      fu.uint32('platformDataSpace'),
 | 
					      fu.uint32('platformDataSpace'),
 | 
				
			||||||
      fu.uint32('platformDataLength'),
 | 
					      fu.uint32('platformDataLength'),
 | 
				
			||||||
      fu.uint32('reserved'),
 | 
					      fu.uint32('reserved'),
 | 
				
			||||||
      fu.struct('platformDataOffset', [
 | 
					      uint64('platformDataOffset'), // Absolute byte offset of the locator data.
 | 
				
			||||||
        // Absolute byte offset of the locator data.
 | 
					 | 
				
			||||||
        fu.uint32('high'),
 | 
					 | 
				
			||||||
        fu.uint32('low'),
 | 
					 | 
				
			||||||
      ]),
 | 
					 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    VHD_PARENT_LOCATOR_ENTRIES
 | 
					    VHD_PARENT_LOCATOR_ENTRIES
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
@@ -123,16 +116,14 @@ const fuHeader = fu.struct([
 | 
				
			|||||||
// Helpers
 | 
					// Helpers
 | 
				
			||||||
// ===================================================================
 | 
					// ===================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SIZE_OF_32_BITS = Math.pow(2, 32)
 | 
					const computeBatSize = entries =>
 | 
				
			||||||
const uint32ToUint64 = fu => fu.high * SIZE_OF_32_BITS + fu.low
 | 
					  sectorsToBytes(sectorsRoundUpNoZero(entries * VHD_ENTRY_SIZE))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Returns a 32 bits integer corresponding to a Vhd version.
 | 
					// Returns a 32 bits integer corresponding to a Vhd version.
 | 
				
			||||||
const getVhdVersion = (major, minor) => (major << 16) | (minor & 0x0000ffff)
 | 
					const getVhdVersion = (major, minor) => (major << 16) | (minor & 0x0000ffff)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Sectors conversions.
 | 
					// Sectors conversions.
 | 
				
			||||||
const sectorsRoundUp = bytes =>
 | 
					const sectorsRoundUpNoZero = bytes => Math.ceil(bytes / VHD_SECTOR_SIZE) || 1
 | 
				
			||||||
  Math.floor((bytes + VHD_SECTOR_SIZE - 1) / VHD_SECTOR_SIZE)
 | 
					 | 
				
			||||||
const sectorsRoundUpNoZero = bytes => sectorsRoundUp(bytes) || 1
 | 
					 | 
				
			||||||
const sectorsToBytes = sectors => sectors * VHD_SECTOR_SIZE
 | 
					const sectorsToBytes = sectors => sectors * VHD_SECTOR_SIZE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Check/Set a bit on a vhd map.
 | 
					// Check/Set a bit on a vhd map.
 | 
				
			||||||
@@ -163,26 +154,39 @@ const unpackField = (field, buf) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Returns the checksum of a raw struct.
 | 
					// Returns the checksum of a raw struct.
 | 
				
			||||||
// The raw struct (footer or header) is altered with the new sum.
 | 
					// The raw struct (footer or header) is altered with the new sum.
 | 
				
			||||||
function checksumStruct (rawStruct, struct) {
 | 
					function checksumStruct (buf, struct) {
 | 
				
			||||||
  const checksumField = struct.fields.checksum
 | 
					  const checksumField = struct.fields.checksum
 | 
				
			||||||
 | 
					 | 
				
			||||||
  let sum = 0
 | 
					  let sum = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Reset current sum.
 | 
					  // Do not use the stored checksum to compute the new checksum.
 | 
				
			||||||
  packField(checksumField, 0, rawStruct)
 | 
					  const checksumOffset = checksumField.offset
 | 
				
			||||||
 | 
					  for (let i = 0, n = checksumOffset; i < n; ++i) {
 | 
				
			||||||
  for (let i = 0, n = struct.size; i < n; i++) {
 | 
					    sum += buf[i]
 | 
				
			||||||
    sum = (sum + rawStruct[i]) & 0xffffffff
 | 
					  }
 | 
				
			||||||
 | 
					  for (
 | 
				
			||||||
 | 
					    let i = checksumOffset + checksumField.size, n = struct.size;
 | 
				
			||||||
 | 
					    i < n;
 | 
				
			||||||
 | 
					    ++i
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    sum += buf[i]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sum = 0xffffffff - sum
 | 
					  sum = ~sum >>> 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Write new sum.
 | 
					  // Write new sum.
 | 
				
			||||||
  packField(checksumField, sum, rawStruct)
 | 
					  packField(checksumField, sum, buf)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return sum
 | 
					  return sum
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const assertChecksum = (name, buf, struct) => {
 | 
				
			||||||
 | 
					  const actual = unpackField(struct.fields.checksum, buf)
 | 
				
			||||||
 | 
					  const expected = checksumStruct(buf, struct)
 | 
				
			||||||
 | 
					  if (actual !== expected) {
 | 
				
			||||||
 | 
					    throw new Error(`invalid ${name} checksum ${actual}, expected ${expected}`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ===================================================================
 | 
					// ===================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Format:
 | 
					// Format:
 | 
				
			||||||
@@ -207,6 +211,10 @@ function checksumStruct (rawStruct, struct) {
 | 
				
			|||||||
// - parentLocatorSize(i) = header.parentLocatorEntry[i].platformDataSpace * sectorSize
 | 
					// - parentLocatorSize(i) = header.parentLocatorEntry[i].platformDataSpace * sectorSize
 | 
				
			||||||
// - sectorSize = 512
 | 
					// - sectorSize = 512
 | 
				
			||||||
export class Vhd {
 | 
					export class Vhd {
 | 
				
			||||||
 | 
					  get batSize () {
 | 
				
			||||||
 | 
					    return computeBatSize(this.header.maxTableEntries)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor (handler, path) {
 | 
					  constructor (handler, path) {
 | 
				
			||||||
    this._handler = handler
 | 
					    this._handler = handler
 | 
				
			||||||
    this._path = path
 | 
					    this._path = path
 | 
				
			||||||
@@ -235,17 +243,10 @@ export class Vhd {
 | 
				
			|||||||
  getEndOfHeaders () {
 | 
					  getEndOfHeaders () {
 | 
				
			||||||
    const { header } = this
 | 
					    const { header } = this
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let end = uint32ToUint64(this.footer.dataOffset) + VHD_HEADER_SIZE
 | 
					    let end = VHD_FOOTER_SIZE + VHD_HEADER_SIZE
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const blockAllocationTableSize = sectorsToBytes(
 | 
					 | 
				
			||||||
      sectorsRoundUpNoZero(header.maxTableEntries * VHD_ENTRY_SIZE)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Max(end, block allocation table end)
 | 
					    // Max(end, block allocation table end)
 | 
				
			||||||
    end = Math.max(
 | 
					    end = Math.max(end, header.tableOffset + this.batSize)
 | 
				
			||||||
      end,
 | 
					 | 
				
			||||||
      uint32ToUint64(header.tableOffset) + blockAllocationTableSize
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (let i = 0; i < VHD_PARENT_LOCATOR_ENTRIES; i++) {
 | 
					    for (let i = 0; i < VHD_PARENT_LOCATOR_ENTRIES; i++) {
 | 
				
			||||||
      const entry = header.parentLocatorEntry[i]
 | 
					      const entry = header.parentLocatorEntry[i]
 | 
				
			||||||
@@ -253,8 +254,7 @@ export class Vhd {
 | 
				
			|||||||
      if (entry.platformCode !== VHD_PLATFORM_CODE_NONE) {
 | 
					      if (entry.platformCode !== VHD_PLATFORM_CODE_NONE) {
 | 
				
			||||||
        end = Math.max(
 | 
					        end = Math.max(
 | 
				
			||||||
          end,
 | 
					          end,
 | 
				
			||||||
          uint32ToUint64(entry.platformDataOffset) +
 | 
					          entry.platformDataOffset + sectorsToBytes(entry.platformDataSpace)
 | 
				
			||||||
            sectorsToBytes(entry.platformDataSpace)
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -286,21 +286,16 @@ export class Vhd {
 | 
				
			|||||||
  // Get the beginning (footer + header) of a vhd file.
 | 
					  // Get the beginning (footer + header) of a vhd file.
 | 
				
			||||||
  async readHeaderAndFooter () {
 | 
					  async readHeaderAndFooter () {
 | 
				
			||||||
    const buf = await this._read(0, VHD_FOOTER_SIZE + VHD_HEADER_SIZE)
 | 
					    const buf = await this._read(0, VHD_FOOTER_SIZE + VHD_HEADER_SIZE)
 | 
				
			||||||
 | 
					    const bufFooter = buf.slice(0, VHD_FOOTER_SIZE)
 | 
				
			||||||
 | 
					    const bufHeader = buf.slice(VHD_FOOTER_SIZE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const sum = unpackField(fuFooter.fields.checksum, buf)
 | 
					    assertChecksum('footer', bufFooter, fuFooter)
 | 
				
			||||||
    const sumToTest = checksumStruct(buf, fuFooter)
 | 
					    assertChecksum('header', bufHeader, fuHeader)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Checksum child & parent.
 | 
					    const footer = (this.footer = fuFooter.unpack(bufFooter))
 | 
				
			||||||
    if (sumToTest !== sum) {
 | 
					    assert.strictEqual(footer.dataOffset, VHD_FOOTER_SIZE)
 | 
				
			||||||
      throw new Error(
 | 
					 | 
				
			||||||
        `Bad checksum in vhd. Expected: ${sum}. Given: ${sumToTest}. (data=${buf.toString(
 | 
					 | 
				
			||||||
          'hex'
 | 
					 | 
				
			||||||
        )})`
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const header = (this.header = fuHeader.unpack(buf.slice(VHD_FOOTER_SIZE)))
 | 
					    const header = (this.header = fuHeader.unpack(bufHeader))
 | 
				
			||||||
    this.footer = fuFooter.unpack(buf)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Compute the number of sectors in one block.
 | 
					    // Compute the number of sectors in one block.
 | 
				
			||||||
    // Default: One block contains 4096 sectors of 512 bytes.
 | 
					    // Default: One block contains 4096 sectors of 512 bytes.
 | 
				
			||||||
@@ -330,13 +325,10 @@ export class Vhd {
 | 
				
			|||||||
  // Returns a buffer that contains the block allocation table of a vhd file.
 | 
					  // Returns a buffer that contains the block allocation table of a vhd file.
 | 
				
			||||||
  async readBlockTable () {
 | 
					  async readBlockTable () {
 | 
				
			||||||
    const { header } = this
 | 
					    const { header } = this
 | 
				
			||||||
 | 
					    this.blockTable = await this._read(
 | 
				
			||||||
    const offset = uint32ToUint64(header.tableOffset)
 | 
					      header.tableOffset,
 | 
				
			||||||
    const size = sectorsToBytes(
 | 
					      header.maxTableEntries * VHD_ENTRY_SIZE
 | 
				
			||||||
      sectorsRoundUpNoZero(header.maxTableEntries * VHD_ENTRY_SIZE)
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.blockTable = await this._read(offset, size)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // return the first sector (bitmap) of a block
 | 
					  // return the first sector (bitmap) of a block
 | 
				
			||||||
@@ -433,71 +425,70 @@ export class Vhd {
 | 
				
			|||||||
      : fromEvent(data.pipe(stream), 'finish')
 | 
					      : fromEvent(data.pipe(stream), 'finish')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async ensureBatSize (size) {
 | 
					  async _freeFirstBlockSpace (spaceNeededBytes) {
 | 
				
			||||||
    const { header } = this
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const prevMaxTableEntries = header.maxTableEntries
 | 
					 | 
				
			||||||
    if (prevMaxTableEntries >= size) {
 | 
					 | 
				
			||||||
      return
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const tableOffset = uint32ToUint64(header.tableOffset)
 | 
					 | 
				
			||||||
    // extend BAT
 | 
					 | 
				
			||||||
    const maxTableEntries = (header.maxTableEntries = size)
 | 
					 | 
				
			||||||
    const batSize = sectorsToBytes(
 | 
					 | 
				
			||||||
      sectorsRoundUpNoZero(maxTableEntries * VHD_ENTRY_SIZE)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    const prevBat = this.blockTable
 | 
					 | 
				
			||||||
    const bat = (this.blockTable = Buffer.allocUnsafe(batSize))
 | 
					 | 
				
			||||||
    prevBat.copy(bat)
 | 
					 | 
				
			||||||
    bat.fill(BUF_BLOCK_UNUSED, prevBat.length)
 | 
					 | 
				
			||||||
    debug(
 | 
					 | 
				
			||||||
      `ensureBatSize: extend in memory BAT ${prevMaxTableEntries} -> ${maxTableEntries}`
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const extendBat = async () => {
 | 
					 | 
				
			||||||
      debug(
 | 
					 | 
				
			||||||
        `ensureBatSize: extend in file BAT ${prevMaxTableEntries} -> ${maxTableEntries}`
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return this._write(
 | 
					 | 
				
			||||||
        constantStream(BUF_BLOCK_UNUSED, maxTableEntries - prevMaxTableEntries),
 | 
					 | 
				
			||||||
        tableOffset + prevBat.length
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const { first, firstSector, lastSector } = this._getFirstAndLastBlocks()
 | 
					      const { first, firstSector, lastSector } = this._getFirstAndLastBlocks()
 | 
				
			||||||
      if (tableOffset + batSize < sectorsToBytes(firstSector)) {
 | 
					      const tableOffset = this.header.tableOffset
 | 
				
			||||||
        return Promise.all([extendBat(), this.writeHeader()])
 | 
					      const { batSize } = this
 | 
				
			||||||
 | 
					      const newMinSector = Math.ceil(
 | 
				
			||||||
 | 
					        (tableOffset + batSize + spaceNeededBytes) / VHD_SECTOR_SIZE
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        tableOffset + batSize + spaceNeededBytes >=
 | 
				
			||||||
 | 
					        sectorsToBytes(firstSector)
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        const { fullBlockSize } = this
 | 
				
			||||||
 | 
					        const newFirstSector = Math.max(
 | 
				
			||||||
 | 
					          lastSector + fullBlockSize / VHD_SECTOR_SIZE,
 | 
				
			||||||
 | 
					          newMinSector
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        debug(
 | 
				
			||||||
 | 
					          `freeFirstBlockSpace: move first block ${firstSector} -> ${newFirstSector}`
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        // copy the first block at the end
 | 
				
			||||||
 | 
					        const stream = await this._readStream(
 | 
				
			||||||
 | 
					          sectorsToBytes(firstSector),
 | 
				
			||||||
 | 
					          fullBlockSize
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        await this._write(stream, sectorsToBytes(newFirstSector))
 | 
				
			||||||
 | 
					        await this._setBatEntry(first, newFirstSector)
 | 
				
			||||||
 | 
					        await this.writeFooter(true)
 | 
				
			||||||
 | 
					        spaceNeededBytes -= this.fullBlockSize
 | 
				
			||||||
 | 
					        if (spaceNeededBytes > 0) {
 | 
				
			||||||
 | 
					          return this._freeFirstBlockSpace(spaceNeededBytes)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
      const { fullBlockSize } = this
 | 
					 | 
				
			||||||
      const newFirstSector = lastSector + fullBlockSize / VHD_SECTOR_SIZE
 | 
					 | 
				
			||||||
      debug(
 | 
					 | 
				
			||||||
        `ensureBatSize: move first block ${firstSector} -> ${newFirstSector}`
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // copy the first block at the end
 | 
					 | 
				
			||||||
      const stream = await this._readStream(
 | 
					 | 
				
			||||||
        sectorsToBytes(firstSector),
 | 
					 | 
				
			||||||
        fullBlockSize
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
      await this._write(stream, sectorsToBytes(newFirstSector))
 | 
					 | 
				
			||||||
      await extendBat()
 | 
					 | 
				
			||||||
      await this._setBatEntry(first, newFirstSector)
 | 
					 | 
				
			||||||
      await this.writeHeader()
 | 
					 | 
				
			||||||
      await this.writeFooter()
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      if (e.noBlock) {
 | 
					      if (!e.noBlock) {
 | 
				
			||||||
        await extendBat()
 | 
					 | 
				
			||||||
        await this.writeHeader()
 | 
					 | 
				
			||||||
        await this.writeFooter()
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        throw e
 | 
					        throw e
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async ensureBatSize (entries) {
 | 
				
			||||||
 | 
					    const { header } = this
 | 
				
			||||||
 | 
					    const prevMaxTableEntries = header.maxTableEntries
 | 
				
			||||||
 | 
					    if (prevMaxTableEntries >= entries) {
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const newBatSize = computeBatSize(entries)
 | 
				
			||||||
 | 
					    await this._freeFirstBlockSpace(newBatSize - this.batSize)
 | 
				
			||||||
 | 
					    const maxTableEntries = (header.maxTableEntries = entries)
 | 
				
			||||||
 | 
					    const prevBat = this.blockTable
 | 
				
			||||||
 | 
					    const bat = (this.blockTable = Buffer.allocUnsafe(newBatSize))
 | 
				
			||||||
 | 
					    prevBat.copy(bat)
 | 
				
			||||||
 | 
					    bat.fill(BUF_BLOCK_UNUSED, prevMaxTableEntries * VHD_ENTRY_SIZE)
 | 
				
			||||||
 | 
					    debug(
 | 
				
			||||||
 | 
					      `ensureBatSize: extend BAT ${prevMaxTableEntries} -> ${maxTableEntries}`
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    await this._write(
 | 
				
			||||||
 | 
					      constantStream(BUF_BLOCK_UNUSED, maxTableEntries - prevMaxTableEntries),
 | 
				
			||||||
 | 
					      header.tableOffset + prevBat.length
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    await this.writeHeader()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // set the first sector (bitmap) of a block
 | 
					  // set the first sector (bitmap) of a block
 | 
				
			||||||
  _setBatEntry (block, blockSector) {
 | 
					  _setBatEntry (block, blockSector) {
 | 
				
			||||||
    const i = block * VHD_ENTRY_SIZE
 | 
					    const i = block * VHD_ENTRY_SIZE
 | 
				
			||||||
@@ -507,7 +498,7 @@ export class Vhd {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return this._write(
 | 
					    return this._write(
 | 
				
			||||||
      blockTable.slice(i, i + VHD_ENTRY_SIZE),
 | 
					      blockTable.slice(i, i + VHD_ENTRY_SIZE),
 | 
				
			||||||
      uint32ToUint64(this.header.tableOffset) + i
 | 
					      this.header.tableOffset + i
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -563,6 +554,9 @@ export class Vhd {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (blockAddr === BLOCK_UNUSED) {
 | 
					    if (blockAddr === BLOCK_UNUSED) {
 | 
				
			||||||
      blockAddr = await this.createBlock(block.id)
 | 
					      blockAddr = await this.createBlock(block.id)
 | 
				
			||||||
 | 
					      parentBitmap = Buffer.alloc(this.bitmapSize, 0)
 | 
				
			||||||
 | 
					    } else if (parentBitmap === undefined) {
 | 
				
			||||||
 | 
					      parentBitmap = (await this._readBlock(block.id, true)).bitmap
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const offset = blockAddr + this.sectorsOfBitmap + beginSectorId
 | 
					    const offset = blockAddr + this.sectorsOfBitmap + beginSectorId
 | 
				
			||||||
@@ -629,11 +623,13 @@ export class Vhd {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Write a context footer. (At the end and beginning of a vhd file.)
 | 
					  // Write a context footer. (At the end and beginning of a vhd file.)
 | 
				
			||||||
  async writeFooter () {
 | 
					  async writeFooter (onlyEndFooter = false) {
 | 
				
			||||||
    const { footer } = this
 | 
					    const { footer } = this
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const offset = this.getEndOfData()
 | 
					 | 
				
			||||||
    const rawFooter = fuFooter.pack(footer)
 | 
					    const rawFooter = fuFooter.pack(footer)
 | 
				
			||||||
 | 
					    const eof = await this._handler.getSize(this._path)
 | 
				
			||||||
 | 
					    // sometimes the file is longer than anticipated, we still need to put the footer at the end
 | 
				
			||||||
 | 
					    const offset = Math.max(this.getEndOfData(), eof - rawFooter.length)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    footer.checksum = checksumStruct(rawFooter, fuFooter)
 | 
					    footer.checksum = checksumStruct(rawFooter, fuFooter)
 | 
				
			||||||
    debug(
 | 
					    debug(
 | 
				
			||||||
@@ -641,8 +637,9 @@ export class Vhd {
 | 
				
			|||||||
        footer.checksum
 | 
					        footer.checksum
 | 
				
			||||||
      }). (data=${rawFooter.toString('hex')})`
 | 
					      }). (data=${rawFooter.toString('hex')})`
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    if (!onlyEndFooter) {
 | 
				
			||||||
    await this._write(rawFooter, 0)
 | 
					      await this._write(rawFooter, 0)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    await this._write(rawFooter, offset)
 | 
					    await this._write(rawFooter, offset)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -658,6 +655,73 @@ export class Vhd {
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    return this._write(rawHeader, offset)
 | 
					    return this._write(rawHeader, offset)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async writeData (offsetSectors, buffer) {
 | 
				
			||||||
 | 
					    const bufferSizeSectors = Math.ceil(buffer.length / VHD_SECTOR_SIZE)
 | 
				
			||||||
 | 
					    const startBlock = Math.floor(offsetSectors / this.sectorsPerBlock)
 | 
				
			||||||
 | 
					    const endBufferSectors = offsetSectors + bufferSizeSectors
 | 
				
			||||||
 | 
					    const lastBlock = Math.ceil(endBufferSectors / this.sectorsPerBlock) - 1
 | 
				
			||||||
 | 
					    await this.ensureBatSize(lastBlock)
 | 
				
			||||||
 | 
					    const blockSizeBytes = this.sectorsPerBlock * VHD_SECTOR_SIZE
 | 
				
			||||||
 | 
					    const coversWholeBlock = (offsetInBlockSectors, endInBlockSectors) =>
 | 
				
			||||||
 | 
					      offsetInBlockSectors === 0 && endInBlockSectors === this.sectorsPerBlock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (
 | 
				
			||||||
 | 
					      let currentBlock = startBlock;
 | 
				
			||||||
 | 
					      currentBlock <= lastBlock;
 | 
				
			||||||
 | 
					      currentBlock++
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      const offsetInBlockSectors = Math.max(
 | 
				
			||||||
 | 
					        0,
 | 
				
			||||||
 | 
					        offsetSectors - currentBlock * this.sectorsPerBlock
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      const endInBlockSectors = Math.min(
 | 
				
			||||||
 | 
					        endBufferSectors - currentBlock * this.sectorsPerBlock,
 | 
				
			||||||
 | 
					        this.sectorsPerBlock
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      const startInBuffer = Math.max(
 | 
				
			||||||
 | 
					        0,
 | 
				
			||||||
 | 
					        (currentBlock * this.sectorsPerBlock - offsetSectors) * VHD_SECTOR_SIZE
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      const endInBuffer = Math.min(
 | 
				
			||||||
 | 
					        ((currentBlock + 1) * this.sectorsPerBlock - offsetSectors) *
 | 
				
			||||||
 | 
					          VHD_SECTOR_SIZE,
 | 
				
			||||||
 | 
					        buffer.length
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      let inputBuffer
 | 
				
			||||||
 | 
					      if (coversWholeBlock(offsetInBlockSectors, endInBlockSectors)) {
 | 
				
			||||||
 | 
					        inputBuffer = buffer.slice(startInBuffer, endInBuffer)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        inputBuffer = Buffer.alloc(blockSizeBytes, 0)
 | 
				
			||||||
 | 
					        buffer.copy(
 | 
				
			||||||
 | 
					          inputBuffer,
 | 
				
			||||||
 | 
					          offsetInBlockSectors * VHD_SECTOR_SIZE,
 | 
				
			||||||
 | 
					          startInBuffer,
 | 
				
			||||||
 | 
					          endInBuffer
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      await this.writeBlockSectors(
 | 
				
			||||||
 | 
					        { id: currentBlock, data: inputBuffer },
 | 
				
			||||||
 | 
					        offsetInBlockSectors,
 | 
				
			||||||
 | 
					        endInBlockSectors
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await this.writeFooter()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async ensureSpaceForParentLocators (neededSectors) {
 | 
				
			||||||
 | 
					    const firstLocatorOffset = VHD_FOOTER_SIZE + VHD_HEADER_SIZE
 | 
				
			||||||
 | 
					    const currentSpace =
 | 
				
			||||||
 | 
					      Math.floor(this.header.tableOffset / VHD_SECTOR_SIZE) -
 | 
				
			||||||
 | 
					      firstLocatorOffset / VHD_SECTOR_SIZE
 | 
				
			||||||
 | 
					    if (currentSpace < neededSectors) {
 | 
				
			||||||
 | 
					      const deltaSectors = neededSectors - currentSpace
 | 
				
			||||||
 | 
					      await this._freeFirstBlockSpace(sectorsToBytes(deltaSectors))
 | 
				
			||||||
 | 
					      this.header.tableOffset += sectorsToBytes(deltaSectors)
 | 
				
			||||||
 | 
					      await this._write(this.blockTable, this.header.tableOffset)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return firstLocatorOffset
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Merge vhd child into vhd parent.
 | 
					// Merge vhd child into vhd parent.
 | 
				
			||||||
@@ -719,9 +783,9 @@ export default concurrency(2)(async function vhdMerge (
 | 
				
			|||||||
      const cFooter = childVhd.footer
 | 
					      const cFooter = childVhd.footer
 | 
				
			||||||
      const pFooter = parentVhd.footer
 | 
					      const pFooter = parentVhd.footer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      pFooter.currentSize = { ...cFooter.currentSize }
 | 
					      pFooter.currentSize = cFooter.currentSize
 | 
				
			||||||
      pFooter.diskGeometry = { ...cFooter.diskGeometry }
 | 
					      pFooter.diskGeometry = { ...cFooter.diskGeometry }
 | 
				
			||||||
      pFooter.originalSize = { ...cFooter.originalSize }
 | 
					      pFooter.originalSize = cFooter.originalSize
 | 
				
			||||||
      pFooter.timestamp = cFooter.timestamp
 | 
					      pFooter.timestamp = cFooter.timestamp
 | 
				
			||||||
      pFooter.uuid = cFooter.uuid
 | 
					      pFooter.uuid = cFooter.uuid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -743,30 +807,51 @@ export async function chainVhd (
 | 
				
			|||||||
  parentHandler,
 | 
					  parentHandler,
 | 
				
			||||||
  parentPath,
 | 
					  parentPath,
 | 
				
			||||||
  childHandler,
 | 
					  childHandler,
 | 
				
			||||||
  childPath
 | 
					  childPath,
 | 
				
			||||||
 | 
					  force = false
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const parentVhd = new Vhd(parentHandler, parentPath)
 | 
					  const parentVhd = new Vhd(parentHandler, parentPath)
 | 
				
			||||||
  const childVhd = new Vhd(childHandler, childPath)
 | 
					  const childVhd = new Vhd(childHandler, childPath)
 | 
				
			||||||
  await Promise.all([
 | 
					 | 
				
			||||||
    parentVhd.readHeaderAndFooter(),
 | 
					 | 
				
			||||||
    childVhd.readHeaderAndFooter(),
 | 
					 | 
				
			||||||
  ])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { header } = childVhd
 | 
					  await childVhd.readHeaderAndFooter()
 | 
				
			||||||
 | 
					  const { header, footer } = childVhd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const parentName = relative(dirname(childPath), parentPath)
 | 
					  if (footer.diskType !== HARD_DISK_TYPE_DIFFERENCING) {
 | 
				
			||||||
  const parentUuid = parentVhd.footer.uuid
 | 
					    if (!force) {
 | 
				
			||||||
  if (
 | 
					      throw new Error('cannot chain disk of type ' + footer.diskType)
 | 
				
			||||||
    header.parentUnicodeName !== parentName ||
 | 
					    }
 | 
				
			||||||
    !isEqual(header.parentUuid, parentUuid)
 | 
					    footer.diskType = HARD_DISK_TYPE_DIFFERENCING
 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    header.parentUuid = parentUuid
 | 
					 | 
				
			||||||
    header.parentUnicodeName = parentName
 | 
					 | 
				
			||||||
    await childVhd.writeHeader()
 | 
					 | 
				
			||||||
    return true
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return false
 | 
					  await Promise.all([
 | 
				
			||||||
 | 
					    childVhd.readBlockTable(),
 | 
				
			||||||
 | 
					    parentVhd.readHeaderAndFooter(),
 | 
				
			||||||
 | 
					  ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const parentName = relative(dirname(childPath), parentPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  header.parentUuid = parentVhd.footer.uuid
 | 
				
			||||||
 | 
					  header.parentUnicodeName = parentName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  header.parentLocatorEntry[0].platformCode = PLATFORM_W2KU
 | 
				
			||||||
 | 
					  const encodedFilename = Buffer.from(parentName, 'utf16le')
 | 
				
			||||||
 | 
					  const dataSpaceSectors = Math.ceil(encodedFilename.length / VHD_SECTOR_SIZE)
 | 
				
			||||||
 | 
					  const position = await childVhd.ensureSpaceForParentLocators(dataSpaceSectors)
 | 
				
			||||||
 | 
					  await childVhd._write(encodedFilename, position)
 | 
				
			||||||
 | 
					  header.parentLocatorEntry[0].platformDataSpace = sectorsToBytes(
 | 
				
			||||||
 | 
					    dataSpaceSectors
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  header.parentLocatorEntry[0].platformDataLength = encodedFilename.length
 | 
				
			||||||
 | 
					  header.parentLocatorEntry[0].platformDataOffset = position
 | 
				
			||||||
 | 
					  for (let i = 1; i < 8; i++) {
 | 
				
			||||||
 | 
					    header.parentLocatorEntry[i].platformCode = VHD_PLATFORM_CODE_NONE
 | 
				
			||||||
 | 
					    header.parentLocatorEntry[i].platformDataSpace = 0
 | 
				
			||||||
 | 
					    header.parentLocatorEntry[i].platformDataLength = 0
 | 
				
			||||||
 | 
					    header.parentLocatorEntry[i].platformDataOffset = 0
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  await childVhd.writeHeader()
 | 
				
			||||||
 | 
					  await childVhd.writeFooter()
 | 
				
			||||||
 | 
					  return true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createReadStream = asyncIteratorToStream(function * (handler, path) {
 | 
					export const createReadStream = asyncIteratorToStream(function * (handler, path) {
 | 
				
			||||||
@@ -797,10 +882,7 @@ export const createReadStream = asyncIteratorToStream(function * (handler, path)
 | 
				
			|||||||
    // TODO: empty parentUuid and parentLocatorEntry-s in header
 | 
					    // TODO: empty parentUuid and parentLocatorEntry-s in header
 | 
				
			||||||
    let header = {
 | 
					    let header = {
 | 
				
			||||||
      ...vhd.header,
 | 
					      ...vhd.header,
 | 
				
			||||||
      tableOffset: {
 | 
					      tableOffset: 512 + 1024,
 | 
				
			||||||
        high: 0,
 | 
					 | 
				
			||||||
        low: 512 + 1024,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      parentUnicodeName: '',
 | 
					      parentUnicodeName: '',
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -815,9 +897,7 @@ export const createReadStream = asyncIteratorToStream(function * (handler, path)
 | 
				
			|||||||
    const sectorsPerBlock =
 | 
					    const sectorsPerBlock =
 | 
				
			||||||
      sectorsPerBlockData + vhd.bitmapSize / VHD_SECTOR_SIZE
 | 
					      sectorsPerBlockData + vhd.bitmapSize / VHD_SECTOR_SIZE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const nBlocks = Math.ceil(
 | 
					    const nBlocks = Math.ceil(footer.currentSize / header.blockSize)
 | 
				
			||||||
      uint32ToUint64(footer.currentSize) / header.blockSize
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const blocksOwner = new Array(nBlocks)
 | 
					    const blocksOwner = new Array(nBlocks)
 | 
				
			||||||
    for (
 | 
					    for (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ const formatFiles = files => {
 | 
				
			|||||||
const testFiles = files =>
 | 
					const testFiles = files =>
 | 
				
			||||||
  run(
 | 
					  run(
 | 
				
			||||||
    './node_modules/.bin/jest',
 | 
					    './node_modules/.bin/jest',
 | 
				
			||||||
    ['--findRelatedTests', '--passWithNoTests'].concat(files)
 | 
					    ['--testRegex=^(?!.*.integ.spec.js$).*.spec.js$', '--findRelatedTests', '--passWithNoTests'].concat(files)
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// -----------------------------------------------------------------------------
 | 
					// -----------------------------------------------------------------------------
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -701,10 +701,6 @@
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    pako "^1.0.3"
 | 
					    pako "^1.0.3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@nraynaud/struct-fu@^1.0.1":
 | 
					 | 
				
			||||||
  version "1.0.1"
 | 
					 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@nraynaud/struct-fu/-/struct-fu-1.0.1.tgz#059a0588dea50647c3677783692dafdadfcadf97"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"@nraynaud/xo-fs@^0.0.5":
 | 
					"@nraynaud/xo-fs@^0.0.5":
 | 
				
			||||||
  version "0.0.5"
 | 
					  version "0.0.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@nraynaud/xo-fs/-/xo-fs-0.0.5.tgz#0f8c525440909223904b6841a37f4d255baa54b3"
 | 
					  resolved "https://registry.yarnpkg.com/@nraynaud/xo-fs/-/xo-fs-0.0.5.tgz#0f8c525440909223904b6841a37f4d255baa54b3"
 | 
				
			||||||
@@ -10886,7 +10882,7 @@ strip-json-comments@~2.0.1:
 | 
				
			|||||||
  version "2.0.1"
 | 
					  version "2.0.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
 | 
					  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct-fu@^1.0.0:
 | 
					struct-fu@^1.0.0, struct-fu@^1.2.0:
 | 
				
			||||||
  version "1.2.0"
 | 
					  version "1.2.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/struct-fu/-/struct-fu-1.2.0.tgz#a40b9eb60a41bb341228cff125fde4887daa85ac"
 | 
					  resolved "https://registry.yarnpkg.com/struct-fu/-/struct-fu-1.2.0.tgz#a40b9eb60a41bb341228cff125fde4887daa85ac"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user