///|
pub(all) struct ValidationPolicy {
required_fields : Array[String]
required_files : Array[String]
allowed_file_content_types : Array[String]
max_text_length : Int
max_file_length : Int
max_filename_length : Int
allow_empty_files : Bool
allow_duplicate_fields : Bool
allow_duplicate_files : Bool
} derive(Debug, Eq)
///|
pub(all) struct ValidationIssue {
code : String
message : String
part_index : Int?
field_name : String?
} derive(Debug, Eq)
///|
pub(all) struct ValidationReport {
issues : Array[ValidationIssue]
} derive(Debug, Eq)
///|
pub fn default_validation_policy() -> ValidationPolicy {
{
required_fields: [],
required_files: [],
allowed_file_content_types: [],
max_text_length: 1024 * 256,
max_file_length: 1024 * 1024 * 4,
max_filename_length: 120,
allow_empty_files: true,
allow_duplicate_fields: true,
allow_duplicate_files: true,
}
}
///|
pub fn strict_validation_policy() -> ValidationPolicy {
{
required_fields: [],
required_files: [],
allowed_file_content_types: [],
max_text_length: 1024 * 64,
max_file_length: 1024 * 1024,
max_filename_length: 80,
allow_empty_files: false,
allow_duplicate_fields: false,
allow_duplicate_files: true,
}
}
///|
pub fn validate_form(
form : MultipartForm,
policy : ValidationPolicy,
) -> ValidationReport {
let issues = Array::new()
validate_required_fields(form, policy, issues)
validate_required_files(form, policy, issues)
validate_parts(form, policy, issues)
validate_duplicates(form, policy, issues)
{ issues, }
}
///|
pub fn MultipartForm::validate(
self : MultipartForm,
policy : ValidationPolicy,
) -> ValidationReport {
validate_form(self, policy)
}
///|
pub fn ValidationReport::is_ok(self : ValidationReport) -> Bool {
self.issues.length() == 0
}
///|
pub fn ValidationReport::issue_count(self : ValidationReport) -> Int {
self.issues.length()
}
///|
pub fn ValidationReport::first_message(self : ValidationReport) -> String? {
if self.issues.length() == 0 {
None
} else {
Some(self.issues[0].message)
}
}
///|
pub fn ValidationReport::messages(self : ValidationReport) -> Array[String] {
let messages = Array::new()
let mut i = 0
while i < self.issues.length() {
messages.push(self.issues[i].message)
i = i + 1
}
messages
}
///|
pub fn ValidationReport::summary(self : ValidationReport) -> String {
if self.is_ok() {
"ok"
} else {
"invalid: " + self.issues.length().to_string() + " issue(s)"
}
}
///|
pub fn ValidationIssue::to_line(self : ValidationIssue) -> String {
let index = match self.part_index {
Some(value) => "part=" + value.to_string()
None => "part=-"
}
let field = match self.field_name {
Some(value) => "field=" + value
None => "field=-"
}
self.code + "; " + index + "; " + field + "; " + self.message
}
///|
pub fn validation_issue(
code : String,
message : String,
part_index? : Int,
field_name? : String,
) -> ValidationIssue {
{ code, message, part_index, field_name }
}
///|
pub fn validation_policy_require_field(
policy : ValidationPolicy,
name : String,
) -> ValidationPolicy {
let required_fields = clone_string_array(policy.required_fields)
required_fields.push(name)
{
required_fields,
required_files: clone_string_array(policy.required_files),
allowed_file_content_types: clone_string_array(
policy.allowed_file_content_types,
),
max_text_length: policy.max_text_length,
max_file_length: policy.max_file_length,
max_filename_length: policy.max_filename_length,
allow_empty_files: policy.allow_empty_files,
allow_duplicate_fields: policy.allow_duplicate_fields,
allow_duplicate_files: policy.allow_duplicate_files,
}
}
///|
pub fn validation_policy_require_file(
policy : ValidationPolicy,
name : String,
) -> ValidationPolicy {
let required_files = clone_string_array(policy.required_files)
required_files.push(name)
{
required_fields: clone_string_array(policy.required_fields),
required_files,
allowed_file_content_types: clone_string_array(
policy.allowed_file_content_types,
),
max_text_length: policy.max_text_length,
max_file_length: policy.max_file_length,
max_filename_length: policy.max_filename_length,
allow_empty_files: policy.allow_empty_files,
allow_duplicate_fields: policy.allow_duplicate_fields,
allow_duplicate_files: policy.allow_duplicate_files,
}
}
///|
pub fn validation_policy_allow_file_type(
policy : ValidationPolicy,
content_type : String,
) -> ValidationPolicy {
let allowed = clone_string_array(policy.allowed_file_content_types)
allowed.push(to_lower_ascii(content_type))
{
required_fields: clone_string_array(policy.required_fields),
required_files: clone_string_array(policy.required_files),
allowed_file_content_types: allowed,
max_text_length: policy.max_text_length,
max_file_length: policy.max_file_length,
max_filename_length: policy.max_filename_length,
allow_empty_files: policy.allow_empty_files,
allow_duplicate_fields: policy.allow_duplicate_fields,
allow_duplicate_files: policy.allow_duplicate_files,
}
}
///|
fn validate_required_fields(
form : MultipartForm,
policy : ValidationPolicy,
issues : Array[ValidationIssue],
) -> Unit {
let mut i = 0
while i < policy.required_fields.length() {
let name = policy.required_fields[i]
if !form.has_field(name) {
issues.push(
validation_issue(
"missing_field",
"required field is missing: " + name,
field_name=name,
),
)
}
i = i + 1
}
}
///|
fn validate_required_files(
form : MultipartForm,
policy : ValidationPolicy,
issues : Array[ValidationIssue],
) -> Unit {
let mut i = 0
while i < policy.required_files.length() {
let name = policy.required_files[i]
if !form.has_file(name) {
issues.push(
validation_issue(
"missing_file",
"required file is missing: " + name,
field_name=name,
),
)
}
i = i + 1
}
}
///|
fn validate_parts(
form : MultipartForm,
policy : ValidationPolicy,
issues : Array[ValidationIssue],
) -> Unit {
let mut i = 0
while i < form.parts.length() {
let part = form.parts[i]
if part.name == "" {
issues.push(
validation_issue(
"empty_name",
"part name must not be empty",
part_index=i,
),
)
}
if part.is_file() {
validate_file_part(part, i, policy, issues)
} else {
validate_text_part(part, i, policy, issues)
}
i = i + 1
}
}
///|
fn validate_text_part(
part : FormPart,
index : Int,
policy : ValidationPolicy,
issues : Array[ValidationIssue],
) -> Unit {
if policy.max_text_length >= 0 && part.body.length() > policy.max_text_length {
issues.push(
validation_issue(
"text_too_large",
"text field exceeds max_text_length: " + part.name,
part_index=index,
field_name=part.name,
),
)
}
}
///|
fn validate_file_part(
part : FormPart,
index : Int,
policy : ValidationPolicy,
issues : Array[ValidationIssue],
) -> Unit {
if !policy.allow_empty_files && part.body.length() == 0 {
issues.push(
validation_issue(
"empty_file",
"empty file is not allowed: " + part.name,
part_index=index,
field_name=part.name,
),
)
}
if policy.max_file_length >= 0 && part.body.length() > policy.max_file_length {
issues.push(
validation_issue(
"file_too_large",
"file exceeds max_file_length: " + part.name,
part_index=index,
field_name=part.name,
),
)
}
match part.filename {
Some(filename) =>
if policy.max_filename_length >= 0 &&
filename.length() > policy.max_filename_length {
issues.push(
validation_issue(
"filename_too_long",
"filename exceeds max_filename_length: " + filename,
part_index=index,
field_name=part.name,
),
)
}
None =>
issues.push(
validation_issue(
"missing_filename",
"file part has no filename: " + part.name,
part_index=index,
field_name=part.name,
),
)
}
if policy.allowed_file_content_types.length() > 0 {
match part.content_type {
Some(content_type) =>
if !content_type_allowed(
content_type,
policy.allowed_file_content_types,
) {
issues.push(
validation_issue(
"content_type_not_allowed",
"file content type is not allowed: " + content_type,
part_index=index,
field_name=part.name,
),
)
}
None =>
issues.push(
validation_issue(
"missing_content_type",
"file content type is required: " + part.name,
part_index=index,
field_name=part.name,
),
)
}
}
}
///|
fn validate_duplicates(
form : MultipartForm,
policy : ValidationPolicy,
issues : Array[ValidationIssue],
) -> Unit {
let names = form.names()
let mut i = 0
while i < names.length() {
let name = names[i]
if !policy.allow_duplicate_fields && form.field_values(name).length() > 1 {
issues.push(
validation_issue(
"duplicate_field",
"duplicate text field is not allowed: " + name,
field_name=name,
),
)
}
if !policy.allow_duplicate_files && form.files(name).length() > 1 {
issues.push(
validation_issue(
"duplicate_file",
"duplicate file field is not allowed: " + name,
field_name=name,
),
)
}
i = i + 1
}
}
///|
fn content_type_allowed(content_type : String, allowed : Array[String]) -> Bool {
let lower = to_lower_ascii(content_type)
let mut i = 0
while i < allowed.length() {
if lower == to_lower_ascii(allowed[i]) {
return true
}
i = i + 1
}
false
}
///|
fn clone_string_array(values : Array[String]) -> Array[String] {
let out = Array::new()
let mut i = 0
while i < values.length() {
out.push(values[i])
i = i + 1
}
out
}