async function query (root, opts) {
  // validate opts
  if (!opts || !opts.path) throw new Error('The `path` parameter is required')
  if (!(typeof opts.path === 'string' || (Array.isArray(opts.path) && opts.path.every(v => typeof v === 'string')))) {
    throw new Error('The `path` parameter must be a string or array of strings')
  }
  if (opts.type && typeof opts.type !== 'string') {
    throw new Error('The `type` parameter must be a string')
  }
  if (opts.mount && typeof opts.mount !== 'string') {
    throw new Error('The `mount` parameter must be a string')
  }
  if (opts.metadata && typeof opts.metadata !== 'object') {
    throw new Error('The `metadata` parameter must be an object')
  }

  // massage opts
  if (opts.mount) {
    opts.mount = await hyperDns.resolveName(opts.mount)
    opts.mount = HYPERDRIVE_HASH_REGEX.exec(opts.mount)[0]
  }

  var keyToUrlCache = {}
  async function keyToUrl (key) {
    if (keyToUrlCache[key]) return keyToUrlCache[key]
    var domain = await hyperDns.reverseResolve(key)
    if (!domain) domain = key
    keyToUrlCache[key] = `hyper://${domain}/`
    return keyToUrlCache[key]
  }

  // iterate all matching paths and match against the query
  var candidates = await expandPaths(root, opts.path)
  var results = []
  await chunkMapAsync(candidates, 100, async (item) => {
    let {path, stat, originDriveKey, originPath} = item

    var type = 'file'
    if (stat.mount && stat.mount.key) type = 'mount'
    else if (stat.isDirectory()) type = 'directory'

    if (opts.type && type !== opts.type) return
    if (opts.mount && (type !== 'mount' || stat.mount.key.toString('hex') !== opts.mount)) return
    if (opts.metadata) {
      let metaMatch = true
      for (let k in opts.metadata) {
        if (stat.metadata[k] !== opts.metadata[k]) {
          metaMatch = false
          break
        }
      }
      if (!metaMatch) return
    }

    var originDrive = await keyToUrl(originDriveKey)
    results.push({
      type,
      path,
      drive: root.url,
      url: joinPath(root.url, path),
      stat,
      mount: type === 'mount' ? await keyToUrl(stat.mount.key.toString('hex')) : undefined,
      origin: {
        path: originPath,
        drive: originDrive,
        url: joinPath(originDrive, originPath)
      }
    })
  })

  if (opts.sort === 'name') {
    results.sort((a, b) => (opts.reverse) ? basename(b.path).toLowerCase().localeCompare(basename(a.path).toLowerCase()) : basename(a.path).toLowerCase().localeCompare(basename(b.path).toLowerCase()))
  } else if (opts.sort === 'mtime') {
    results.sort((a, b) => (opts.reverse) ? b.stat.mtime - a.stat.mtime : a.stat.mtime - b.stat.mtime)
  } else if (opts.sort === 'ctime') {
    results.sort((a, b) => (opts.reverse) ? b.stat.ctime - a.stat.ctime : a.stat.ctime - b.stat.ctime)
  }

  if (opts.offset && opts.limit) results = results.slice(opts.offset, opts.offset + opts.limit)
  else if (opts.offset) results = results.slice(opts.offset)
  else if (opts.limit) results = results.slice(0, opts.limit)

  return results
}
@param {DaemonHyperdrive} root
@param {FSQueryOpts} opts
@returns {Promise<FSQueryResult[]>}