///|
/// Diagnostic severity for validation findings.
pub(all) enum Severity {
Info
Warning
Error
} derive(Eq, Debug)
///|
/// Validation diagnostic.
pub(all) struct Diagnostic {
severity : Severity
code : String
path : String
message : String
} derive(Eq, Debug)
///|
/// Validate an archive and return all findings.
pub fn validate_archive(archive : HarArchive) -> Array[Diagnostic] {
let out : Array[Diagnostic] = []
validate_log(archive.log, out)
for i = 0; i < archive.log.pages.length(); i = i + 1 {
validate_page(archive.log.pages[i], i, out)
}
for i = 0; i < archive.log.entries.length(); i = i + 1 {
validate_entry(archive.log.entries[i], i, out)
}
out
}
///|
pub fn is_valid_archive(archive : HarArchive) -> Bool {
validate_archive(archive).all(d => d.severity != Error)
}
///|
fn push_diag(
out : Array[Diagnostic],
severity : Severity,
code : String,
path : String,
message : String,
) -> Unit {
out.push(Diagnostic::{ severity, code, path, message })
}
///|
fn validate_log(log : HarLog, out : Array[Diagnostic]) -> Unit {
if log.version == "" {
push_diag(
out,
Error,
"har.log.version.missing",
"$.log.version",
"HAR log version is required",
)
}
if log.version != "" && log.version != "1.1" && log.version != "1.2" {
push_diag(
out,
Warning,
"har.log.version.unknown",
"$.log.version",
"HAR version is not a common 1.1/1.2 value",
)
}
if log.creator_name == "" {
push_diag(
out,
Warning,
"har.creator.name.missing",
"$.log.creator.name",
"creator name is empty",
)
}
if log.entries.is_empty() {
push_diag(
out,
Warning,
"har.entries.empty",
"$.log.entries",
"archive contains no request entries",
)
}
}
///|
fn validate_page(page : HarPage, index : Int, out : Array[Diagnostic]) -> Unit {
let path = "$.log.pages[" + index.to_string() + "]"
if page.id == "" {
push_diag(
out,
Warning,
"har.page.id.empty",
path + ".id",
"page id is empty",
)
}
if page.started_date_time == "" {
push_diag(
out,
Warning,
"har.page.started.empty",
path + ".startedDateTime",
"page start time is empty",
)
}
if page.page_timings.on_content_load < -1 {
push_diag(
out,
Error,
"har.page.timings.content.negative",
path + ".pageTimings.onContentLoad",
"page timing cannot be below -1",
)
}
if page.page_timings.on_load < -1 {
push_diag(
out,
Error,
"har.page.timings.load.negative",
path + ".pageTimings.onLoad",
"page timing cannot be below -1",
)
}
if page.page_timings.on_content_load > page.page_timings.on_load &&
page.page_timings.on_load >= 0 {
push_diag(
out,
Warning,
"har.page.timings.order",
path + ".pageTimings",
"content load happens after page load",
)
}
}
///|
fn validate_entry(
entry : HarEntry,
index : Int,
out : Array[Diagnostic],
) -> Unit {
let path = "$.log.entries[" + index.to_string() + "]"
if entry.started_date_time == "" {
push_diag(
out,
Warning,
"har.entry.started.empty",
path + ".startedDateTime",
"entry start time is empty",
)
}
if entry.time < 0 {
push_diag(
out,
Error,
"har.entry.time.negative",
path + ".time",
"entry total time cannot be negative",
)
}
validate_request(entry.request, path + ".request", out)
validate_response(entry.response, path + ".response", out)
validate_timings(entry, path + ".timings", out)
}
///|
fn validate_request(
req : HarRequest,
path : String,
out : Array[Diagnostic],
) -> Unit {
if req.http_method == "" {
push_diag(
out,
Error,
"har.request.method.empty",
path + ".method",
"request method is required",
)
}
if !is_common_method(req.http_method) {
push_diag(
out,
Warning,
"har.request.method.uncommon",
path + ".method",
"request method is uncommon",
)
}
if req.url == "" {
push_diag(
out,
Error,
"har.request.url.empty",
path + ".url",
"request URL is required",
)
}
if req.url != "" &&
!(req.url.has_prefix("http://") || req.url.has_prefix("https://")) {
push_diag(
out,
Warning,
"har.request.url.scheme",
path + ".url",
"request URL is not http or https",
)
}
if req.headers_size < -1 {
push_diag(
out,
Error,
"har.request.headersSize.range",
path + ".headersSize",
"headersSize must be -1 or non-negative",
)
}
if req.body_size < -1 {
push_diag(
out,
Error,
"har.request.bodySize.range",
path + ".bodySize",
"bodySize must be -1 or non-negative",
)
}
validate_pairs(req.headers, path + ".headers", out)
validate_pairs(req.query_string, path + ".queryString", out)
validate_pairs(req.cookies, path + ".cookies", out)
match req.post_data {
Some(pd) => validate_post_data(pd, path + ".postData", out)
None => ()
}
}
///|
fn validate_response(
res : HarResponse,
path : String,
out : Array[Diagnostic],
) -> Unit {
if res.status < 0 || res.status > 999 {
push_diag(
out,
Error,
"har.response.status.range",
path + ".status",
"HTTP status must be between 0 and 999",
)
}
if res.status >= 400 {
push_diag(
out,
Warning,
"har.response.status.failure",
path + ".status",
"response status is an HTTP error",
)
}
if res.headers_size < -1 {
push_diag(
out,
Error,
"har.response.headersSize.range",
path + ".headersSize",
"headersSize must be -1 or non-negative",
)
}
if res.body_size < -1 {
push_diag(
out,
Error,
"har.response.bodySize.range",
path + ".bodySize",
"bodySize must be -1 or non-negative",
)
}
if res.content.size < 0 {
push_diag(
out,
Error,
"har.response.content.size.range",
path + ".content.size",
"content size cannot be negative",
)
}
if res.content.mime_type == "" {
push_diag(
out,
Info,
"har.response.content.mime.empty",
path + ".content.mimeType",
"content MIME type is empty",
)
}
validate_pairs(res.headers, path + ".headers", out)
validate_pairs(res.cookies, path + ".cookies", out)
}
///|
fn validate_timings(
entry : HarEntry,
path : String,
out : Array[Diagnostic],
) -> Unit {
validate_phase(entry.timings.blocked, path + ".blocked", out)
validate_phase(entry.timings.dns, path + ".dns", out)
validate_phase(entry.timings.connect, path + ".connect", out)
validate_phase(entry.timings.ssl, path + ".ssl", out)
validate_phase(entry.timings.send, path + ".send", out)
validate_phase(entry.timings.wait, path + ".wait", out)
validate_phase(entry.timings.receive, path + ".receive", out)
let total = sum_known_timings(entry.timings)
if entry.time > 0 && total > entry.time * 2 {
push_diag(
out,
Warning,
"har.timings.sum.large",
path,
"known timing phases are much larger than entry time",
)
}
}
///|
fn validate_phase(value : Int, path : String, out : Array[Diagnostic]) -> Unit {
if value < -1 {
push_diag(
out,
Error,
"har.timings.phase.range",
path,
"timing phase must be -1 or non-negative",
)
}
}
///|
fn validate_pairs(
values : Array[HarPair],
path : String,
out : Array[Diagnostic],
) -> Unit {
for i = 0; i < values.length(); i = i + 1 {
let item = values[i]
let item_path = path + "[" + i.to_string() + "]"
if item.name == "" {
push_diag(
out,
Warning,
"har.pair.name.empty",
item_path + ".name",
"name is empty",
)
}
}
}
///|
fn validate_post_data(
pd : HarPostData,
path : String,
out : Array[Diagnostic],
) -> Unit {
if pd.mime_type == "" && pd.text != "" {
push_diag(
out,
Info,
"har.postData.mime.empty",
path + ".mimeType",
"post data has text but no MIME type",
)
}
validate_pairs(pd.params, path + ".params", out)
}
///|
fn is_common_method(http_method : String) -> Bool {
http_method == "GET" ||
http_method == "POST" ||
http_method == "PUT" ||
http_method == "PATCH" ||
http_method == "DELETE" ||
http_method == "HEAD" ||
http_method == "OPTIONS" ||
http_method == "TRACE"
}
///|
pub fn sum_known_timings(t : HarTimings) -> Int {
positive(t.blocked) +
positive(t.dns) +
positive(t.connect) +
positive(t.ssl) +
positive(t.send) +
positive(t.wait) +
positive(t.receive)
}
///|
fn positive(value : Int) -> Int {
if value > 0 {
value
} else {
0
}
}