///|
fn validate_source_limits(options : Options) -> Unit raise PackError {
  if options.max_file_size <= 0 ||
    options.max_files < 0 ||
    options.max_total_size < 0 ||
    options.max_total_size > 9007199254740991L {
    raise ConfigurationError
  }
}

///|
priv struct ScreenedRead {
  result : @input.ReadResult
  findings : Array[Finding]
  read_bytes : Int64
}

///|
// One source of truth for pack and query-level explanation. No body is returned
// for skipped input; read accounting includes bytes rejected by later stages.
async fn read_screened(
  candidate : @selection.Candidate,
  options : Options,
  remaining : Int64,
) -> ScreenedRead {
  let metered = @input.read_validated_metered(
    candidate.path,
    options.max_file_size,
    remaining,
  ) catch {
    @input.TotalSizeLimitExceeded =>
      raise InputLimitExceeded(
        kind="maxTotalSize",
        limit=options.max_total_size,
      )
    err => raise err
  }
  let validated = match metered.result {
    Loaded(snapshot) => snapshot
    Skipped(reason) =>
      return {
        result: @input.Skipped(reason),
        findings: [],
        read_bytes: metered.read_bytes,
      }
  }
  let snapshot = validated.snapshot()
  if options.security_check {
    let findings = @security.scan_validated(
      candidate.relative,
      validated.validated_text(),
    ) catch {
      _ => raise ScanError
    }
    if !findings.is_empty() {
      return {
        result: @input.Skipped("SecurityFinding"),
        findings,
        read_bytes: metered.read_bytes,
      }
    }
  }
  if options.style is Xml &&
    (
      !@output.xml_representable(candidate.relative) ||
      !@output.xml_representable(snapshot.text)
    ) {
    return {
      result: @input.Skipped("UnrepresentableText"),
      findings: [],
      read_bytes: metered.read_bytes,
    }
  }
  {
    result: @input.Loaded(snapshot),
    findings: [],
    read_bytes: metered.read_bytes,
  }
}