///|
pub(all) struct VerifyReport {
  entries : Int
  inodes : Int
  files : Int
  bytes_checked : UInt64
  xattrs : Int
  fragments : Int
  export_checked : Bool
} derive(Debug)

///|
/// Validate the reachable filesystem, IDs, fragments, export index and xattrs.
/// Fails on the first inconsistency; decompression errors propagate to the caller.
pub fn Image::verify(
  self : Image,
  max_bytes? : UInt64 = 4294967296UL,
) -> VerifyReport raise SquashError {
  let tree = self.tree()
  self.validate_layout(tree)
  self.validate_data_extents(tree)
  let unique : Map[Int, Inode] = Map([])
  let counts : Map[Int, Int] = Map([])
  let mut files = 0
  let mut bytes = 0UL
  let mut xattrs = 0
  let directories : Map[String, Int] = Map([])
  for e in tree {
    if e.inode.content is Directory(_, _, _) {
      directories[e.path] = e.inode.number
    }
  }
  for entry in tree {
    let node = entry.inode
    guard node.mode <= 4095 && node.links > 0 else {
      raise Invalid(node.number, "mode or link count")
    }
    counts[node.number] = counts.get(node.number).unwrap_or(0) + 1
    if unique.get(node.number) is Some(previous) {
      guard previous.reference == node.reference &&
        !(node.content is Directory(_, _, _)) else {
        raise Invalid(node.number, "inode alias or directory hard link")
      }
      continue
    }
    unique[node.number] = node
    xattrs += self.xattrs(node).length()
    match node.content {
      Directory(_, _, _) => {
        if entry.path == "/" {
          guard node.parent == node.number ||
            node.parent == self.superblock.inodes + 1 else {
            raise Invalid(node.number, "root parent")
          }
        } else {
          let parts = entry.path.split("/").map(p => p.to_owned()).to_array()
          ignore(parts.pop())
          let parent = if parts.length() == 1 { "/" } else { parts.join("/") }
          guard directories.get(parent) is Some(number) && number == node.parent else {
            raise Invalid(node.number, "directory parent mismatch")
          }
        }
        let children = self.entries(node)
        let mut previous : Bytes? = None
        let mut directories = 0
        for child in children {
          if previous is Some(name) {
            guard name.lexical_compare(child.name) < 0 else {
              raise Invalid(node.number, "directory not byte sorted")
            }
          }
          previous = Some(child.name)
          if child.kind == 1 {
            directories += 1
          }
        }
        guard node.links == directories + 2 else {
          raise Invalid(node.number, "directory link count")
        }
      }
      File(_, size, frag, off, _) => {
        guard size.to_uint64() <= max_bytes &&
          bytes <= max_bytes - size.to_uint64() else {
          raise Limit("verification decoded data budget")
        }
        if frag != 0xffffffffU {
          guard size % self.superblock.block_size != 0 &&
            off < self.superblock.block_size else {
            raise Invalid(node.number, "invalid file fragment")
          }
        }
        let mut at = 0
        while at < size {
          let n = Int::min(self.superblock.block_size, size - at)
          ignore(self.read_range(node, at, n))
          at += n
        }
        bytes += size.to_uint64()
        files += 1
      }
      Symlink(target) => {
        guard !target.iter().any(c => c == 0) else {
          raise Invalid(node.number, "NUL symlink")
        }
      }
      _ => ()
    }
  }
  guard unique.length() == self.superblock.inodes else {
    raise Invalid(4, "unreachable or missing inodes")
  }
  for number, node in unique {
    if !(node.content is Directory(_, _, _)) && node.kind != 2 {
      guard counts[number] == node.links else {
        raise Invalid(number, "hard-link count mismatch")
      }
    }
  }
  for i in 0..