///|
fn datatype_contains_vlen(datatype : Datatype) -> Bool {
  if datatype.class == 9 {
    return true
  }
  for field in datatype.compound_members {
    if datatype_contains_vlen(field.datatype) {
      return true
    }
  }
  for base in datatype.base_types {
    if datatype_contains_vlen(base) {
      return true
    }
  }
  false
}

///|
fn dataset_fill_value(
  header : ObjectHeader,
  datatype : Datatype,
) -> Bytes raise Hdf5Error {
  let message = match header.first(msg_fill_value) {
    Some(message) => message
    None =>
      match header.first(msg_fill_value_old) {
        Some(message) => message
        None => return b""
      }
  }
  guard (message.flags.to_int() & 0x02) == 0 else {
    raise UnsupportedFeature("shared fill value messages are not supported yet")
  }
  let reader = Reader::new(message.data)
  let mut fill_enabled = true
  if message.msg_type == msg_fill_value {
    let version = reader.read_u8().to_int()
    let (allocation, writing, defined, has_value) = match version {
      1 | 2 => {
        let allocation = reader.read_u8().to_int()
        let writing = reader.read_u8().to_int()
        let defined = reader.read_u8().to_int()
        guard defined <= 1 else {
          raise InvalidField("invalid fill value definition")
        }
        (allocation, writing, defined == 1, defined == 1)
      }
      3 => {
        let flags = reader.read_u8().to_int()
        guard (flags & 0xC0) == 0 && (flags & 0x30) != 0x30 else {
          raise InvalidField("invalid fill value flags")
        }
        (flags & 3, (flags >> 2) & 3, (flags & 0x10) == 0, (flags & 0x20) != 0)
      }
      _ => raise UnsupportedFeature("unsupported fill value message version")
    }
    guard allocation >= 1 && allocation <= 3 && writing <= 2 else {
      raise InvalidField("invalid fill value allocation or writing policy")
    }
    // HDF5 reads unallocated storage as zero when no fill value is defined or
    // when the fill value is not written. Keep parsing an explicitly stored
    // payload so malformed messages are still rejected, but do not use it.
    fill_enabled = defined && writing != 1
    if !has_value {
      return b""
    }
  }
  let size = reader.read_u32_le().reinterpret_as_int()
  guard size >= 0 else { raise InvalidField("invalid fill value size") }
  if size == 0 {
    return b""
  }
  guard size == datatype.size else {
    raise InvalidField("fill value size does not match datatype")
  }
  let fill = reader.read_bytes(size)
  if fill_enabled {
    fill
  } else {
    b""
  }
}

///|
fn filled_payload(
  count : Int64,
  size : Int32,
  fill : Bytes,
) -> FixedArray[Byte] raise Hdf5Error {
  guard count >= 0 && size > 0 && count <= 0x7FFFFFFFL / size.to_int64() else {
    raise InvalidField("dataset byte size is too large")
  }
  guard fill.length() == 0 || fill.length() == size else {
    raise InvalidField("fill value size does not match datatype")
  }
  let output = FixedArray::make(count.to_int() * size, b'\x00')
  if fill.length() > 0 {
    for element in 0..