///|
fn fail_at(offset : Int32, message : String) -> Unit raise PeError {
  raise PeError(offset, message)
}

///|
fn ensure_range(
  data : Bytes,
  offset : Int32,
  length : Int32,
) -> Unit raise PeError {
  if offset < 0 || length < 0 || offset > data.length() - length {
    fail_at(offset, "read exceeds the available file data")
  }
}

///|
fn read_u8(data : Bytes, offset : Int32) -> Int32 raise PeError {
  ensure_range(data, offset, 1)
  data[offset].to_int()
}

///|
fn read_u16(data : Bytes, offset : Int32) -> Int32 raise PeError {
  ensure_range(data, offset, 2)
  data[offset].to_int() | (data[offset + 1].to_int() << 8)
}

///|
fn read_u32(data : Bytes, offset : Int32) -> UInt64 raise PeError {
  ensure_range(data, offset, 4)
  data[offset].to_int().to_uint64() |
  (data[offset + 1].to_int().to_uint64() << 8) |
  (data[offset + 2].to_int().to_uint64() << 16) |
  (data[offset + 3].to_int().to_uint64() << 24)
}

///|
fn read_u64(data : Bytes, offset : Int32) -> UInt64 raise PeError {
  ensure_range(data, offset, 8)
  read_u32(data, offset) | (read_u32(data, offset + 4) << 32)
}

///|
fn checked_int(
  value : UInt64,
  offset : Int32,
  field : String,
) -> Int32 raise PeError {
  if value > 0x7fffffffUL {
    fail_at(offset, "\{field} is too large for this tool")
  }
  value.to_int()
}

///|
fn read_ascii_z(
  data : Bytes,
  offset : Int32,
  max_length : Int32,
) -> String raise PeError {
  ensure_range(data, offset, 1)
  let builder = StringBuilder()
  let limit = if max_length < data.length() - offset {
    max_length
  } else {
    data.length() - offset
  }
  let mut index = 0
  while index < limit {
    let byte = data[offset + index].to_int()
    if byte == 0 {
      return builder.to_string()
    }
    if byte >= 0x20 && byte <= 0x7e {
      builder.write_char(byte.unsafe_to_char())
    } else {
      builder.write_char('.')
    }
    index = index + 1
  }
  builder.to_string()
}

///|
fn read_ascii_fixed(
  data : Bytes,
  offset : Int32,
  length : Int32,
) -> String raise PeError {
  ensure_range(data, offset, length)
  let builder = StringBuilder()
  let mut index = 0
  while index < length {
    let byte = data[offset + index].to_int()
    if byte == 0 {
      break
    }
    if byte >= 0x20 && byte <= 0x7e {
      builder.write_char(byte.unsafe_to_char())
    } else {
      builder.write_char('.')
    }
    index = index + 1
  }
  builder.to_string()
}