///|
pub fn lint_header(header : String) -> HeaderReview {
lint_header_with_policy(header, FilenamePolicy::default())
}
///|
pub fn lint_header_with_policy(
header : String,
policy : FilenamePolicy,
) -> HeaderReview {
let recommendations : Array[Recommendation] = []
match parse_header(header) {
Ok(disposition) => {
append_diagnostic_recommendations(
recommendations,
disposition.diagnostics,
)
append_shape_recommendations(recommendations, disposition)
let filename = match disposition.filename() {
Some(value) => {
let report = sanitize_filename_with_policy(value, policy)
append_filename_recommendations(recommendations, report)
Some(report)
}
None => None
}
{
header,
disposition: Some(disposition),
filename,
recommendations,
status: status_for_recommendations(recommendations),
}
}
Err(err) => {
recommendations.push(
Recommendation::make(
FixSyntax,
Error,
err.message,
None,
None,
err.offset,
),
)
{
header,
disposition: None,
filename: None,
recommendations,
status: Rejected,
}
}
}
}
///|
pub fn lint_headers(headers : Array[String]) -> Array[HeaderReview] {
let reviews : Array[HeaderReview] = []
for h in headers {
reviews.push(lint_header(h))
}
reviews
}
///|
pub fn lint_headers_with_policy(
headers : Array[String],
policy : FilenamePolicy,
) -> Array[HeaderReview] {
let reviews : Array[HeaderReview] = []
for h in headers {
reviews.push(lint_header_with_policy(h, policy))
}
reviews
}
///|
pub fn summarize_reviews(reviews : Array[HeaderReview]) -> AuditSummary {
let mut accepted = 0
let mut needs_attention = 0
let mut rejected = 0
let mut warnings = 0
let mut errors = 0
let mut filenames_changed = 0
for review in reviews {
match review.status {
Accepted => accepted = accepted + 1
NeedsAttention => needs_attention = needs_attention + 1
Rejected => rejected = rejected + 1
}
warnings = warnings + review.warning_count()
errors = errors + review.error_count()
match review.filename {
Some(report) =>
if report.changed {
filenames_changed = filenames_changed + 1
}
None => ()
}
}
{
total: reviews.length(),
accepted,
needs_attention,
rejected,
warnings,
errors,
filenames_changed,
}
}
///|
pub fn HeaderReview::is_ok(self : HeaderReview) -> Bool {
self.status == Accepted
}
///|
pub fn HeaderReview::warning_count(self : HeaderReview) -> Int {
count_recommendations_by_severity(self.recommendations, Warning)
}
///|
pub fn HeaderReview::error_count(self : HeaderReview) -> Int {
count_recommendations_by_severity(self.recommendations, Error)
}
///|
pub fn HeaderReview::filename_value(self : HeaderReview) -> String? {
match self.filename {
Some(report) => Some(report.value)
None => None
}
}
///|
pub fn HeaderReview::disposition_kind(self : HeaderReview) -> String? {
match self.disposition {
Some(d) => Some(d.kind.to_token())
None => None
}
}
///|
fn append_diagnostic_recommendations(
recommendations : Array[Recommendation],
diagnostics : Array[Diagnostic],
) -> Unit {
for d in diagnostics {
recommendations.push(recommendation_from_diagnostic(d))
}
}
///|
fn append_shape_recommendations(
recommendations : Array[Recommendation],
disposition : Disposition,
) -> Unit {
if disposition.is_form_data() && !disposition.has_param("name") {
recommendations.push(
Recommendation::make(
AddMultipartName,
Warning,
"Multipart form-data should include a name parameter",
Some("name"),
None,
None,
),
)
}
match disposition.filename() {
Some(value) => {
if has_non_ascii(value) && disposition.extended_filename() is None {
recommendations.push(
Recommendation::make(
PreferFilenameStar,
Warning,
"Non-ASCII filenames should include an RFC 5987 filename* parameter",
Some("filename*"),
Some(value),
None,
),
)
}
if disposition.plain_filename() is None &&
disposition.extended_filename() is Some(_) {
recommendations.push(
Recommendation::make(
AddAsciiFallback,
Note,
"An ASCII filename fallback improves compatibility with older clients",
Some("filename"),
Some(filename_ascii_fallback(value)),
None,
),
)
}
}
None => ()
}
}
///|
fn append_filename_recommendations(
recommendations : Array[Recommendation],
report : FilenameReport,
) -> Unit {
if report.value.is_empty() {
recommendations.push(
Recommendation::make(
AvoidEmptyFilename,
Error,
"Filename is empty after normalization",
Some("filename"),
None,
None,
),
)
} else if report.changed {
recommendations.push(
Recommendation::make(
SanitizeFilename,
Warning,
"Filename should be normalized before writing to disk",
Some("filename"),
Some(report.value),
None,
),
)
}
for d in report.diagnostics {
if d.code == LengthReduced {
recommendations.push(
Recommendation::make(
LimitFilenameLength,
Warning,
d.message,
Some("filename"),
Some(report.value),
d.offset,
),
)
}
}
}
///|
fn recommendation_from_diagnostic(d : Diagnostic) -> Recommendation {
let code = match d.code {
DuplicateParameter => RemoveDuplicateParameter
UnsupportedCharset => UseSupportedCharset
UnsafeFilename => SanitizeFilename
PathSegmentStripped => SanitizeFilename
ReservedName => SanitizeFilename
EmptyFilename => AvoidEmptyFilename
LengthReduced => LimitFilenameLength
_ => FixSyntax
}
Recommendation::make(code, d.severity, d.message, None, None, d.offset)
}
///|
fn count_recommendations_by_severity(
recommendations : Array[Recommendation],
severity : Severity,
) -> Int {
let mut count = 0
for r in recommendations {
if r.severity == severity {
count = count + 1
}
}
count
}
///|
fn status_for_recommendations(
recommendations : Array[Recommendation],
) -> ReviewStatus {
let mut has_warning = false
for r in recommendations {
if r.severity == Error {
return Rejected
}
if r.severity == Warning {
has_warning = true
}
}
if has_warning {
NeedsAttention
} else {
Accepted
}
}
///|
fn has_non_ascii(value : String) -> Bool {
value.any(c => !c.is_ascii())
}