///|
pub(all) enum ClientProfile {
ModernHttpClient
LegacyHttpClient
BrowserDownload
MultipartFormParser
ObjectStorageGateway
} derive(Eq, Debug)
///|
pub(all) struct CompatibilityReport {
profile : ClientProfile
header : String
compatible : Bool
recommendations : Array[Recommendation]
} derive(Eq, Debug)
///|
pub fn ClientProfile::name(self : ClientProfile) -> String {
match self {
ModernHttpClient => "modern-http-client"
LegacyHttpClient => "legacy-http-client"
BrowserDownload => "browser-download"
MultipartFormParser => "multipart-form-parser"
ObjectStorageGateway => "object-storage-gateway"
}
}
///|
pub fn check_compatibility(
header : String,
profile : ClientProfile,
) -> CompatibilityReport {
let recommendations : Array[Recommendation] = []
match parse_header(header) {
Ok(disposition) => {
append_profile_recommendations(recommendations, disposition, profile)
{
profile,
header,
compatible: !recommendations.any(r => r.severity == Error),
recommendations,
}
}
Err(err) => {
recommendations.push(
Recommendation::make(
FixSyntax,
Error,
err.message,
None,
None,
err.offset,
),
)
{ profile, header, compatible: false, recommendations }
}
}
}
///|
pub fn compatibility_matrix(header : String) -> Array[CompatibilityReport] {
[
check_compatibility(header, ModernHttpClient),
check_compatibility(header, LegacyHttpClient),
check_compatibility(header, BrowserDownload),
check_compatibility(header, MultipartFormParser),
check_compatibility(header, ObjectStorageGateway),
]
}
///|
pub fn CompatibilityReport::status(self : CompatibilityReport) -> ReviewStatus {
if self.recommendations.any(r => r.severity == Error) {
Rejected
} else if self.recommendations.any(r => r.severity == Warning) {
NeedsAttention
} else {
Accepted
}
}
///|
pub fn CompatibilityReport::to_text(self : CompatibilityReport) -> String {
let buf = StringBuilder(size_hint=self.header.length() + 96)
buf.write_string(self.profile.name())
buf.write_string(": ")
buf.write_string(self.status().name())
if self.recommendations.length() > 0 {
for r in self.recommendations {
buf.write_char('\n')
buf.write_string("- ")
buf.write_string(r.to_text())
}
}
buf.to_string()
}
///|
pub fn compatibility_matrix_to_text(
reports : Array[CompatibilityReport],
) -> String {
let buf = StringBuilder(size_hint=256)
let mut first = true
for report in reports {
if first {
first = false
} else {
buf.write_string("\n\n")
}
buf.write_string(report.to_text())
}
buf.to_string()
}
///|
fn append_profile_recommendations(
recommendations : Array[Recommendation],
disposition : Disposition,
profile : ClientProfile,
) -> Unit {
append_diagnostic_recommendations(recommendations, disposition.diagnostics)
match profile {
ModernHttpClient =>
append_modern_recommendations(recommendations, disposition)
LegacyHttpClient =>
append_legacy_recommendations(recommendations, disposition)
BrowserDownload =>
append_browser_download_recommendations(recommendations, disposition)
MultipartFormParser =>
append_multipart_recommendations(recommendations, disposition)
ObjectStorageGateway =>
append_object_storage_recommendations(recommendations, disposition)
}
}
///|
fn append_modern_recommendations(
recommendations : Array[Recommendation],
disposition : Disposition,
) -> Unit {
match disposition.filename() {
Some(value) =>
if has_non_ascii(value) && disposition.extended_filename() is None {
recommendations.push(
Recommendation::make(
PreferFilenameStar,
Warning,
"Modern HTTP clients should receive filename* for non-ASCII names",
Some("filename*"),
Some(value),
None,
),
)
}
None => ()
}
}
///|
fn append_legacy_recommendations(
recommendations : Array[Recommendation],
disposition : Disposition,
) -> Unit {
match disposition.plain_filename() {
Some(value) =>
if has_non_ascii(value) {
recommendations.push(
Recommendation::make(
AddAsciiFallback,
Warning,
"Legacy clients need an ASCII filename fallback",
Some("filename"),
Some(filename_ascii_fallback(value)),
None,
),
)
}
None =>
if disposition.extended_filename() is Some(value) {
recommendations.push(
Recommendation::make(
AddAsciiFallback,
Warning,
"Legacy clients may ignore filename* without a plain filename",
Some("filename"),
Some(filename_ascii_fallback(value)),
None,
),
)
}
}
}
///|
fn append_browser_download_recommendations(
recommendations : Array[Recommendation],
disposition : Disposition,
) -> Unit {
if !disposition.is_attachment() {
recommendations.push(
Recommendation::make(
PreferAttachmentDisposition,
Warning,
"Browser download flows should use attachment disposition",
None,
Some("attachment"),
None,
),
)
}
append_modern_recommendations(recommendations, disposition)
}
///|
fn append_multipart_recommendations(
recommendations : Array[Recommendation],
disposition : Disposition,
) -> Unit {
if !disposition.is_form_data() {
recommendations.push(
Recommendation::make(
PreserveExplicitDisposition,
Error,
"Multipart parsers expect form-data disposition",
None,
Some("form-data"),
None,
),
)
}
if !disposition.has_param("name") {
recommendations.push(
Recommendation::make(
AddMultipartName,
Error,
"Multipart form-data parts require a name parameter",
Some("name"),
None,
None,
),
)
}
}
///|
fn append_object_storage_recommendations(
recommendations : Array[Recommendation],
disposition : Disposition,
) -> Unit {
match disposition.filename() {
Some(value) => {
let report = sanitize_object_key(value)
if report.changed {
recommendations.push(
Recommendation::make(
SanitizeFilename,
Warning,
"Object storage keys should be normalized before persistence",
Some("filename"),
Some(report.value),
None,
),
)
}
}
None => ()
}
if disposition.diagnostics.any(d => d.severity == Error) {
recommendations.push(
Recommendation::make(
FixSyntax,
Error,
"Header has syntax or decoding errors",
None,
None,
None,
),
)
}
}