///|
priv struct WorkbookSheetInfo {
name : String
sheet_id : UInt
state : SheetState
rel_id : String
}
///|
let transitional_spreadsheet_namespace = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
///|
let strict_spreadsheet_namespace = "http://purl.oclc.org/ooxml/spreadsheetml/main"
///|
let transitional_relationship_attribute_namespace = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
///|
let strict_relationship_attribute_namespace = "http://purl.oclc.org/ooxml/officeDocument/relationships"
///|
fn workbook_scanner_next(
scanner : @ooxml.XmlStartTagScanner,
) -> Bool raise XlsxError {
scanner.next() catch {
InvalidXml(msg~) => raise InvalidXml(msg~)
ReadCancelled => raise ReadCancelled
}
}
///|
fn workbook_scanner_attribute(
scanner : @ooxml.XmlStartTagScanner,
namespace_uri : StringView,
local_name : StringView,
) -> String? raise XlsxError {
scanner.attribute(namespace_uri, local_name) catch {
InvalidXml(msg~) => raise InvalidXml(msg~)
ReadCancelled => raise ReadCancelled
}
}
///|
fn workbook_scanner_exact_attribute_values(
scanner : @ooxml.XmlStartTagScanner,
expected : ArrayView[(String, String)],
) -> Array[String]? raise XlsxError {
scanner.exact_attribute_values(expected) catch {
InvalidXml(msg~) => raise InvalidXml(msg~)
ReadCancelled => raise ReadCancelled
}
}
///|
fn workbook_scanner_has_exact_attributes(
scanner : @ooxml.XmlStartTagScanner,
expected : ArrayView[(String, String, String)],
) -> Bool raise XlsxError {
let names : Array[(String, String)] = []
for entry in expected {
let (namespace_uri, local_name, _) = entry
names.push((namespace_uri, local_name))
}
guard workbook_scanner_exact_attribute_values(scanner, names) is Some(values) else {
return false
}
for index, entry in expected {
let (_, _, expected_value) = entry
if values[index] != expected_value {
return false
}
}
true
}
///|
fn parse_workbook_sheets(
xml : StringView,
max_sheets : Int,
cancelled? : () -> Bool = () => false,
) -> Array[WorkbookSheetInfo] raise XlsxError {
let sheets : Array[WorkbookSheetInfo] = []
let sheet_names : Map[String, Bool] = Map([])
let sheet_ids : Map[UInt, Bool] = Map([])
let scanner = @ooxml.XmlStartTagScanner::new(xml, cancelled~)
let mut workbook_namespace : String? = None
while workbook_scanner_next(scanner) {
let namespace_uri = scanner.namespace_uri()
if scanner.depth() == 1 {
if scanner.local_name() != "workbook" ||
(
namespace_uri != transitional_spreadsheet_namespace &&
namespace_uri != strict_spreadsheet_namespace
) {
raise InvalidXml(msg="workbook document element is invalid")
}
workbook_namespace = Some(namespace_uri.to_owned())
continue
}
if (
namespace_uri != transitional_spreadsheet_namespace &&
namespace_uri != strict_spreadsheet_namespace
) ||
scanner.local_name() != "sheet" {
continue
}
let correct_parent = scanner.parent_local_name() is Some("sheets") &&
scanner.parent_namespace_uri() == workbook_namespace
if scanner.depth() != 3 ||
namespace_uri.to_owned() != workbook_namespace.unwrap_or("") ||
!correct_parent {
// SpreadsheetML elements outside the schema-defined sheet collection
// are extension payload, not logical workbook tabs.
continue
}
if sheets.length() >= max_sheets {
raise ResourceLimitExceeded(
kind="workbook_sheets",
limit=max_sheets,
actual=max_sheets + 1,
)
}
let name = match workbook_scanner_attribute(scanner, "", "name") {
Some(value) => value
None => raise InvalidXml(msg="sheet name missing")
}
check_sheet_name(name) catch {
InvalidSheetName(msg~) => raise InvalidXml(msg~)
error => raise error
}
let normalized_name = normalize_sheet_name(name)
if sheet_names.contains(normalized_name) {
raise InvalidXml(msg="duplicate sheet name")
}
sheet_names[normalized_name] = true
let sheet_id = match workbook_scanner_attribute(scanner, "", "sheetId") {
Some(value) =>
@string.parse_uint(value, base=10) catch {
_ => raise InvalidXml(msg="sheetId invalid")
}
None => raise InvalidXml(msg="sheetId missing")
}
if sheet_ids.contains(sheet_id) {
raise InvalidXml(msg="duplicate sheetId")
}
sheet_ids[sheet_id] = true
let relationship_namespace = if namespace_uri ==
strict_spreadsheet_namespace {
strict_relationship_attribute_namespace
} else {
transitional_relationship_attribute_namespace
}
let rel_id = match
workbook_scanner_attribute(scanner, relationship_namespace, "id") {
Some(value) => value
None => raise InvalidXml(msg="sheet relationship missing")
}
let state = match workbook_scanner_attribute(scanner, "", "state") {
Some(value) =>
match value {
"visible" => Visible
"hidden" => Hidden
"veryHidden" => VeryHidden
_ => raise InvalidXml(msg="sheet state invalid")
}
None => Visible
}
sheets.push({ name, sheet_id, state, rel_id })
}
sheets
}
///|
test "read workbook sheets rejects invalid and case-duplicate names" {
let prefix = ""
let suffix = ""
let invalid = prefix +
"" +
suffix
try parse_workbook_sheets(invalid, 10) catch {
InvalidXml(msg~) =>
inspect(msg, content="sheet name contains invalid characters")
_ => fail("unexpected invalid sheet-name error")
} noraise {
_ => fail("expected invalid sheet-name rejection")
}
let duplicate = prefix +
"" +
"" +
suffix
try parse_workbook_sheets(duplicate, 10) catch {
InvalidXml(msg~) => inspect(msg, content="duplicate sheet name")
_ => fail("unexpected duplicate sheet-name error")
} noraise {
_ => fail("expected duplicate sheet-name rejection")
}
}
///|
test "read workbook sheets requires unique unsigned sheet ids" {
let prefix = ""
let suffix = ""
let invalid : Array[(String, String)] = [
("", "sheetId missing"),
("", "sheetId invalid"),
(
"", "sheetId invalid",
),
(
"",
"duplicate sheetId",
),
]
for entry in invalid {
let (body, expected) = entry
try parse_workbook_sheets(prefix + body + suffix, 10) catch {
InvalidXml(msg~) => assert_eq(msg, expected)
_ => fail("unexpected sheetId error")
} noraise {
_ => fail("expected invalid sheetId rejection")
}
}
let maximum = parse_workbook_sheets(
prefix +
"" +
suffix,
10,
)
assert_eq(maximum.length(), 1)
assert_eq(maximum[0].sheet_id, 4294967295U)
}
///|
test "read workbook sheets accepts only exact visibility states" {
let prefix = ""
let suffix = ""
let valid = parse_workbook_sheets(
prefix +
"" +
"" +
"" +
"" +
suffix,
10,
)
assert_true(valid[0].state is Visible)
assert_true(valid[1].state is Visible)
assert_true(valid[2].state is Hidden)
assert_true(valid[3].state is VeryHidden)
for state in ["Visible", "Hidden", "veryhidden", "unknown", ""] {
let xml = prefix +
"" +
suffix
try parse_workbook_sheets(xml, 10) catch {
InvalidXml(msg~) => assert_eq(msg, "sheet state invalid")
_ => fail("unexpected sheet-state error")
} noraise {
_ => fail("expected invalid sheet-state rejection")
}
}
}
///|
fn parse_active_sheet_index(xml : StringView) -> Int? raise XlsxError {
let xml_str = xml.to_owned()
let marker = " None
Some(start) => {
let rest = xml_str[start + marker.length():]
let end = match rest.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="workbookView tag not closed")
}
let tag = rest[:end]
match attr_value(tag, "activeTab") {
Some(value) =>
Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="activeTab invalid")
},
)
None => None
}
}
}
}
///|
fn parse_workbook_props(
xml : StringView,
) -> WorkbookPropsOptions raise XlsxError {
let xml_str = xml.to_owned()
let marker = " WorkbookPropsOptions::new()
Some(start) => {
let rest = xml_str[start + marker.length():]
let end = match rest.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="workbookPr tag not closed")
}
let tag = rest[:end]
let props = WorkbookPropsOptions::new()
match attr_value(tag, "date1904") {
Some(value) => props.date_1904 = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "filterPrivacy") {
Some(value) => props.filter_privacy = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "codeName") {
Some(value) => props.code_name = Some(unescape_xml_text(value))
None => ()
}
props
}
}
}
///|
fn parse_calc_props(xml : StringView) -> CalcPropsOptions raise XlsxError {
let xml_str = xml.to_owned()
let marker = " CalcPropsOptions::new()
Some(start) => {
let rest = xml_str[start + marker.length():]
let end = match rest.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="calcPr tag not closed")
}
let tag = rest[:end]
let props = CalcPropsOptions::new()
match attr_value(tag, "calcId") {
Some(value) =>
props.calc_id = Some(
@string.parse_uint(value, base=10) catch {
_ => raise InvalidXml(msg="calcId invalid")
},
)
None => ()
}
match attr_value(tag, "calcMode") {
Some(value) =>
match normalize_calc_mode(value) {
Some(mode) => props.calc_mode = Some(mode)
None => raise InvalidXml(msg="calcMode invalid")
}
None => ()
}
match attr_value(tag, "fullCalcOnLoad") {
Some(value) => props.full_calc_on_load = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "refMode") {
Some(value) =>
match normalize_ref_mode(value) {
Some(mode) => props.ref_mode = Some(mode)
None => raise InvalidXml(msg="refMode invalid")
}
None => ()
}
match attr_value(tag, "iterate") {
Some(value) => props.iterate = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "iterateCount") {
Some(value) =>
props.iterate_count = Some(
@string.parse_uint(value, base=10) catch {
_ => raise InvalidXml(msg="iterateCount invalid")
},
)
None => ()
}
match attr_value(tag, "iterateDelta") {
Some(value) =>
props.iterate_delta = Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="iterateDelta invalid")
},
)
None => ()
}
match attr_value(tag, "fullPrecision") {
Some(value) => props.full_precision = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "calcCompleted") {
Some(value) => props.calc_completed = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "calcOnSave") {
Some(value) => props.calc_on_save = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "concurrentCalc") {
Some(value) => props.concurrent_calc = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "concurrentManualCount") {
Some(value) =>
props.concurrent_manual_count = Some(
@string.parse_uint(value, base=10) catch {
_ => raise InvalidXml(msg="concurrentManualCount invalid")
},
)
None => ()
}
match attr_value(tag, "forceFullCalc") {
Some(value) => props.force_full_calc = Some(parse_cell_bool(value))
None => ()
}
props
}
}
}
///|
fn parse_workbook_protection(
xml : StringView,
) -> WorkbookProtection? raise XlsxError {
let xml_str = xml.to_owned()
let marker = " None
Some(start) => {
let rest = xml_str[start + marker.length():]
let end = match rest.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="workbookProtection tag not closed")
}
let tag = rest[:end]
let protection : WorkbookProtection = {
lock_structure: false,
lock_windows: false,
algorithm_name: "",
hash_value: "",
salt_value: "",
spin_count: 0,
}
match attr_value(tag, "lockStructure") {
Some(value) => protection.lock_structure = parse_cell_bool(value)
None => ()
}
match attr_value(tag, "lockWindows") {
Some(value) => protection.lock_windows = parse_cell_bool(value)
None => ()
}
match attr_value(tag, "workbookAlgorithmName") {
Some(value) => protection.algorithm_name = value.to_string()
None => ()
}
match attr_value(tag, "workbookHashValue") {
Some(value) => protection.hash_value = value.to_string()
None => ()
}
match attr_value(tag, "workbookSaltValue") {
Some(value) => protection.salt_value = value.to_string()
None => ()
}
match attr_value(tag, "workbookSpinCount") {
Some(value) =>
protection.spin_count = @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="workbookSpinCount invalid")
}
None => ()
}
Some(protection)
}
}
}