///|
pub(all) enum FileKind {
NoExtension
TextFile
DocumentFile
SpreadsheetFile
PresentationFile
ImageFile
AudioFile
VideoFile
ArchiveFile
CodeFile
ExecutableFile
FontFile
DataFile
UnknownFile
} derive(Eq, Debug)
///|
pub(all) enum ExtensionRisk {
LowRisk
MediumRisk
HighRisk
UnknownRisk
} derive(Eq, Debug)
///|
pub(all) struct DownloadRequest {
filename : String?
content_type : String?
prefer_inline : Bool
} derive(Eq, Debug)
///|
pub(all) struct PreparedDownload {
header : String
disposition : Disposition
filename : FilenameReport
content_type : String?
extension : String?
file_kind : FileKind
risk : ExtensionRisk
recommendations : Array[Recommendation]
} derive(Eq, Debug)
///|
pub fn DownloadRequest::attachment(filename : String) -> DownloadRequest {
{ filename: Some(filename), content_type: None, prefer_inline: false }
}
///|
pub fn DownloadRequest::inline_preview(
filename : String,
content_type? : String,
) -> DownloadRequest {
{ filename: Some(filename), content_type, prefer_inline: true }
}
///|
pub fn DownloadRequest::from_content_type(
content_type : String,
) -> DownloadRequest {
{ filename: None, content_type: Some(content_type), prefer_inline: false }
}
///|
pub fn prepare_download(request : DownloadRequest) -> PreparedDownload {
let suggested = match request.filename {
Some(name) => name
None => infer_filename_from_content_type(request.content_type)
}
let policy = FilenamePolicy::strict_download()
let filename = sanitize_filename_preserve_extension(suggested, policy)
let extension = filename_extension(filename.value)
let file_kind = classify_filename(filename.value)
let risk = extension_risk(file_kind, extension)
let disposition_kind = if should_allow_inline(
request.prefer_inline,
file_kind,
risk,
) {
Inline
} else {
Attachment
}
let disposition = DispositionBuilder::new(disposition_kind)
.with_filename_policy(suggested, policy)
.build()
let recommendations = prepare_download_recommendations(
request, filename, file_kind, risk, disposition_kind,
)
{
header: disposition.to_header(),
disposition,
filename,
content_type: normalized_content_type_option(request.content_type),
extension,
file_kind,
risk,
recommendations,
}
}
///|
pub fn PreparedDownload::is_inline(self : PreparedDownload) -> Bool {
self.disposition.kind == Inline
}
///|
pub fn PreparedDownload::is_attachment(self : PreparedDownload) -> Bool {
self.disposition.kind == Attachment
}
///|
pub fn PreparedDownload::has_high_risk_extension(
self : PreparedDownload,
) -> Bool {
self.risk == HighRisk
}
///|
pub fn FileKind::name(self : FileKind) -> String {
match self {
NoExtension => "no-extension"
TextFile => "text"
DocumentFile => "document"
SpreadsheetFile => "spreadsheet"
PresentationFile => "presentation"
ImageFile => "image"
AudioFile => "audio"
VideoFile => "video"
ArchiveFile => "archive"
CodeFile => "code"
ExecutableFile => "executable"
FontFile => "font"
DataFile => "data"
UnknownFile => "unknown"
}
}
///|
pub fn ExtensionRisk::name(self : ExtensionRisk) -> String {
match self {
LowRisk => "low"
MediumRisk => "medium"
HighRisk => "high"
UnknownRisk => "unknown"
}
}
///|
pub fn normalize_content_type(content_type : String) -> String {
let base = match content_type.split_once(";") {
Some((first, _)) => first.to_owned()
None => content_type
}
base.trim(chars=" \t\r\n").to_owned().to_lower()
}
///|
pub fn extension_for_content_type(content_type : String) -> String? {
let normalized = normalize_content_type(content_type)
if normalized == "text/plain" {
Some("txt")
} else if normalized == "text/csv" {
Some("csv")
} else if normalized == "text/html" {
Some("html")
} else if normalized == "text/markdown" {
Some("md")
} else if normalized == "application/json" {
Some("json")
} else if normalized == "application/xml" || normalized == "text/xml" {
Some("xml")
} else if normalized == "application/pdf" {
Some("pdf")
} else if normalized == "application/zip" {
Some("zip")
} else if normalized == "application/gzip" {
Some("gz")
} else if normalized == "application/x-tar" {
Some("tar")
} else if normalized == "image/png" {
Some("png")
} else if normalized == "image/jpeg" {
Some("jpg")
} else if normalized == "image/gif" {
Some("gif")
} else if normalized == "image/webp" {
Some("webp")
} else if normalized == "image/svg+xml" {
Some("svg")
} else if normalized == "audio/mpeg" {
Some("mp3")
} else if normalized == "audio/wav" || normalized == "audio/wave" {
Some("wav")
} else if normalized == "audio/ogg" {
Some("ogg")
} else if normalized == "video/mp4" {
Some("mp4")
} else if normalized == "video/webm" {
Some("webm")
} else if normalized == "application/vnd.ms-excel" {
Some("xls")
} else if normalized ==
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" {
Some("xlsx")
} else if normalized == "application/msword" {
Some("doc")
} else if normalized ==
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" {
Some("docx")
} else if normalized == "application/vnd.ms-powerpoint" {
Some("ppt")
} else if normalized ==
"application/vnd.openxmlformats-officedocument.presentationml.presentation" {
Some("pptx")
} else if normalized == "application/wasm" {
Some("wasm")
} else if normalized == "application/octet-stream" {
Some("bin")
} else {
None
}
}
///|
pub fn content_type_for_extension(extension : String) -> String? {
let ext = normalize_extension(extension)
if ext == "txt" {
Some("text/plain")
} else if ext == "csv" {
Some("text/csv")
} else if ext == "html" || ext == "htm" {
Some("text/html")
} else if ext == "md" || ext == "markdown" {
Some("text/markdown")
} else if ext == "json" {
Some("application/json")
} else if ext == "xml" {
Some("application/xml")
} else if ext == "pdf" {
Some("application/pdf")
} else if ext == "zip" {
Some("application/zip")
} else if ext == "gz" {
Some("application/gzip")
} else if ext == "tar" {
Some("application/x-tar")
} else if ext == "png" {
Some("image/png")
} else if ext == "jpg" || ext == "jpeg" {
Some("image/jpeg")
} else if ext == "gif" {
Some("image/gif")
} else if ext == "webp" {
Some("image/webp")
} else if ext == "svg" {
Some("image/svg+xml")
} else if ext == "mp3" {
Some("audio/mpeg")
} else if ext == "wav" {
Some("audio/wav")
} else if ext == "ogg" {
Some("audio/ogg")
} else if ext == "mp4" {
Some("video/mp4")
} else if ext == "webm" {
Some("video/webm")
} else if ext == "xls" {
Some("application/vnd.ms-excel")
} else if ext == "xlsx" {
Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
} else if ext == "doc" {
Some("application/msword")
} else if ext == "docx" {
Some(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
} else if ext == "ppt" {
Some("application/vnd.ms-powerpoint")
} else if ext == "pptx" {
Some(
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
)
} else if ext == "wasm" {
Some("application/wasm")
} else if ext == "bin" {
Some("application/octet-stream")
} else {
None
}
}
///|
pub fn infer_filename_from_content_type(content_type : String?) -> String {
match content_type {
Some(value) =>
match extension_for_content_type(value) {
Some(ext) => "download." + ext
None => "download"
}
None => "download"
}
}
///|
pub fn classify_filename(filename : String) -> FileKind {
match filename_extension(filename) {
Some(ext) => classify_extension(ext)
None => NoExtension
}
}
///|
pub fn classify_extension(extension : String) -> FileKind {
let ext = normalize_extension(extension)
if ext.is_empty() {
NoExtension
} else if is_text_extension(ext) {
TextFile
} else if is_document_extension(ext) {
DocumentFile
} else if is_spreadsheet_extension(ext) {
SpreadsheetFile
} else if is_presentation_extension(ext) {
PresentationFile
} else if is_image_extension(ext) {
ImageFile
} else if is_audio_extension(ext) {
AudioFile
} else if is_video_extension(ext) {
VideoFile
} else if is_archive_extension(ext) {
ArchiveFile
} else if is_code_extension(ext) {
CodeFile
} else if is_executable_extension(ext) {
ExecutableFile
} else if is_font_extension(ext) {
FontFile
} else if is_data_extension(ext) {
DataFile
} else {
UnknownFile
}
}
///|
pub fn extension_risk(kind : FileKind, extension : String?) -> ExtensionRisk {
match kind {
NoExtension => UnknownRisk
TextFile => LowRisk
DocumentFile => MediumRisk
SpreadsheetFile => MediumRisk
PresentationFile => MediumRisk
ImageFile => LowRisk
AudioFile => LowRisk
VideoFile => LowRisk
ArchiveFile => MediumRisk
CodeFile => MediumRisk
ExecutableFile => HighRisk
FontFile => LowRisk
DataFile => risk_for_data_extension(extension)
UnknownFile => UnknownRisk
}
}
///|
pub fn inline_allowed_for_kind(kind : FileKind) -> Bool {
match kind {
TextFile => true
ImageFile => true
AudioFile => true
VideoFile => true
FontFile => true
_ => false
}
}
///|
fn should_allow_inline(
prefer_inline : Bool,
kind : FileKind,
risk : ExtensionRisk,
) -> Bool {
prefer_inline && inline_allowed_for_kind(kind) && risk != HighRisk
}
///|
fn normalized_content_type_option(content_type : String?) -> String? {
match content_type {
Some(value) => Some(normalize_content_type(value))
None => None
}
}
///|
fn prepare_download_recommendations(
request : DownloadRequest,
filename : FilenameReport,
kind : FileKind,
risk : ExtensionRisk,
disposition_kind : DispositionKind,
) -> Array[Recommendation] {
let recommendations : Array[Recommendation] = []
if request.filename is None {
recommendations.push(
Recommendation::make(
InferFilename,
Note,
"Filename was inferred from the Content-Type value",
Some("filename"),
Some(filename.value),
None,
),
)
}
if filename.changed {
recommendations.push(
Recommendation::make(
SanitizeFilename,
Warning,
"Download filename was normalized before header generation",
Some("filename"),
Some(filename.value),
None,
),
)
}
if risk == HighRisk {
recommendations.push(
Recommendation::make(
AvoidExecutableDownload,
Warning,
"Executable file extensions should be reviewed before download",
Some("filename"),
Some(filename.value),
None,
),
)
} else if risk == MediumRisk || risk == UnknownRisk {
recommendations.push(
Recommendation::make(
ReviewFileExtension,
Note,
"File extension should be reviewed for the deployment context",
Some("filename"),
Some(kind.name()),
None,
),
)
}
if request.prefer_inline && disposition_kind == Attachment {
recommendations.push(
Recommendation::make(
PreferAttachmentDisposition,
Note,
"Attachment disposition was selected because inline preview is not appropriate",
None,
None,
None,
),
)
}
recommendations
}
///|
fn risk_for_data_extension(extension : String?) -> ExtensionRisk {
match extension {
Some(value) => {
let ext = normalize_extension(value)
if ext == "wasm" {
MediumRisk
} else if ext == "bin" || ext == "dat" || ext == "db" {
UnknownRisk
} else {
LowRisk
}
}
None => UnknownRisk
}
}
///|
fn is_text_extension(ext : String) -> Bool {
ext == "txt" ||
ext == "text" ||
ext == "log" ||
ext == "md" ||
ext == "markdown" ||
ext == "rst" ||
ext == "csv" ||
ext == "tsv" ||
ext == "ini" ||
ext == "conf" ||
ext == "cfg" ||
ext == "properties"
}
///|
fn is_document_extension(ext : String) -> Bool {
ext == "pdf" ||
ext == "doc" ||
ext == "docx" ||
ext == "odt" ||
ext == "rtf" ||
ext == "tex" ||
ext == "typ" ||
ext == "epub" ||
ext == "pages"
}
///|
fn is_spreadsheet_extension(ext : String) -> Bool {
ext == "xls" ||
ext == "xlsx" ||
ext == "ods" ||
ext == "numbers" ||
ext == "parquet" ||
ext == "orc"
}
///|
fn is_presentation_extension(ext : String) -> Bool {
ext == "ppt" || ext == "pptx" || ext == "odp" || ext == "key"
}
///|
fn is_image_extension(ext : String) -> Bool {
ext == "png" ||
ext == "jpg" ||
ext == "jpeg" ||
ext == "gif" ||
ext == "webp" ||
ext == "bmp" ||
ext == "tiff" ||
ext == "tif" ||
ext == "svg" ||
ext == "avif" ||
ext == "ico"
}
///|
fn is_audio_extension(ext : String) -> Bool {
ext == "mp3" ||
ext == "wav" ||
ext == "ogg" ||
ext == "flac" ||
ext == "aac" ||
ext == "m4a" ||
ext == "opus" ||
ext == "weba"
}
///|
fn is_video_extension(ext : String) -> Bool {
ext == "mp4" ||
ext == "webm" ||
ext == "mov" ||
ext == "m4v" ||
ext == "avi" ||
ext == "mkv" ||
ext == "ogv"
}
///|
fn is_archive_extension(ext : String) -> Bool {
ext == "zip" ||
ext == "tar" ||
ext == "gz" ||
ext == "tgz" ||
ext == "bz2" ||
ext == "xz" ||
ext == "7z" ||
ext == "rar" ||
ext == "jar" ||
ext == "war"
}
///|
fn is_code_extension(ext : String) -> Bool {
ext == "mbt" ||
ext == "js" ||
ext == "ts" ||
ext == "jsx" ||
ext == "tsx" ||
ext == "py" ||
ext == "rs" ||
ext == "go" ||
ext == "java" ||
ext == "c" ||
ext == "h" ||
ext == "cc" ||
ext == "cpp" ||
ext == "hpp" ||
ext == "cs" ||
ext == "scala" ||
ext == "kt" ||
ext == "swift" ||
ext == "sh" ||
ext == "bat" ||
ext == "ps1" ||
ext == "sql" ||
ext == "lua" ||
ext == "rb" ||
ext == "php" ||
ext == "erl" ||
ext == "ex" ||
ext == "exs"
}
///|
fn is_executable_extension(ext : String) -> Bool {
ext == "exe" ||
ext == "dll" ||
ext == "msi" ||
ext == "com" ||
ext == "scr" ||
ext == "cmd" ||
ext == "app" ||
ext == "deb" ||
ext == "rpm" ||
ext == "apk" ||
ext == "dmg" ||
ext == "pkg"
}
///|
fn is_font_extension(ext : String) -> Bool {
ext == "ttf" ||
ext == "otf" ||
ext == "woff" ||
ext == "woff2" ||
ext == "eot"
}
///|
fn is_data_extension(ext : String) -> Bool {
ext == "json" ||
ext == "xml" ||
ext == "yaml" ||
ext == "yml" ||
ext == "toml" ||
ext == "bin" ||
ext == "dat" ||
ext == "db" ||
ext == "sqlite" ||
ext == "sqlite3" ||
ext == "wasm"
}