OverviewFilesCommitsIssuesDocsReleasesPeers

autobase-doctor / node_modules / device-file / index.js

289 lines · javascript ref:
const fs = require('fs')
const path = require('path')
const fsx = require('fs-native-extensions')
const b4a = require('b4a')
const ReadyResource = require('ready-resource')
const FDLock = require('fd-lock')

const PLATFORM = global.Bare ? global.Bare.platform : global.process.platform
const IS_WIN = PLATFORM === 'win32'
const IS_LINUX = PLATFORM === 'linux'
const MODIFIED_SLACK = 5000
const EMPTY = b4a.alloc(0)
const ATTR = IS_LINUX ? 'user.device-file' : 'device-file'

const nl = IS_WIN ? '\r\n' : '\n'

class DeviceFile extends ReadyResource {
  constructor(filename, { create = true, wait = false, lock = wait, data = {} } = {}) {
    super()

    this.filename = filename
    this.data = data
    this.lock = null

    this._create = create
    this._wait = wait
    this._lock = lock
  }

  async _open() {
    if (await verifyDeviceFile(this)) return
    if (!this._create) throwDeviceFileError('No device file present', false)
    await writeDeviceFile(this)
  }

  static async validate(filename, data) {
    const file = new DeviceFile(filename, { create: false, wait: false, data })
    try {
      await verifyDeviceFile(file)
    } catch (err) {
      if (err.fatal && err.code === 'DEVICE_FILE') return false
      throw err
    }
    return true
  }

  transfer() {
    return this.lock ? this.lock.transfer() : -1
  }

  async _close() {
    if (this.lock) await this.lock.close()
  }

  async suspend() {
    if (!this.opened) await this.ready()
    if (this.lock) await this.lock.suspend()
  }

  async resume() {
    if (!this.opened) await this.ready()
    if (this.lock) await this.lock.resume()
  }
}

module.exports = DeviceFile

async function writeDeviceFile(device) {
  let s = ''

  for (const [key, value] of Object.entries(device.data)) {
    if (value === null) continue
    s += key + '=' + value + nl
  }

  await fs.promises.mkdir(path.dirname(device.filename), { recursive: true })

  const fd = await open(device.filename, 'w')

  device.lock = device._lock ? new FDLock(fd, { wait: device._wait }) : null

  if (device.lock) {
    try {
      await device.lock.ready()
    } catch (err) {
      await device.lock.close()
      throw err
    }
  }

  const st = await fstat(fd)

  const created = Date.now()

  s += 'device/platform=' + PLATFORM + nl
  s += 'device/inode=' + st.ino + nl
  s += 'device/created=' + created + nl

  if (await setAttr(fd, ATTR, b4a.from('original'))) {
    s += 'device/attribute=original' + nl
  }

  await write(fd, b4a.from(s))

  if (!device.lock) await close(fd)
}

async function verifyDeviceFile(device) {
  let fd = 0

  try {
    fd = await open(device.filename, 'r+')
  } catch (e) {
    fd = 0
  }

  if (fd === 0) return false

  device.lock = device._lock ? new FDLock(fd, { wait: device._wait }) : null

  if (device.lock) {
    try {
      await device.lock.ready()
    } catch (err) {
      await device.lock.close()
      throw err
    }
  }

  const buf = await read(fd)
  const result = {}

  const s = b4a.toString(buf).trim().split('\n')

  let inode = 0
  let created = 0
  let attr = ''
  let platform = ''

  for (const ln of s) {
    const i = ln.indexOf('=')
    if (i === -1) continue

    const k = ln.slice(0, i).trim()
    const v = ln.slice(i + 1).trim()

    switch (k) {
      case 'device/platform':
        platform = v
        break
      case 'device/inode':
        inode = Number(v)
        break
      case 'device/created':
        created = Number(v)
        break
      case 'device/attribute':
        attr = v
        break
      default:
        result[k] = v
        break
    }
  }

  for (const [k, v] of Object.entries(device.data)) {
    if (v === null) continue
    if (result[k] === undefined) continue // allow upserts
    if (result[k] !== '' + v) {
      await teardown()
      throwDeviceFileError(`Invalid device file, ${k} has changed. Was ${result[k]}, is ${v}`, true)
    }
  }

  const st = await fstat(fd)
  const at = await getAttr(fd, ATTR)

  const sameAttr = b4a.toString(at || EMPTY) === attr
  const modified = Math.max(st.mtime.getTime(), st.birthtime.getTime())

  if (platform && platform !== PLATFORM) {
    await teardown()
    throwDeviceFileError('Invalid device file, was made on different platform', true)
  }

  if (!sameAttr) {
    await teardown()
    throwDeviceFileError('Invalid device file, was moved unsafely', true)
  }

  if (st.ino !== inode || (created && Math.abs(modified - created) >= MODIFIED_SLACK)) {
    await teardown()
    throwDeviceFileError('Invalid device file, was modified', true)
  }

  if (!device.lock) await close(fd)

  device.data = result
  return true

  async function teardown() {
    if (device.lock) await device.lock.close()
    else await close(fd)
  }
}

async function getAttr(fd, name) {
  try {
    return await fsx.getAttr(fd, name)
  } catch {
    return null
  }
}

async function setAttr(fd, name, value) {
  try {
    await fsx.setAttr(fd, name, value)
    return true
  } catch {
    return false
  }
}

function fstat(fd) {
  return new Promise((resolve, reject) => {
    fs.fstat(fd, (err, st) => {
      if (err) reject(err)
      resolve(st)
    })
  })
}

function close(fd) {
  return new Promise((resolve, reject) => {
    fs.close(fd, (err, st) => {
      if (err) reject(err)
      resolve(st)
    })
  })
}

function write(fd, buf) {
  return new Promise((resolve, reject) => {
    let offset = 0

    onwrite(null, 0)

    function onwrite(err, wrote) {
      if (err) return reject(err)
      if (offset === buf.byteLength) return resolve()
      offset += wrote
      fs.write(fd, buf, offset, buf.byteLength - offset, offset, onwrite)
    }
  })
}

function read(fd) {
  const buf = b4a.allocUnsafe(4096)

  return new Promise((resolve, reject) => {
    let offset = 0

    fs.read(fd, buf, 0, buf.byteLength, 0, onread)

    function onread(err, read) {
      if (err) return reject(err)
      if (read === 0) return resolve(buf.subarray(0, offset))
      offset += read
      fs.read(fd, buf, offset, buf.byteLength - offset, offset, onread)
    }
  })
}

function open(filename, flags) {
  return new Promise((resolve, reject) => {
    fs.open(filename, flags, (err, fd) => {
      if (err) reject(err)
      resolve(fd)
    })
  })
}

function throwDeviceFileError(message, fatal) {
  const err = new Error(message)
  err.code = 'DEVICE_FILE'
  err.fatal = fatal
  throw err
}