///|
/// Validate typed constraints as well as JSON-decoded ones. Public callers can
/// construct AssetSpec directly, so scan_assets must not rely on the parser.
fn asset_spec_error(spec : AssetSpec) -> String? {
  if spec.max_bytes <= 0 {
    return Some("max_bytes 必须为正整数")
  }
  let dimensions_are_valid = match (spec.width, spec.height) {
    (Some(width), Some(height)) => width > 0 && height > 0
    (None, None) => true
    _ => false
  }
  if !dimensions_are_valid {
    return Some("width 和 height 必须同时省略或同时为正整数")
  }
  match spec.min_bytes {
    Some(minimum) if minimum <= 0 => return Some("min_bytes 必须为正整数")
    Some(minimum) if minimum > spec.max_bytes =>
      return Some("min_bytes 不能大于 max_bytes")
    _ => ()
  }
  match spec.expected_sha256 {
    Some(expected) => {
      if expected.length() != 64 {
        return Some("expected_sha256 必须是 64 位十六进制字符串")
      }
      for character in expected {
        if !is_hex_character(character) {
          return Some("expected_sha256 必须是 64 位十六进制字符串")
        }
      }
    }
    None => ()
  }
  match spec.aspect {
    Some(aspect) if parse_aspect(aspect) is None =>
      return Some("aspect 必须是 W:H 形式的正整数比例")
    _ => ()
  }
  match spec.format {
    Some(format) if canonical_format(format) is None =>
      return Some(
        "format 必须是受支持的格式名(如 png、jpeg、webp、svg)",
      )
    _ => ()
  }
  None
}