///|
pub fn Image::inode(
  self : Image,
  reference : UInt64,
) -> Inode raise SquashError {
  let relative = index(reference >> 16, 32)
  guard relative < self.superblock.directory_table - self.superblock.inode_table else {
    raise Invalid(relative, "inode reference outside inode table")
  }
  let block = self.superblock.inode_table + relative
  let offset = (reference & 0xffffUL).to_int()
  let base = self.metadata(block, offset, 16)
  let kind = u16(base, 0)
  let number = index(u32(base, 12).to_uint64(), block)
  guard number > 0 && number <= self.superblock.inodes else {
    raise Invalid(block, "inode number out of range")
  }
  let uid = self.identity(u16(base, 4))
  let gid = self.identity(u16(base, 6))
  let mut links = 1
  let mut xattr = 0xffffffffU
  let mut parent = 0
  let content = match kind {
    1 | 8 => {
      let long = kind == 8
      let b = self.metadata(block, offset, if long { 40 } else { 32 })
      let start = index(u32(b, if long { 24 } else { 16 }).to_uint64(), block)
      let size = if long {
        index(u32(b, 20).to_uint64(), block)
      } else {
        u16(b, 24)
      }
      let off = u16(b, if long { 34 } else { 26 })
      links = index(u32(b, if long { 16 } else { 20 }).to_uint64(), block)
      if long {
        xattr = u32(b, 36)
      }
      parent = index(u32(b, 28).to_uint64(), block)
      guard size >= 3 &&
        start <= self.superblock.bytes_used - self.superblock.directory_table else {
        raise Invalid(block, "invalid directory")
      }
      Directory(self.superblock.directory_table + start, off, size - 3)
    }
    2 | 9 => {
      let long = kind == 9
      let head = if long { 56 } else { 32 }
      let b = self.metadata(block, offset, head)
      let start = if long {
        index(u64(b, 16), block)
      } else {
        index(u32(b, 16).to_uint64(), block)
      }
      let size = if long {
        index(u64(b, 24), block)
      } else {
        index(u32(b, 28).to_uint64(), block)
      }
      let fragment = u32(b, if long { 44 } else { 20 })
      let fragment_offset = index(
        u32(b, if long { 48 } else { 24 }).to_uint64(),
        block,
      )
      if long {
        links = index(u32(b, 40).to_uint64(), block)
        xattr = u32(b, 52)
      }
      let bs = self.superblock.block_size
      let count = size / bs +
        (if fragment == 0xffffffffU && size % bs != 0 { 1 } else { 0 })
      guard count <= 1048576 else { raise Limit("file block list") }
      let all = self.metadata(block, offset, head + count * 4)
      let blocks = Array::makei(count, i => u32(all, head + i * 4))
      File(start, size, fragment, fragment_offset, blocks)
    }
    3 | 10 => {
      let b = self.metadata(block, offset, 24)
      links = index(u32(b, 16).to_uint64(), block)
      let len = index(u32(b, 20).to_uint64(), block)
      guard len > 0 && len <= 65536 else { raise Limit("symlink length") }
      if kind == 10 {
        xattr = u32(self.metadata(block, offset, 28 + len), 24 + len)
      }
      Symlink(region(self.metadata(block, offset, 24 + len), 24, len))
    }
    4 | 5 | 11 | 12 => {
      let b = self.metadata(block, offset, 24)
      links = index(u32(b, 16).to_uint64(), block)
      if kind > 7 {
        xattr = u32(self.metadata(block, offset, 28), 24)
      }
      Device(u32(b, 20))
    }
    6 | 7 | 13 | 14 => {
      let b = self.metadata(block, offset, 20)
      links = index(u32(b, 16).to_uint64(), block)
      if kind > 7 {
        xattr = u32(self.metadata(block, offset, 24), 20)
      }
      Special
    }
    _ => raise Invalid(block, "unknown inode type \{kind}")
  }
  {
    reference,
    number,
    kind,
    mode: u16(base, 2),
    uid,
    gid,
    mtime: u32(base, 8),
    links,
    xattr,
    parent,
    content,
  }
}

///|
pub fn Image::root(self : Image) -> Inode raise SquashError {
  self.inode(self.superblock.root)
}

///|
pub fn Image::entries(
  self : Image,
  dir : Inode,
) -> Array[DirEntry] raise SquashError {
  guard dir.content is Directory(block, offset, size) else {
    raise Invalid(dir.number, "not a directory")
  }
  let b = self.metadata(block, offset, size)
  let entries : Array[DirEntry] = []
  let names : Map[Bytes, Bool] = Map([])
  let mut pos = 0
  while pos < b.length() {
    let count_minus_one = u32(b, pos)
    guard count_minus_one <= 255U else {
      raise Invalid(pos, "directory group exceeds 256 entries")
    }
    let count = count_minus_one.reinterpret_as_int() + 1
    let start = u32(b, pos + 4)
    let base_number = index(u32(b, pos + 8).to_uint64(), pos)
    pos += 12
    for _ in 0..= 32768 { delta - 65536 } else { delta }
      let kind = u16(b, pos + 4)
      let len = u16(b, pos + 6) + 1
      guard len <= 256 && inode_offset < 8192 && kind >= 1 && kind <= 7 else {
        raise Invalid(pos, "invalid directory entry")
      }
      let name = region(b, pos + 8, len)
      guard name != b"." &&
        name != b".." &&
        !name.iter().any(c => c == b'/' || c == 0) else {
        raise Invalid(pos, "unsafe directory name")
      }
      guard !names.contains(name) else {
        raise Invalid(pos, "duplicate directory name")
      }
      names[name] = true
      let number64 = base_number.to_int64() + delta.to_int64()
      guard number64 > 0L && number64 <= self.superblock.inodes.to_int64() else {
        raise Invalid(pos, "directory inode number")
      }
      entries.push({
        name,
        reference: (start.to_uint64() << 16) | inode_offset.to_uint64(),
        number: number64.to_int(),
        kind,
      })
      pos += 8 + len
    }
  }
  entries
}

///|
pub fn Image::lookup(self : Image, path : String) -> Inode raise SquashError {
  let mut node = self.root()
  if path == "/" {
    return node
  }
  guard path.has_prefix("/") else { raise Invalid(0, "path must be absolute") }
  let parts = path.split("/").to_array()
  for i in 1..