///|
pub fn FilenamePolicy::with_replacement(
self : FilenamePolicy,
replacement : String,
) -> FilenamePolicy {
{
replacement,
max_length: self.max_length,
default_name: self.default_name,
allow_hidden: self.allow_hidden,
}
}
///|
pub fn FilenamePolicy::with_max_length(
self : FilenamePolicy,
max_length : Int,
) -> FilenamePolicy {
{
replacement: self.replacement,
max_length,
default_name: self.default_name,
allow_hidden: self.allow_hidden,
}
}
///|
pub fn FilenamePolicy::with_default_name(
self : FilenamePolicy,
default_name : String,
) -> FilenamePolicy {
{
replacement: self.replacement,
max_length: self.max_length,
default_name,
allow_hidden: self.allow_hidden,
}
}
///|
pub fn FilenamePolicy::with_hidden_files(
self : FilenamePolicy,
allow_hidden : Bool,
) -> FilenamePolicy {
{
replacement: self.replacement,
max_length: self.max_length,
default_name: self.default_name,
allow_hidden,
}
}
///|
pub fn FilenamePolicy::validate(self : FilenamePolicy) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
if self.replacement.length() > 8 {
push_diag(
diagnostics,
UnsafeFilename,
Warning,
"Replacement should be short to avoid surprising filenames",
None,
)
}
if self.default_name.trim(chars=" \t\r\n.").to_owned().is_empty() {
push_diag(
diagnostics,
EmptyFilename,
Error,
"Default filename must not be empty after trimming",
None,
)
}
if self.max_length > 0 && self.max_length < 8 {
push_diag(
diagnostics,
LengthReduced,
Warning,
"Very small filename length limits may remove useful extensions",
None,
)
}
diagnostics
}
///|
pub fn sanitize_filename_strict(filename : String) -> FilenameReport {
sanitize_filename_with_policy(filename, FilenamePolicy::strict_download())
}
///|
pub fn sanitize_upload_filename(filename : String) -> FilenameReport {
sanitize_filename_with_policy(filename, FilenamePolicy::multipart_upload())
}
///|
pub fn sanitize_object_key(filename : String) -> FilenameReport {
sanitize_filename_with_policy(filename, FilenamePolicy::object_storage_key())
}
///|
pub fn sanitize_filename_preserve_extension(
filename : String,
policy : FilenamePolicy,
) -> FilenameReport {
let base_policy : FilenamePolicy = {
replacement: policy.replacement,
max_length: 0,
default_name: policy.default_name,
allow_hidden: policy.allow_hidden,
}
let report = sanitize_filename_with_policy(filename, base_policy)
if policy.max_length <= 0 || report.value.char_length() <= policy.max_length {
return report
}
let shortened = shorten_preserving_extension(report.value, policy.max_length)
push_diag(
report.diagnostics,
LengthReduced,
Warning,
"Filename was shortened while preserving the extension when possible",
None,
)
{
value: shortened,
original: report.original,
changed: true,
diagnostics: report.diagnostics,
}
}
///|
pub fn is_probably_safe_filename(filename : String) -> Bool {
let report = sanitize_filename(filename)
!report.changed && !report.diagnostics.any(d => d.severity == Error)
}
///|
pub fn filename_extension(filename : String) -> String? {
match last_dot_index(filename) {
Some(index) =>
if index + 1 >= filename.length() {
None
} else {
Some(filename[index + 1:].to_owned())
}
None => None
}
}
///|
pub fn filename_stem(filename : String) -> String {
match last_dot_index(filename) {
Some(index) =>
if index == 0 {
filename
} else {
filename[0:index].to_owned()
}
None => filename
}
}
///|
pub fn filename_has_extension(filename : String, extension : String) -> Bool {
match filename_extension(filename) {
Some(value) => value.to_lower() == normalize_extension(extension)
None => false
}
}
///|
pub fn normalize_extension(extension : String) -> String {
let trimmed = extension.trim(chars=" \t\r\n").to_owned().to_lower()
if trimmed.has_prefix(".") && trimmed.length() > 1 {
trimmed[1:].to_owned()
} else {
trimmed
}
}
///|
fn last_dot_index(filename : String) -> Int? {
let mut result : Int? = None
let mut index = 0
while index < filename.length() {
let c = char_at(filename, index)
if c == '.' {
result = Some(index)
}
index = index + c.utf16_len()
}
result
}
///|
fn shorten_preserving_extension(filename : String, max_chars : Int) -> String {
if max_chars <= 0 || filename.char_length() <= max_chars {
return filename
}
match last_dot_index(filename) {
Some(dot) => {
let ext = filename[dot:].to_owned()
let ext_chars = ext.char_length()
if dot == 0 || ext_chars + 2 >= max_chars {
take_chars(filename, max_chars)
} else {
let stem_limit = max_chars - ext_chars
take_chars(filename[0:dot].to_owned(), stem_limit) + ext
}
}
None => take_chars(filename, max_chars)
}
}