///|
/// Registered and extension disposition kinds from HTTP Content-Disposition.
pub(all) enum DispositionKind {
Inline
Attachment
FormData
Other(String)
} derive(Eq, Debug)
///|
/// Diagnostic severity used by parser and filename policy checks.
pub(all) enum Severity {
Note
Warning
Error
} derive(Eq, Debug)
///|
/// Overall result produced by header linting.
pub(all) enum ReviewStatus {
Accepted
NeedsAttention
Rejected
} derive(Eq, Debug)
///|
/// Stable machine-readable diagnostic codes.
pub(all) enum DiagnosticCode {
EmptyHeader
InvalidDisposition
InvalidParameterName
MissingEquals
MissingValue
UnterminatedQuote
BadEscape
DuplicateParameter
InvalidPercentEncoding
InvalidExtendedValue
UnsupportedCharset
UnsafeFilename
PathSegmentStripped
ReservedName
EmptyFilename
LengthReduced
ControlCharacter
} derive(Eq, Debug)
///|
/// Stable machine-readable recommendation codes.
pub(all) enum RecommendationCode {
PreferFilenameStar
AddAsciiFallback
SanitizeFilename
RemoveDuplicateParameter
FixSyntax
UseSupportedCharset
AddMultipartName
AvoidEmptyFilename
LimitFilenameLength
PreserveExplicitDisposition
ReviewFileExtension
AvoidExecutableDownload
PreferAttachmentDisposition
InferFilename
} derive(Eq, Debug)
///|
/// A parser or policy diagnostic with optional UTF-16 offset in the input.
pub(all) struct Diagnostic {
code : DiagnosticCode
severity : Severity
message : String
offset : Int?
} derive(Eq, Debug)
///|
/// Human-oriented recommendation produced by linting.
pub(all) struct Recommendation {
code : RecommendationCode
severity : Severity
message : String
parameter : String?
value : String?
offset : Int?
} derive(Eq, Debug)
///|
/// Fatal parse error returned by strict entry points.
pub(all) struct ParseError {
code : DiagnosticCode
message : String
offset : Int?
} derive(Eq, Debug)
///|
/// A single Content-Disposition parameter.
///
/// `name` is normalized to lowercase and has the RFC 5987 `*` suffix removed.
/// `extended` records whether the parameter came from `name*=...`.
pub(all) struct Parameter {
name : String
value : String
raw_name : String
raw_value : String
extended : Bool
charset : String?
language : String?
position : Int
} derive(Eq, Debug)
///|
/// Parsed header plus recoverable diagnostics.
pub(all) struct Disposition {
kind : DispositionKind
params : Array[Parameter]
diagnostics : Array[Diagnostic]
} derive(Eq, Debug)
///|
/// Result of filename normalization.
pub(all) struct FilenameReport {
value : String
original : String
changed : Bool
diagnostics : Array[Diagnostic]
} derive(Eq, Debug)
///|
/// Policy for `sanitize_filename_with_policy`.
pub(all) struct FilenamePolicy {
replacement : String
max_length : Int
default_name : String
allow_hidden : Bool
} derive(Eq, Debug)
///|
/// Lint result for one Content-Disposition header.
pub(all) struct HeaderReview {
header : String
disposition : Disposition?
filename : FilenameReport?
recommendations : Array[Recommendation]
status : ReviewStatus
} derive(Eq, Debug)
///|
/// Aggregate counters for a batch of header reviews.
pub(all) struct AuditSummary {
total : Int
accepted : Int
needs_attention : Int
rejected : Int
warnings : Int
errors : Int
filenames_changed : Int
} derive(Eq, Debug)
///|
/// Policy used to convert a batch review into a CI or gateway decision.
pub(all) struct AuditGatePolicy {
max_warnings : Int
max_errors : Int
allow_needs_attention : Bool
allow_filename_changes : Bool
allow_rejected_headers : Bool
} derive(Eq, Debug)
///|
/// Decision produced by applying an `AuditGatePolicy` to review results.
pub(all) struct AuditGateResult {
accepted : Bool
status : ReviewStatus
summary : AuditSummary
reasons : Array[String]
} derive(Eq, Debug)
///|
pub fn FilenamePolicy::default() -> FilenamePolicy {
{
replacement: "_",
max_length: 120,
default_name: "download",
allow_hidden: false,
}
}
///|
pub fn FilenamePolicy::strict_download() -> FilenamePolicy {
{
replacement: "_",
max_length: 96,
default_name: "download",
allow_hidden: false,
}
}
///|
pub fn FilenamePolicy::object_storage_key() -> FilenamePolicy {
{
replacement: "-",
max_length: 180,
default_name: "object",
allow_hidden: true,
}
}
///|
pub fn FilenamePolicy::multipart_upload() -> FilenamePolicy {
{
replacement: "_",
max_length: 128,
default_name: "upload",
allow_hidden: false,
}
}
///|
pub fn AuditGatePolicy::strict() -> AuditGatePolicy {
{
max_warnings: 0,
max_errors: 0,
allow_needs_attention: false,
allow_filename_changes: false,
allow_rejected_headers: false,
}
}
///|
pub fn AuditGatePolicy::download_gateway() -> AuditGatePolicy {
{
max_warnings: 8,
max_errors: 0,
allow_needs_attention: true,
allow_filename_changes: true,
allow_rejected_headers: false,
}
}
///|
pub fn AuditGatePolicy::report_only() -> AuditGatePolicy {
{
max_warnings: 1000000,
max_errors: 1000000,
allow_needs_attention: true,
allow_filename_changes: true,
allow_rejected_headers: true,
}
}
///|
pub fn AuditGatePolicy::with_max_warnings(
self : AuditGatePolicy,
max_warnings : Int,
) -> AuditGatePolicy {
{ ..self, max_warnings, }
}
///|
pub fn AuditGatePolicy::with_max_errors(
self : AuditGatePolicy,
max_errors : Int,
) -> AuditGatePolicy {
{ ..self, max_errors, }
}
///|
pub fn AuditGatePolicy::with_needs_attention(
self : AuditGatePolicy,
allowed : Bool,
) -> AuditGatePolicy {
{ ..self, allow_needs_attention: allowed }
}
///|
pub fn AuditGatePolicy::with_filename_changes(
self : AuditGatePolicy,
allowed : Bool,
) -> AuditGatePolicy {
{ ..self, allow_filename_changes: allowed }
}
///|
pub fn AuditGatePolicy::with_rejected_headers(
self : AuditGatePolicy,
allowed : Bool,
) -> AuditGatePolicy {
{ ..self, allow_rejected_headers: allowed }
}
///|
pub fn Diagnostic::make(
code : DiagnosticCode,
severity : Severity,
message : String,
offset : Int?,
) -> Diagnostic {
{ code, severity, message, offset }
}
///|
pub fn Recommendation::make(
code : RecommendationCode,
severity : Severity,
message : String,
parameter : String?,
value : String?,
offset : Int?,
) -> Recommendation {
{ code, severity, message, parameter, value, offset }
}
///|
pub fn ParseError::from_diagnostic(d : Diagnostic) -> ParseError {
{ code: d.code, message: d.message, offset: d.offset }
}
///|
pub fn Parameter::plain(name : String, value : String) -> Parameter {
{
name: normalize_param_name(name),
value,
raw_name: name,
raw_value: value,
extended: false,
charset: None,
language: None,
position: 0,
}
}
///|
pub fn Parameter::extended(
name : String,
value : String,
charset? : String = "UTF-8",
language? : String = "",
) -> Parameter {
{
name: normalize_param_name(name),
value,
raw_name: name + "*",
raw_value: encode_ext_value(value, charset~, language~),
extended: true,
charset: Some(charset),
language: Some(language),
position: 0,
}
}
///|
pub fn DispositionKind::from_token(token : String) -> DispositionKind {
let lower = token.to_lower()
if lower == "inline" {
Inline
} else if lower == "attachment" {
Attachment
} else if lower == "form-data" {
FormData
} else {
Other(lower)
}
}
///|
pub fn DispositionKind::to_token(self : DispositionKind) -> String {
match self {
Inline => "inline"
Attachment => "attachment"
FormData => "form-data"
Other(name) => name
}
}
///|
pub fn DiagnosticCode::name(self : DiagnosticCode) -> String {
match self {
EmptyHeader => "empty-header"
InvalidDisposition => "invalid-disposition"
InvalidParameterName => "invalid-parameter-name"
MissingEquals => "missing-equals"
MissingValue => "missing-value"
UnterminatedQuote => "unterminated-quote"
BadEscape => "bad-escape"
DuplicateParameter => "duplicate-parameter"
InvalidPercentEncoding => "invalid-percent-encoding"
InvalidExtendedValue => "invalid-extended-value"
UnsupportedCharset => "unsupported-charset"
UnsafeFilename => "unsafe-filename"
PathSegmentStripped => "path-segment-stripped"
ReservedName => "reserved-name"
EmptyFilename => "empty-filename"
LengthReduced => "length-reduced"
ControlCharacter => "control-character"
}
}
///|
pub fn RecommendationCode::name(self : RecommendationCode) -> String {
match self {
PreferFilenameStar => "prefer-filename-star"
AddAsciiFallback => "add-ascii-fallback"
SanitizeFilename => "sanitize-filename"
RemoveDuplicateParameter => "remove-duplicate-parameter"
FixSyntax => "fix-syntax"
UseSupportedCharset => "use-supported-charset"
AddMultipartName => "add-multipart-name"
AvoidEmptyFilename => "avoid-empty-filename"
LimitFilenameLength => "limit-filename-length"
PreserveExplicitDisposition => "preserve-explicit-disposition"
ReviewFileExtension => "review-file-extension"
AvoidExecutableDownload => "avoid-executable-download"
PreferAttachmentDisposition => "prefer-attachment-disposition"
InferFilename => "infer-filename"
}
}
///|
pub fn Severity::name(self : Severity) -> String {
match self {
Note => "note"
Warning => "warning"
Error => "error"
}
}
///|
pub fn ReviewStatus::name(self : ReviewStatus) -> String {
match self {
Accepted => "accepted"
NeedsAttention => "needs-attention"
Rejected => "rejected"
}
}
///|
pub fn Disposition::get_param(self : Disposition, name : String) -> String? {
self.find_param(name).map(p => p.value)
}
///|
pub fn Disposition::find_param(self : Disposition, name : String) -> Parameter? {
let wanted = normalize_param_name(name)
let mut plain : Parameter? = None
for p in self.params {
if p.name == wanted {
if p.extended {
return Some(p)
}
if plain is None {
plain = Some(p)
}
}
}
plain
}
///|
pub fn Disposition::plain_filename(self : Disposition) -> String? {
for p in self.params {
if p.name == "filename" && !p.extended {
return Some(p.value)
}
}
None
}
///|
pub fn Disposition::extended_filename(self : Disposition) -> String? {
for p in self.params {
if p.name == "filename" && p.extended {
return Some(p.value)
}
}
None
}
///|
pub fn Disposition::params_named(
self : Disposition,
name : String,
) -> Array[Parameter] {
let wanted = normalize_param_name(name)
let found : Array[Parameter] = []
for p in self.params {
if p.name == wanted {
found.push(p)
}
}
found
}
///|
pub fn Disposition::has_param(self : Disposition, name : String) -> Bool {
self.find_param(name) is Some(_)
}
///|
pub fn Disposition::is_attachment(self : Disposition) -> Bool {
self.kind == Attachment
}
///|
pub fn Disposition::is_inline(self : Disposition) -> Bool {
self.kind == Inline
}
///|
pub fn Disposition::is_form_data(self : Disposition) -> Bool {
self.kind == FormData
}
///|
pub fn Disposition::filename(self : Disposition) -> String? {
self.get_param("filename")
}
///|
pub fn Disposition::name(self : Disposition) -> String? {
self.get_param("name")
}
///|
pub fn Disposition::has_errors(self : Disposition) -> Bool {
self.diagnostics.any(d => d.severity == Error)
}
///|
pub fn Disposition::to_header(self : Disposition) -> String {
serialize(self)
}
///|
pub fn Parameter::is_filename(self : Parameter) -> Bool {
self.name == "filename"
}
///|
pub fn Parameter::is_name(self : Parameter) -> Bool {
self.name == "name"
}
///|
pub fn Parameter::effective_name(self : Parameter) -> String {
if self.extended {
self.name + "*"
} else {
self.name
}
}
///|
pub fn normalize_param_name(name : String) -> String {
let trimmed = name.trim(chars=" \t").to_owned()
let without_star = if trimmed.has_suffix("*") && trimmed.length() > 0 {
trimmed[0:trimmed.length() - 1].to_owned()
} else {
trimmed
}
without_star.to_lower()
}