///|
let package_relationships_namespace_for_validation = "http://schemas.openxmlformats.org/package/2006/relationships"
///|
fn is_relationship_package_part(name : StringView) -> Bool {
name.to_owned().to_lower().has_suffix(".rels")
}
///|
fn relationship_scanner_attribute_view(
scanner : @ooxml.XmlStartTagScanner,
name : StringView,
) -> StringView? raise XlsxError {
scanner.attribute_view("", name) catch {
InvalidXml(msg~) => raise InvalidXml(msg~)
ReadCancelled => raise ReadCancelled
}
}
///|
fn relationship_scanner_attribute(
scanner : @ooxml.XmlStartTagScanner,
name : StringView,
) -> String? raise XlsxError {
scanner.attribute("", name) catch {
InvalidXml(msg~) => raise InvalidXml(msg~)
ReadCancelled => raise ReadCancelled
}
}
///|
/// Visits relationship start tags through the namespace-aware XML scanner.
/// In particular, XML whitespace after the QName and declared QName prefixes
/// are accepted instead of relying on one serialized spelling such as
/// ` Bool raise XlsxError,
cancelled? : () -> Bool = () => false,
) -> Unit raise XlsxError {
let scanner = @ooxml.XmlStartTagScanner::new(xml, cancelled~)
let mut scanning = true
while scanning {
let advanced = scanner.next() catch {
InvalidXml(msg~) => raise InvalidXml(msg~)
ReadCancelled => raise ReadCancelled
}
if !advanced {
scanning = false
continue
}
if scanner.namespace_uri() == package_relationships_namespace_for_validation &&
scanner.local_name() == "Relationship" {
if !visit(scanner) {
scanning = false
}
}
}
}
///|
fn charge_relationship_attribute(
value : StringView,
kind : String,
maximum : Int,
total : Ref[Int],
total_maximum : Int,
) -> Unit raise XlsxError {
if value.length() > maximum {
raise ResourceLimitExceeded(kind~, limit=maximum, actual=value.length())
}
if value.length() > total_maximum - total.val {
raise ResourceLimitExceeded(
kind="total_relationship_chars",
limit=total_maximum,
actual=bounded_actual_above_limit(total_maximum),
)
}
total.val = total.val + value.length()
}
///|
/// Preflights every relationship part before any relationship map or resolved
/// path is materialized. Attribute values stay borrowed until their individual
/// and package-wide ceilings have been charged. This makes the limits
/// cumulative even though downstream readers intentionally build separate maps
/// for different relationship types.
fn preflight_archive_relationship_limits(
archive : @zip.Archive,
limits : ReadLimits,
transcoder? : (String, Bytes) -> String raise XlsxError,
decode? : (BytesView) -> String raise XlsxError,
budget? : ReadBudget,
cancelled? : () -> Bool = () => false,
) -> Unit raise XlsxError {
let records = Ref(0)
let attribute_chars = Ref(0)
for entry in archive.entries() {
check_read_cancelled(cancelled)
if !is_relationship_package_part(entry.name()) {
continue
}
let xml = match decode {
Some(value) => value(entry.data())
None => decode_utf8(entry.data(), transcoder)
}
match budget {
Some(value) => {
value.checkpoint()
// The namespace-aware visitor below is an additional full pass beyond
// the source preflight performed by the shared decoder.
value.charge_work(xml.length())
}
None => ()
}
for_each_relationship_start_tag(
xml,
scanner => {
check_read_cancelled(cancelled)
records.val = records.val + 1
if records.val > limits.max_relationship_records {
raise ResourceLimitExceeded(
kind="relationship_records",
limit=limits.max_relationship_records,
actual=records.val,
)
}
match relationship_scanner_attribute_view(scanner, "Id") {
Some(value) =>
charge_relationship_attribute(
value,
"relationship_id_chars",
limits.max_relationship_id_chars,
attribute_chars,
limits.max_total_relationship_chars,
)
None => ()
}
match relationship_scanner_attribute_view(scanner, "Type") {
Some(value) =>
charge_relationship_attribute(
value,
"relationship_type_chars",
limits.max_relationship_type_chars,
attribute_chars,
limits.max_total_relationship_chars,
)
None => ()
}
match relationship_scanner_attribute_view(scanner, "Target") {
Some(value) =>
charge_relationship_attribute(
value,
"relationship_target_chars",
limits.max_relationship_target_chars,
attribute_chars,
limits.max_total_relationship_chars,
)
None => ()
}
// TargetMode is normally one of two short tokens. Charge it under the
// type ceiling so an irrelevant attacker-sized value cannot hide outside
// the package-wide relationship budget.
match relationship_scanner_attribute_view(scanner, "TargetMode") {
Some(value) =>
charge_relationship_attribute(
value,
"relationship_mode_chars",
limits.max_relationship_type_chars,
attribute_chars,
limits.max_total_relationship_chars,
)
None => ()
}
true
},
cancelled~,
)
}
}