///|
fn is_utf8_encoding(value : StringView) -> Bool {
let normalized = value.to_owned().to_lower()
normalized == "utf-8" || normalized == "utf8"
}
///|
fn is_encoding_char(value : Byte) -> Bool {
(value >= b'a' && value <= b'z') ||
(value >= b'A' && value <= b'Z') ||
(value >= b'0' && value <= b'9') ||
value == b'-' ||
value == b'_' ||
value == b'.'
}
///|
fn xml_encoding(bytes : BytesView) -> String? {
let len = bytes.length()
if len < 5 {
return None
}
let prefix_len = if len < 256 { len } else { 256 }
let prefix = bytes[:prefix_len]
let start = if prefix_len >= 3 && prefix[:3].equal(b"\xEF\xBB\xBF") {
3
} else {
0
}
if prefix_len < start + 5 || !prefix[start:start + 5].equal(b" (pos + b"encoding=\"".length(), b'"')
None =>
match prefix.find(b"encoding='") {
Some(pos) => (pos + b"encoding='".length(), b'\'')
None => return None
}
}
let mut i = needle
while i < prefix_len && prefix[i] != quote {
if !is_encoding_char(prefix[i]) {
return None
}
i = i + 1
}
if i <= needle || i >= prefix_len {
return None
}
let slice = prefix[needle:i]
let value = @encoding/utf8.decode(slice) catch { _ => return None }
Some(value)
}
///|
fn decode_utf8(
bytes : BytesView,
transcoder : ((String, Bytes) -> String raise XlsxError)?,
) -> String raise XlsxError {
// A UTF-16 byte-order mark is authoritative: a UTF-16 document's
// declaration is itself UTF-16 bytes, so the ASCII scan below
// cannot detect it. Fall back to the declared encoding otherwise.
let declared = match utf16_bom_label(bytes) {
Some(label) => Some(label)
None =>
match xml_encoding(bytes) {
Some(encoding) =>
if is_utf8_encoding(encoding) {
None
} else {
Some(encoding)
}
None => None
}
}
match declared {
Some(encoding) =>
match transcoder {
// A caller-supplied transcoder replaces the default entirely,
// like Go's SetCharsetReader.
Some(value) => return value(encoding, bytes.to_owned())
None =>
match builtin_charset_decode(encoding, bytes) {
Some(text) => return text
None =>
raise InvalidXml(
msg="xml encoding \{encoding} requires charset transcoder",
)
}
}
None => ()
}
@encoding/utf8.decode(bytes, ignore_bom=true) catch {
_ => raise InvalidXml(msg="invalid utf8")
}
}
///|
test "XLSX XML decoding strips a legal UTF-8 BOM" {
inspect(decode_utf8(b"\xEF\xBB\xBF", None), content="")
}
///|
/// OOXML numeric attributes must be finite before they enter the workbook
/// model. MoonBit's parser accepts IEEE NaN and infinities, but those values
/// cannot be represented by JSON and make comparisons in layout/style logic
/// non-deterministic.
fn parse_finite_double_for_read(
value : StringView,
message : String,
) -> Double raise XlsxError {
let parsed = @string.parse_double(value) catch {
_ => raise InvalidXml(msg=message)
}
if parsed.is_nan() || parsed.is_inf() {
raise InvalidXml(msg=message)
}
parsed
}
///|
fn parse_default_font(xml : StringView) -> String {
let xml_str = xml.to_owned()
let marker = " "Calibri"
Some(start) => {
let rest = xml_str[start + marker.length():]
match rest.find(" "Calibri"
Some(name_start) => {
let name_rest = rest[name_start + 5:]
match name_rest.find(">") {
None => "Calibri"
Some(end) => {
let tag = name_rest[:end]
let value_result : Result[String?, Error] = Ok(
attr_value(tag, "val"),
) catch {
e => Err(e)
}
match value_result {
Ok(value) =>
match value {
Some(text) => unescape_xml_text(text)
None => "Calibri"
}
Err(_) => "Calibri"
}
}
}
}
}
}
}
}
///|
fn parse_font_entry(xml : StringView) -> Font raise XlsxError {
let font = Font::new()
match tag_attributes_in(xml, "b") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => if parse_bool_attr(value) { font.bold = Some(true) }
None => font.bold = Some(true)
}
None => ()
}
match tag_attributes_in(xml, "i") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => if parse_bool_attr(value) { font.italic = Some(true) }
None => font.italic = Some(true)
}
None => ()
}
match tag_attributes_in(xml, "strike") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => if parse_bool_attr(value) { font.strike = Some(true) }
None => font.strike = Some(true)
}
None => ()
}
match tag_attributes_in(xml, "outline") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => if parse_bool_attr(value) { font.outline = Some(true) }
None => font.outline = Some(true)
}
None => ()
}
match tag_attributes_in(xml, "shadow") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => if parse_bool_attr(value) { font.shadow = Some(true) }
None => font.shadow = Some(true)
}
None => ()
}
match tag_attributes_in(xml, "condense") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => if parse_bool_attr(value) { font.condense = Some(true) }
None => font.condense = Some(true)
}
None => ()
}
match tag_attributes_in(xml, "extend") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => if parse_bool_attr(value) { font.extended = Some(true) }
None => font.extended = Some(true)
}
None => ()
}
match tag_attributes_in(xml, "u") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => font.underline = Some(unescape_xml_text(value))
None => font.underline = Some("single")
}
None => ()
}
match tag_attributes_in(xml, "sz") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) =>
font.size = Some(
parse_finite_double_for_read(value, "font sz invalid"),
)
None => ()
}
None => ()
}
match tag_attributes_in(xml, "charset") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) =>
try @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="font charset invalid")
} noraise {
val => font.charset = Some(val)
}
None => ()
}
None => ()
}
match tag_attributes_in(xml, "name") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => font.family = Some(unescape_xml_text(value))
None => ()
}
None => ()
}
match tag_attributes_in(xml, "family") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) =>
try @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="font family invalid")
} noraise {
val => if val != 2 { font.family_number = Some(val) }
}
None => ()
}
None => ()
}
match tag_attributes_in(xml, "vertAlign") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => font.vert_align = Some(unescape_xml_text(value))
None => ()
}
None => ()
}
match tag_attributes_in(xml, "scheme") {
Some(tag) =>
match attr_value(tag, "val") {
Some(value) => font.scheme = Some(unescape_xml_text(value))
None => ()
}
None => ()
}
match tag_attributes_in(xml, "color") {
Some(tag) => {
match attr_value(tag, "rgb") {
Some(value) => {
let rgb = unescape_xml_text(value)
font.color = Some(normalize_rgb_hex(rgb))
}
None => ()
}
match attr_value(tag, "theme") {
Some(value) =>
try @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="font color theme invalid")
} noraise {
val => font.color_theme = Some(val)
}
None => ()
}
match attr_value(tag, "indexed") {
Some(value) =>
try @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="font color indexed invalid")
} noraise {
val => font.color_indexed = Some(val)
}
None => ()
}
match attr_value(tag, "tint") {
Some(value) =>
font.color_tint = Some(
parse_finite_double_for_read(value, "font color tint invalid"),
)
None => ()
}
}
None => ()
}
font
}
///|
fn parse_fonts(xml : StringView) -> Array[Font] raise XlsxError {
let fonts : Array[Font] = []
let body = match extract_tag_body(xml, "fonts") {
Some(value) => value
None => {
fonts.push(Font::new())
return fonts
}
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None => continue
}
let rest = text[open_end + 1:]
let end = match rest.find("") {
Some(pos) => pos
None => continue
}
let content = rest[:end]
fonts.push(parse_font_entry(content))
}
if fonts.length() == 0 {
fonts.push(Font::new())
}
fonts
}
///|
fn parse_fill_entry(xml : StringView) -> Fill raise XlsxError {
let fill = Fill::new()
let patterns : Array[String] = [
"none", "solid", "mediumGray", "darkGray", "lightGray", "darkHorizontal", "darkVertical",
"darkDown", "darkUp", "darkGrid", "darkTrellis", "lightHorizontal", "lightVertical",
"lightDown", "lightUp", "lightGrid", "lightTrellis", "gray125", "gray0625",
]
fn parse_rgb_color(rgb : StringView) -> (String, Int?) {
// Strip an 8-char value's alpha only when the alpha really is hex —
// a malformed value must stay malformed (GG112233 is not #112233).
// Char-level, since parse_int would accept a signed "+1"/"-1".
fn is_hex(c : Int) -> Bool {
(c >= '0'.to_int() && c <= '9'.to_int()) ||
(c >= 'a'.to_int() && c <= 'f'.to_int()) ||
(c >= 'A'.to_int() && c <= 'F'.to_int())
}
let alpha_is_hex = rgb.length() == 8 &&
is_hex(rgb[0].to_int()) &&
is_hex(rgb[1].to_int())
if alpha_is_hex {
let alpha = parse_hex_byte(rgb, 0)
let opacity = (alpha * 100 + 127) / 255
let t = 100 - opacity
let transparency = if t <= 0 { None } else { Some(t) }
let color = rgb[2:].to_owned()
(color, transparency)
} else {
(rgb.to_owned(), None)
}
}
match tag_attributes_in(xml, "gradientFill") {
Some(tag) => {
fill.typ = Some("gradient")
let bottom = match attr_value(tag, "bottom") {
Some(value) =>
parse_finite_double_for_read(value, "gradientFill bottom invalid")
None => 0.0
}
let degree = match attr_value(tag, "degree") {
Some(value) =>
parse_finite_double_for_read(value, "gradientFill degree invalid")
None => 0.0
}
let left = match attr_value(tag, "left") {
Some(value) =>
parse_finite_double_for_read(value, "gradientFill left invalid")
None => 0.0
}
let right = match attr_value(tag, "right") {
Some(value) =>
parse_finite_double_for_read(value, "gradientFill right invalid")
None => 0.0
}
let top = match attr_value(tag, "top") {
Some(value) =>
parse_finite_double_for_read(value, "gradientFill top invalid")
None => 0.0
}
let typ = match attr_value(tag, "type") {
Some(value) => unescape_xml_text(value)
None => ""
}
let mut transparency : Int? = None
let stop_positions : Array[Double] = []
let stop_colors : Array[String] = []
match extract_tag_body(xml, "gradientFill") {
Some(body) => {
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None => continue
}
let open_tag = text[:open_end]
match attr_value(open_tag, "position") {
Some(value) =>
stop_positions.push(
parse_finite_double_for_read(
value, "gradientFill stop position invalid",
),
)
None => stop_positions.push(0.0)
}
let rest = text[open_end + 1:]
let end = match rest.find("") {
Some(pos) => pos
None => continue
}
let content = rest[:end]
match tag_attributes_in(content, "color") {
Some(color_tag) =>
match attr_value(color_tag, "rgb") {
Some(value) => {
let rgb = unescape_xml_text(value)
let (c, t) = parse_rgb_color(rgb)
stop_colors.push(c)
match (transparency, t) {
(None, Some(v)) => transparency = Some(v)
_ => ()
}
}
None => ()
}
None => ()
}
}
}
None => ()
}
let mut shading = 0
for i in 0.. fill.colors = Some([c1, c2])
_ => ()
}
match transparency {
Some(v) => fill.transparency = Some(v)
None => ()
}
}
None => ()
}
match tag_attributes_in(xml, "patternFill") {
Some(tag) => {
let pattern_type = match attr_value(tag, "patternType") {
Some(value) => unescape_xml_text(value)
None => "none"
}
let mut pattern = 0
let mut found = false
for i in 0.. {
match attr_value(color_tag, "theme") {
Some(value) =>
try @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="fill fgColor theme invalid")
} noraise {
val => fill.fg_theme = Some(val)
}
None => ()
}
match attr_value(color_tag, "indexed") {
Some(value) =>
try @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="fill fgColor indexed invalid")
} noraise {
val => fill.fg_indexed = Some(val)
}
None => ()
}
match attr_value(color_tag, "tint") {
Some(value) =>
fill.fg_tint = Some(
parse_finite_double_for_read(value, "fill fgColor tint invalid"),
)
None => ()
}
match attr_value(color_tag, "rgb") {
Some(value) => {
let rgb = unescape_xml_text(value)
let (c, t) = parse_rgb_color(rgb)
fg_color = Some(c)
match t {
Some(v) => transparency = Some(v)
None => ()
}
}
None => ()
}
}
None => ()
}
match tag_attributes_in(xml, "bgColor") {
Some(color_tag) => {
match attr_value(color_tag, "theme") {
Some(value) =>
try @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="fill bgColor theme invalid")
} noraise {
val => fill.bg_theme = Some(val)
}
None => ()
}
match attr_value(color_tag, "indexed") {
Some(value) =>
try @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="fill bgColor indexed invalid")
} noraise {
val => fill.bg_indexed = Some(val)
}
None => ()
}
match attr_value(color_tag, "tint") {
Some(value) =>
fill.bg_tint = Some(
parse_finite_double_for_read(value, "fill bgColor tint invalid"),
)
None => ()
}
match attr_value(color_tag, "rgb") {
Some(value) => {
let rgb = unescape_xml_text(value)
let (c, t) = parse_rgb_color(rgb)
bg_color = Some(c)
match t {
Some(v) => transparency = Some(v)
None => ()
}
}
None => ()
}
}
None => ()
}
let colors : Array[String] = []
match fg_color {
Some(c) => colors.push(c)
None => ()
}
match bg_color {
Some(c) => colors.push(c)
None => ()
}
if colors.length() > 0 {
fill.colors = Some(colors)
}
match transparency {
Some(v) => fill.transparency = Some(v)
None => ()
}
fill
}
None => fill
}
}
///|
fn parse_fills(xml : StringView) -> Array[Fill] raise XlsxError {
let fills : Array[Fill] = []
let body = match extract_tag_body(xml, "fills") {
Some(value) => value
None => return fills
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None => continue
}
let open_tag = text[:open_end]
if open_tag.has_suffix("/") {
fills.push(Fill::new())
continue
}
let rest = text[open_end + 1:]
let end = match rest.find("") {
Some(pos) => pos
None => continue
}
let content = rest[:end]
fills.push(parse_fill_entry(content))
}
fills
}
///|
fn parse_border_entry(
border_tag : StringView,
xml : StringView,
) -> Array[Border] raise XlsxError {
let border_styles : Array[String] = [
"none", "thin", "medium", "dashed", "dotted", "thick", "double", "hair", "mediumDashed",
"dashDot", "mediumDashDot", "dashDotDot", "mediumDashDotDot", "slantDashDot",
]
fn parse_side(
xml : StringView,
side : StringView,
) -> Border? raise XlsxError {
let tag = match tag_attributes_in(xml, side) {
Some(value) => value
None => return None
}
let style_name = match attr_value(tag, "style") {
Some(value) => unescape_xml_text(value)
None => "none"
}
let mut style = 0
for i in 0..
match tag_attributes_in(body, "color") {
Some(color_tag) =>
match attr_value(color_tag, "rgb") {
Some(value) => color = Some(unescape_xml_text(value))
None => ()
}
None => ()
}
None => ()
}
match color {
Some(rgb) =>
Some(
Border::with_values(
side.to_owned(),
color=normalize_rgb_hex(rgb),
style~,
),
)
None =>
if style > 0 {
Some(Border::with_values(side.to_owned(), style~))
} else {
None
}
}
}
let diagonal_up = match attr_value(border_tag, "diagonalUp") {
Some(value) => parse_bool_attr(value)
None => false
}
let diagonal_down = match attr_value(border_tag, "diagonalDown") {
Some(value) => parse_bool_attr(value)
None => false
}
let borders : Array[Border] = []
match parse_side(xml, "left") {
Some(b) => borders.push(b)
None => ()
}
match parse_side(xml, "right") {
Some(b) => borders.push(b)
None => ()
}
match parse_side(xml, "top") {
Some(b) => borders.push(b)
None => ()
}
match parse_side(xml, "bottom") {
Some(b) => borders.push(b)
None => ()
}
let diagonal = parse_side(xml, "diagonal")
match diagonal {
Some(b) =>
if diagonal_up || diagonal_down {
let diagonal_style = match b.style {
Some(value) => value
None => 0
}
if diagonal_up {
match b.color {
Some(color) =>
borders.push(
Border::with_values("diagonalUp", color~, style=diagonal_style),
)
None =>
borders.push(
Border::with_values("diagonalUp", style=diagonal_style),
)
}
}
if diagonal_down {
match b.color {
Some(color) =>
borders.push(
Border::with_values(
"diagonalDown",
color~,
style=diagonal_style,
),
)
None =>
borders.push(
Border::with_values("diagonalDown", style=diagonal_style),
)
}
}
} else {
borders.push(b)
}
None => ()
}
match parse_side(xml, "vertical") {
Some(b) => borders.push(b)
None => ()
}
match parse_side(xml, "horizontal") {
Some(b) => borders.push(b)
None => ()
}
borders
}
///|
fn parse_borders(xml : StringView) -> Array[Array[Border]] raise XlsxError {
let borders : Array[Array[Border]] = []
let body = match extract_tag_body(xml, "borders") {
Some(value) => value
None => {
borders.push([])
return borders
}
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None => continue
}
let open_tag = text[:open_end]
if open_tag.has_suffix("/") {
borders.push([])
continue
}
let rest = text[open_end + 1:]
let end = match rest.find("") {
Some(pos) => pos
None => continue
}
let content = rest[:end]
borders.push(parse_border_entry(open_tag, content))
}
if borders.length() == 0 {
borders.push([])
}
borders
}
///|
fn parse_protection_entry(xml : StringView) -> Protection? raise XlsxError {
let tag = match tag_attributes_in(xml, "protection") {
Some(value) => value
None => return None
}
let protection = Protection::new()
match attr_value(tag, "hidden") {
Some(value) => protection.hidden = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "locked") {
Some(value) => protection.locked = Some(parse_bool_attr(value))
None => ()
}
match (protection.hidden, protection.locked) {
(None, None) => None
_ => Some(protection)
}
}
///|
fn parse_alignment_entry(xml : StringView) -> Alignment? raise XlsxError {
let tag = match tag_attributes_in(xml, "alignment") {
Some(value) => value
None => return None
}
let alignment = Alignment::new()
match attr_value(tag, "horizontal") {
Some(value) => alignment.horizontal = Some(unescape_xml_text(value))
None => ()
}
match attr_value(tag, "vertical") {
Some(value) => alignment.vertical = Some(unescape_xml_text(value))
None => ()
}
match attr_value(tag, "wrapText") {
Some(value) => alignment.wrap_text = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "textRotation") {
Some(value) =>
alignment.text_rotation = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="textRotation invalid")
},
)
None => ()
}
match attr_value(tag, "indent") {
Some(value) =>
alignment.indent = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="indent invalid")
},
)
None => ()
}
match attr_value(tag, "shrinkToFit") {
Some(value) => alignment.shrink_to_fit = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "justifyLastLine") {
Some(value) => alignment.justify_last_line = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "readingOrder") {
Some(value) =>
alignment.reading_order = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="readingOrder invalid")
},
)
None => ()
}
match attr_value(tag, "relativeIndent") {
Some(value) =>
alignment.relative_indent = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="relativeIndent invalid")
},
)
None => ()
}
match
(
alignment.horizontal,
alignment.vertical,
alignment.wrap_text,
alignment.text_rotation,
alignment.indent,
alignment.shrink_to_fit,
alignment.justify_last_line,
alignment.reading_order,
alignment.relative_indent,
) {
(None, None, None, None, None, None, None, None, None) => None
_ => Some(alignment)
}
}
///|
fn parse_pivot_table_name(xml : StringView) -> String raise XlsxError {
match tag_attributes_in(xml, "pivotTableDefinition") {
Some(tag) =>
match attr_value(tag, "name") {
Some(value) => unescape_xml_text(value)
None => ""
}
None => ""
}
}
///|
fn parse_defined_names(
xml : StringView,
sheet_names : Array[String],
) -> Array[DefinedName] raise XlsxError {
let names : Array[DefinedName] = []
let body = match extract_tag_body(xml, "definedNames") {
Some(value) => value
None => return names
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None => raise InvalidXml(msg="definedName tag not closed")
}
let tag = text[:end]
let name = match attr_value(tag, "name") {
Some(value) => unescape_xml_text(value)
None => raise InvalidXml(msg="definedName name missing")
}
let comment = match attr_value(tag, "comment") {
Some(value) => unescape_xml_text(value)
None => ""
}
let local_id = match attr_value(tag, "localSheetId") {
Some(value) =>
Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="definedName localSheetId invalid")
},
)
None => None
}
let rest = text[end + 1:]
let data_end = match rest.find("") {
Some(pos) => pos
None => raise InvalidXml(msg="definedName close missing")
}
let data = rest[:data_end]
let scope = match local_id {
Some(id) =>
if id >= 0 && id < sheet_names.length() {
sheet_names[id]
} else {
raise InvalidXml(msg="definedName localSheetId missing")
}
None => "Workbook"
}
names.push({ name, refers_to: unescape_xml_text(data), scope, comment })
}
names
}
///|
fn parse_merge_cells(xml : StringView) -> Array[String] raise XlsxError {
let merged : Array[String] = []
let body = match extract_tag_body(xml, "mergeCells") {
Some(value) => value
None => return merged
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None =>
match text.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="mergeCell tag not closed")
}
}
let tag = text[:end]
let ref_value = match attr_value(tag, "ref") {
Some(value) => value
None => raise InvalidXml(msg="mergeCell ref missing")
}
let normalized = if ref_value.contains(":") {
normalize_range_ref(ref_value)
} else {
let (row, col) = cell_ref_to_rc(ref_value)
let cell_ref = cell_ref_from(row, col)
"\{cell_ref}:\{cell_ref}"
}
merged.push(normalized)
}
merged
}
///|
fn parse_data_validations(
xml : StringView,
budget? : ReadBudget,
) -> Array[String] raise XlsxError {
let validations : Array[String] = []
let body = match extract_tag_body(xml, "dataValidations") {
Some(value) => value
None => return validations
}
let mut first = true
for chunk in body.split(" {
let end = pos + close_tag.length()
let slice = text[:end]
" {
let end = match text.find("/>") {
Some(pos) => pos + 2
None => raise InvalidXml(msg="dataValidation tag not closed")
}
let slice = text[:end]
" Array[String] raise XlsxError {
let validations : Array[String] = []
let xml_str = xml.to_owned()
if !xml_str.contains(" pos + close_tag.length()
None => raise InvalidXml(msg="x14:dataValidation tag not closed")
}
let slice = text[:end]
let full = "") {
Some(pos) => pos
None => raise InvalidXml(msg="x14:dataValidation open tag missing")
}
let attrs = full[" value
None => "0"
}
let typ = match attr_value(attrs, "type") {
Some(value) => value
None => ""
}
let sqref = match extract_tag_body_from(full, "xm:sqref") {
Some(value) =>
normalize_sqref_for_read(
unescape_xml_text(value),
"x14 data validation",
budget?,
)
None => raise InvalidXml(msg="x14:dataValidation sqref missing")
}
let mut formula1 = ""
match extract_tag_body_from(full, "x14:formula1") {
Some(value) => {
let body = value.to_string()
match extract_tag_body_from(body, "xm:f") {
Some(f) => formula1 = unescape_xml_text(f)
None => formula1 = unescape_xml_text(value)
}
}
None => ()
}
let mut formula2 = ""
match extract_tag_body_from(full, "x14:formula2") {
Some(value) => {
let body = value.to_string()
match extract_tag_body_from(body, "xm:f") {
Some(f) => formula2 = unescape_xml_text(f)
None => formula2 = unescape_xml_text(value)
}
}
None => ()
}
let sb = StringBuilder::new()
sb.write_view("")
if formula1 != "" {
sb.write_view("")
sb.write_view(escape_xml_text(formula1))
sb.write_view("")
}
if formula2 != "" {
sb.write_view("")
sb.write_view(escape_xml_text(formula2))
sb.write_view("")
}
sb.write_view("")
validations.push(sb.to_string())
}
validations
}
///|
fn parse_conditional_formats(
xml : StringView,
budget? : ReadBudget,
) -> Array[String] raise XlsxError {
let formats : Array[String] = []
let xml_str = xml.to_owned()
if !xml_str.contains(" {
let end = pos + close_tag.length()
let slice = text[:end]
" {
let end = match text.find("/>") {
Some(pos) => pos + 2
None => raise InvalidXml(msg="conditionalFormatting tag not closed")
}
let slice = text[:end]
" String {
let sb = StringBuilder::new()
sb.write_view("")
let show_value = if opt.icons_only { "0" } else { "1" }
sb.write_view(
"")
if cfvos.length() > 0 {
for cfvo in cfvos {
sb.write_view(cfvo)
}
} else {
match icon_set_steps(opt.icon_style) {
Some(steps) =>
for step in steps {
sb.write_view("")
}
None => ()
}
}
sb.write_view("")
sb.write_view("")
sb.to_string()
}
///|
fn parse_x14_cfvo_values(xml : StringView) -> Array[String] raise XlsxError {
let cfvos : Array[String] = []
let xml_str = xml.to_owned()
if !xml_str.contains(" {
let end = pos + close_tag.length()
let slice = text[:end]
" {
let end = match text.find("/>") {
Some(pos) => pos + 2
None => raise InvalidXml(msg="x14:cfvo tag not closed")
}
let slice = text[:end]
"") {
Some(pos) => pos
None => raise InvalidXml(msg="x14:cfvo open tag missing")
}
let attrs = full[" value.to_string()
None => ""
}
let value = match extract_tag_body_from(full, "xm:f") {
Some(v) => unescape_xml_text(v)
None =>
match extract_tag_body_from(full, "f") {
Some(v) => unescape_xml_text(v)
None => ""
}
}
if typ == "" {
continue
}
let sb = StringBuilder::new()
sb.write_view("")
cfvos.push(sb.to_string())
}
cfvos
}
///|
fn color_text_from_rgb_x14(rgb : StringView) -> String {
let text = rgb.to_owned()
match text.strip_prefix("FF") {
Some(rest) if text.length() == 8 => "#\{rest}"
_ => "#\{text}"
}
}
///|
fn parse_x14_conditional_formatting_blocks(
xml : StringView,
) -> Array[String] raise XlsxError {
let blocks : Array[String] = []
if xml.contains("") {
raise InvalidXml(msg="x14:conditionalFormattings tag not closed")
}
let needle = "
raise InvalidXml(msg="x14:conditionalFormatting close before open")
(Some(_), None) =>
raise InvalidXml(msg="x14:conditionalFormatting close without open")
_ => ()
}
let rel_start = match rel_start {
Some(pos) => pos
None => break
}
let start = search_from + rel_start
let after_needle = start + needle.length()
if after_needle >= xml.length() {
raise InvalidXml(msg="x14:conditionalFormatting tag not closed")
}
if xml[after_needle] == ('s' : UInt16) {
search_from = after_needle
continue
}
if !is_xml_open_tag_boundary(xml[after_needle]) {
search_from = after_needle
continue
}
let open_end = match xml_open_tag_end_from(xml, after_needle) {
Some(pos) => pos
None => raise InvalidXml(msg="x14:conditionalFormatting tag not closed")
}
let self_closing = open_end > start && xml[open_end - 1] == ('/' : UInt16)
let end = if self_closing {
open_end + 1
} else {
let body_start = open_end + 1
let relative_close = match xml[body_start:].find(close_tag) {
Some(pos) => pos
None => raise InvalidXml(msg="x14:conditionalFormatting tag not closed")
}
body_start + relative_close + close_tag.length()
}
blocks.push(xml[start:end].to_owned())
search_from = end
}
blocks
}
///|
fn parse_x14_data_bars(
xml : StringView,
budget? : ReadBudget,
) -> Map[String, X14DataBarProps] raise XlsxError {
let bars : Map[String, X14DataBarProps] = Map([])
let blocks = parse_x14_conditional_formatting_blocks(xml)
if blocks.length() == 0 {
return bars
}
for full in blocks {
let sqref = match extract_tag_body_from(full, "xm:sqref") {
Some(value) =>
normalize_sqref_for_read(
unescape_xml_text(value),
"x14 conditional formatting",
budget?,
)
None =>
match extract_tag_body_from(full, "sqref") {
Some(value) =>
normalize_sqref_for_read(
unescape_xml_text(value),
"x14 conditional formatting",
budget?,
)
None => continue
}
}
if !full.contains(" {
let end = pos + rule_close.length()
let slice = rule_text[:end]
" {
let end = match rule_text.find("/>") {
Some(pos) => pos + 2
None => raise InvalidXml(msg="x14:cfRule tag not closed")
}
let slice = rule_text[:end]
"") {
Some(pos) => pos
None => continue
}
let attrs = rule_full[" value.to_string()
None => ""
}
if typ != "dataBar" {
continue
}
let id = match attr_value(attrs, "id") {
Some(value) => value.to_string()
None => continue
}
let mut bar_direction = ""
let mut bar_solid = false
let mut bar_border_color = ""
match tag_attributes_in(rule_full, "x14:dataBar") {
Some(tag) => {
match attr_value(tag, "direction") {
Some(value) => bar_direction = value.to_string()
None => ()
}
match attr_value(tag, "gradient") {
Some(value) => bar_solid = !parse_xml_bool(value)
None => ()
}
}
None => ()
}
match tag_attributes_in(rule_full, "x14:borderColor") {
Some(tag) =>
match attr_value(tag, "rgb") {
Some(value) => bar_border_color = color_text_from_rgb_x14(value)
None => ()
}
None => ()
}
bars[id] = { sqref, bar_direction, bar_solid, bar_border_color }
}
}
bars
}
///|
fn worksheet_ext_uri_is_known(uri : StringView) -> Bool {
match uri {
"{78C0D931-6437-407d-A8EE-F0AAD7539E65}" => true
"{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" => true
"{A8765BA9-456A-4dab-B4F3-ACF838C121DE}" => true
"{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}" => true
_ => false
}
}
///|
fn parse_unknown_worksheet_ext_blocks(
xml : StringView,
) -> Array[String] raise XlsxError {
let unknown : Array[String] = []
let text = xml.to_owned()
let ext_start = match text.rev_find(" pos
None => return unknown
}
let ext_tail = text[ext_start:]
let open_end = match ext_tail.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="worksheet extLst open tag invalid")
}
let close_rel = match ext_tail.find("") {
Some(pos) => pos
None => raise InvalidXml(msg="worksheet extLst close tag missing")
}
let body = ext_tail[open_end + 1:close_rel]
let mut first = true
for chunk in body.to_owned().split("") {
Some(pos) => pos + "".length()
None =>
match full.find("/>") {
Some(pos) => pos + 2
None => raise InvalidXml(msg="worksheet ext block not closed")
}
}
let block = full[:end]
let open_end = match block.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="worksheet ext open tag invalid")
}
let open_tag = block[:open_end]
let uri = match attr_value(open_tag, "uri") {
Some(value) => value.to_string()
None => ""
}
if worksheet_ext_uri_is_known(uri) {
continue
}
if block.contains(" Array[String] raise XlsxError {
let formats : Array[String] = []
let blocks = parse_x14_conditional_formatting_blocks(xml)
if blocks.length() == 0 {
return formats
}
for full in blocks {
let sqref = match extract_tag_body_from(full, "xm:sqref") {
Some(value) =>
normalize_sqref_for_read(
unescape_xml_text(value),
"x14 conditional formatting",
budget?,
)
None =>
match extract_tag_body_from(full, "sqref") {
Some(value) =>
normalize_sqref_for_read(
unescape_xml_text(value),
"x14 conditional formatting",
budget?,
)
None =>
raise InvalidXml(msg="x14:conditionalFormatting sqref missing")
}
}
let rules : Array[String] = []
if full.contains(" {
let end = pos + rule_close.length()
let slice = rule_text[:end]
" {
let end = match rule_text.find("/>") {
Some(pos) => pos + 2
None => raise InvalidXml(msg="x14:cfRule tag not closed")
}
let slice = rule_text[:end]
"") {
Some(pos) => pos
None => continue
}
let attrs = rule_full[" value.to_string()
None => ""
}
if typ != "iconSet" {
continue
}
let mut priority = match attr_value(attrs, "priority") {
Some(value) =>
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="x14:cfRule priority invalid")
}
None => 1
}
if priority <= 0 {
priority = 1
}
let stop_if_true = match attr_value(attrs, "stopIfTrue") {
Some(value) => parse_xml_bool(value)
None => false
}
let opt = ConditionalFormatOptions::new("icon_set")
opt.stop_if_true = stop_if_true
match tag_attributes_in(rule_full, "x14:iconSet") {
Some(tag) => {
match attr_value(tag, "iconSet") {
Some(value) => opt.icon_style = value.to_string()
None => ()
}
match attr_value(tag, "reverse") {
Some(value) => opt.reverse_icons = parse_xml_bool(value)
None => ()
}
let show_value = match attr_value(tag, "showValue") {
Some(value) => parse_xml_bool(value)
None => true
}
opt.icons_only = !show_value
match attr_value(tag, "percent") {
Some(value) => opt.percent = parse_xml_bool(value)
None =>
match attr_value(attrs, "percent") {
Some(value) => opt.percent = parse_xml_bool(value)
None => ()
}
}
}
None => continue
}
let cfvos = parse_x14_cfvo_values(rule_full)
rules.push(build_icon_set_rule_xml_from_x14(opt, priority, cfvos))
}
}
if rules.length() > 0 {
formats.push(conditional_format_xml(sqref, rules))
}
}
formats
}
///|
fn parse_sheet_dimension(xml : StringView) -> String? 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 =>
match rest.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="dimension tag not closed")
}
}
let tag = rest[:end]
match attr_value(tag, "ref") {
Some(value) =>
Some(
validate_cell_or_range_ref_for_read(
unescape_xml_text(value),
"dimension",
),
)
None => None
}
}
}
}
///|
fn parse_sheet_view_pane(body : StringView) -> SheetPane? raise XlsxError {
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None =>
match text.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="pane tag not closed")
}
}
let tag = text[:end]
let pane = SheetPane::new()
match attr_value(tag, "activePane") {
Some(value) => pane.active_pane = unescape_xml_text(value)
None => ()
}
match attr_value(tag, "state") {
Some(value) => pane.state = unescape_xml_text(value)
None => ()
}
match attr_value(tag, "topLeftCell") {
Some(value) =>
pane.top_left_cell = validate_cell_ref_for_read(
unescape_xml_text(value),
"pane topLeftCell",
)
None => ()
}
match attr_value(tag, "xSplit") {
Some(value) =>
pane.x_split = @string.parse_double(value) catch {
_ => raise InvalidXml(msg="pane xSplit invalid")
}
None => ()
}
match attr_value(tag, "ySplit") {
Some(value) =>
pane.y_split = @string.parse_double(value) catch {
_ => raise InvalidXml(msg="pane ySplit invalid")
}
None => ()
}
return Some(pane)
}
None
}
///|
fn parse_sheet_view_selections(
body : StringView,
budget? : ReadBudget,
) -> Array[Selection] raise XlsxError {
let selections : Array[Selection] = []
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None =>
match text.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="selection tag not closed")
}
}
let tag = text[:end]
let selection = { sqref: "", active_cell: "", pane: "" }
match attr_value(tag, "sqref") {
Some(value) =>
selection.sqref = normalize_sqref_for_read(
unescape_xml_text(value),
"selection",
budget?,
)
None => ()
}
match attr_value(tag, "activeCell") {
Some(value) =>
selection.active_cell = validate_cell_ref_for_read(
unescape_xml_text(value),
"selection activeCell",
)
None => ()
}
match attr_value(tag, "pane") {
Some(value) => selection.pane = unescape_xml_text(value)
None => ()
}
selections.push(selection)
}
selections
}
///|
fn parse_sheet_views(
xml : StringView,
budget? : ReadBudget,
) -> Array[SheetView] raise XlsxError {
let views : Array[SheetView] = []
let body = match extract_tag_body(xml, "sheetViews") {
Some(value) => value
None => return views
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None => raise InvalidXml(msg="sheetView tag not closed")
}
let tag = text[:end]
let view = SheetView::new()
match attr_value(tag, "defaultGridColor") {
Some(value) => view.default_grid_color = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "rightToLeft") {
Some(value) => view.right_to_left = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "showFormulas") {
Some(value) => view.show_formulas = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "showGridLines") {
Some(value) => view.show_grid_lines = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "showRowColHeaders") {
Some(value) => view.show_row_col_headers = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "showRuler") {
Some(value) => view.show_ruler = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "showZeros") {
Some(value) => view.show_zeros = Some(parse_cell_bool(value))
None => ()
}
match attr_value(tag, "topLeftCell") {
Some(value) =>
view.top_left_cell = Some(
validate_cell_ref_for_read(
unescape_xml_text(value),
"sheetView topLeftCell",
),
)
None => ()
}
match attr_value(tag, "view") {
Some(value) => view.view = Some(unescape_xml_text(value))
None => ()
}
match attr_value(tag, "zoomScale") {
Some(value) =>
view.zoom_scale = Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="sheetView zoomScale invalid")
},
)
None => ()
}
match attr_value(tag, "tabSelected") {
Some(value) => view.tab_selected = parse_cell_bool(value)
None => ()
}
match attr_value(tag, "workbookViewId") {
Some(value) =>
view.workbook_view_id = @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="sheetView workbookViewId invalid")
}
None => ()
}
let self_closing = tag.to_owned().trim().has_suffix("/")
if !self_closing {
let close = match text.find("") {
Some(pos) => pos
None => raise InvalidXml(msg="sheetView close missing")
}
let body_start = end + 1
if close > body_start {
let inner = text[body_start:close]
view.pane = parse_sheet_view_pane(inner)
let selections = parse_sheet_view_selections(inner, budget?)
view.selection.clear()
view.selection.append(selections)
}
}
views.push(view)
}
views
}
///|
fn parse_sheet_props(xml : StringView) -> SheetPropsOptions? raise XlsxError {
let mut options : SheetPropsOptions? = None
match tag_attributes_in(xml, "sheetPr") {
Some(tag) => {
let opts = SheetPropsOptions::new()
match attr_value(tag, "codeName") {
Some(value) => opts.code_name = Some(unescape_xml_text(value))
None => ()
}
match attr_value(tag, "enableFormatConditionsCalculation") {
Some(value) =>
opts.enable_format_conditions_calculation = Some(
parse_bool_attr(value),
)
None => ()
}
match attr_value(tag, "published") {
Some(value) => opts.published = Some(parse_bool_attr(value))
None => ()
}
options = Some(opts)
}
None => ()
}
match tag_attributes_in(xml, "tabColor") {
Some(tag) => {
let opts = match options {
Some(value) => value
None => SheetPropsOptions::new()
}
match attr_value(tag, "indexed") {
Some(value) =>
opts.tab_color_indexed = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="tabColor indexed invalid")
},
)
None => ()
}
match attr_value(tag, "rgb") {
Some(value) => opts.tab_color_rgb = Some(value.to_string())
None => ()
}
match attr_value(tag, "theme") {
Some(value) =>
opts.tab_color_theme = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="tabColor theme invalid")
},
)
None => ()
}
match attr_value(tag, "tint") {
Some(value) =>
opts.tab_color_tint = Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="tabColor tint invalid")
},
)
None => ()
}
options = Some(opts)
}
None => ()
}
match tag_attributes_in(xml, "outlinePr") {
Some(tag) => {
let opts = match options {
Some(value) => value
None => SheetPropsOptions::new()
}
match attr_value(tag, "summaryBelow") {
Some(value) => opts.outline_summary_below = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "summaryRight") {
Some(value) => opts.outline_summary_right = Some(parse_bool_attr(value))
None => ()
}
options = Some(opts)
}
None => ()
}
match tag_attributes_in(xml, "pageSetUpPr") {
Some(tag) => {
let opts = match options {
Some(value) => value
None => SheetPropsOptions::new()
}
match attr_value(tag, "autoPageBreaks") {
Some(value) => opts.auto_page_breaks = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "fitToPage") {
Some(value) => opts.fit_to_page = Some(parse_bool_attr(value))
None => ()
}
options = Some(opts)
}
None => ()
}
match tag_attributes_in(xml, "sheetFormatPr") {
Some(tag) => {
let opts = match options {
Some(value) => value
None => SheetPropsOptions::new()
}
match attr_value(tag, "baseColWidth") {
Some(value) =>
opts.base_col_width = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="sheetFormatPr baseColWidth invalid")
},
)
None => ()
}
match attr_value(tag, "defaultColWidth") {
Some(value) =>
opts.default_col_width = Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="sheetFormatPr defaultColWidth invalid")
},
)
None => ()
}
match attr_value(tag, "defaultRowHeight") {
Some(value) =>
opts.default_row_height = Some(
@string.parse_double(value) catch {
_ =>
raise InvalidXml(msg="sheetFormatPr defaultRowHeight invalid")
},
)
None => ()
}
match attr_value(tag, "customHeight") {
Some(value) => opts.custom_height = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "zeroHeight") {
Some(value) => opts.zero_height = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "thickTop") {
Some(value) => opts.thick_top = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "thickBottom") {
Some(value) => opts.thick_bottom = Some(parse_bool_attr(value))
None => ()
}
options = Some(opts)
}
None => ()
}
match options {
Some(value) => if sheet_props_is_empty(value) { None } else { Some(value) }
None => None
}
}
///|
fn parse_page_margins(
xml : StringView,
) -> PageLayoutMarginsOptions? raise XlsxError {
let mut options : PageLayoutMarginsOptions? = None
match tag_attributes_in(xml, "pageMargins") {
Some(tag) => {
let opts = PageLayoutMarginsOptions::new()
match attr_value(tag, "left") {
Some(value) =>
opts.left = Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="pageMargins left invalid")
},
)
None => ()
}
match attr_value(tag, "right") {
Some(value) =>
opts.right = Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="pageMargins right invalid")
},
)
None => ()
}
match attr_value(tag, "top") {
Some(value) =>
opts.top = Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="pageMargins top invalid")
},
)
None => ()
}
match attr_value(tag, "bottom") {
Some(value) =>
opts.bottom = Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="pageMargins bottom invalid")
},
)
None => ()
}
match attr_value(tag, "header") {
Some(value) =>
opts.header = Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="pageMargins header invalid")
},
)
None => ()
}
match attr_value(tag, "footer") {
Some(value) =>
opts.footer = Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="pageMargins footer invalid")
},
)
None => ()
}
options = Some(opts)
}
None => ()
}
match tag_attributes_in(xml, "printOptions") {
Some(tag) => {
let opts = match options {
Some(value) => value
None => PageLayoutMarginsOptions::new()
}
match attr_value(tag, "horizontalCentered") {
Some(value) => opts.horizontally = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "verticalCentered") {
Some(value) => opts.vertically = Some(parse_bool_attr(value))
None => ()
}
options = Some(opts)
}
None => ()
}
options
}
///|
fn parse_page_layout(xml : StringView) -> PageLayoutOptions? raise XlsxError {
let tag = match tag_attributes_in(xml, "pageSetup") {
Some(value) => value
None => return None
}
let opts = PageLayoutOptions::new()
match attr_value(tag, "paperSize") {
Some(value) =>
opts.size = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="pageSetup paperSize invalid")
},
)
None => ()
}
match attr_value(tag, "orientation") {
Some(value) => opts.orientation = Some(value.to_string())
None => ()
}
match attr_value(tag, "firstPageNumber") {
Some(value) =>
opts.first_page_number = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="pageSetup firstPageNumber invalid")
},
)
None => ()
}
match attr_value(tag, "scale") {
Some(value) =>
opts.adjust_to = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="pageSetup scale invalid")
},
)
None => ()
}
match attr_value(tag, "fitToHeight") {
Some(value) =>
opts.fit_to_height = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="pageSetup fitToHeight invalid")
},
)
None => ()
}
match attr_value(tag, "fitToWidth") {
Some(value) =>
opts.fit_to_width = Some(
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="pageSetup fitToWidth invalid")
},
)
None => ()
}
match attr_value(tag, "blackAndWhite") {
Some(value) => opts.black_and_white = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "pageOrder") {
Some(value) => opts.page_order = Some(value.to_string())
None => ()
}
Some(opts)
}
///|
fn parse_header_footer(
xml : StringView,
) -> HeaderFooterOptions? raise XlsxError {
let tag = match tag_attributes_in(xml, "headerFooter") {
Some(value) => value
None => return None
}
let options = HeaderFooterOptions::new()
match attr_value(tag, "differentOddEven") {
Some(value) => options.different_odd_even = parse_bool_attr(value)
None => ()
}
match attr_value(tag, "differentFirst") {
Some(value) => options.different_first = parse_bool_attr(value)
None => ()
}
match attr_value(tag, "scaleWithDoc") {
Some(value) => options.scale_with_doc = Some(parse_bool_attr(value))
None => ()
}
match attr_value(tag, "alignWithMargins") {
Some(value) => options.align_with_margins = Some(parse_bool_attr(value))
None => ()
}
let body = match extract_tag_body(xml, "headerFooter") {
Some(value) => value
None => ""
}
match extract_tag_body_from(body, "oddHeader") {
Some(value) => options.odd_header = unescape_xml_text(value)
None => ()
}
match extract_tag_body_from(body, "oddFooter") {
Some(value) => options.odd_footer = unescape_xml_text(value)
None => ()
}
match extract_tag_body_from(body, "evenHeader") {
Some(value) => options.even_header = unescape_xml_text(value)
None => ()
}
match extract_tag_body_from(body, "evenFooter") {
Some(value) => options.even_footer = unescape_xml_text(value)
None => ()
}
match extract_tag_body_from(body, "firstHeader") {
Some(value) => options.first_header = unescape_xml_text(value)
None => ()
}
match extract_tag_body_from(body, "firstFooter") {
Some(value) => options.first_footer = unescape_xml_text(value)
None => ()
}
Some(options)
}
///|
fn parse_sheet_protection(xml : StringView) -> SheetProtection? raise XlsxError {
let tag = match tag_attributes_in(xml, "sheetProtection") {
Some(value) => value
None => return None
}
let mut spin_count = 0
match attr_value(tag, "spinCount") {
Some(value) =>
spin_count = @string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="sheetProtection spinCount invalid")
}
None => ()
}
Some({
algorithm_name: match attr_value(tag, "algorithmName") {
Some(value) => value.to_string()
None => ""
},
password: match attr_value(tag, "password") {
Some(value) => value.to_string()
None => ""
},
hash_value: match attr_value(tag, "hashValue") {
Some(value) => value.to_string()
None => ""
},
salt_value: match attr_value(tag, "saltValue") {
Some(value) => value.to_string()
None => ""
},
spin_count,
sheet: match attr_value(tag, "sheet") {
Some(value) => parse_bool_attr(value)
None => false
},
objects: match attr_value(tag, "objects") {
Some(value) => parse_bool_attr(value)
None => false
},
scenarios: match attr_value(tag, "scenarios") {
Some(value) => parse_bool_attr(value)
None => false
},
format_cells: match attr_value(tag, "formatCells") {
Some(value) => parse_bool_attr(value)
None => false
},
format_columns: match attr_value(tag, "formatColumns") {
Some(value) => parse_bool_attr(value)
None => false
},
format_rows: match attr_value(tag, "formatRows") {
Some(value) => parse_bool_attr(value)
None => false
},
insert_columns: match attr_value(tag, "insertColumns") {
Some(value) => parse_bool_attr(value)
None => false
},
insert_rows: match attr_value(tag, "insertRows") {
Some(value) => parse_bool_attr(value)
None => false
},
insert_hyperlinks: match attr_value(tag, "insertHyperlinks") {
Some(value) => parse_bool_attr(value)
None => false
},
delete_columns: match attr_value(tag, "deleteColumns") {
Some(value) => parse_bool_attr(value)
None => false
},
delete_rows: match attr_value(tag, "deleteRows") {
Some(value) => parse_bool_attr(value)
None => false
},
select_locked_cells: match attr_value(tag, "selectLockedCells") {
Some(value) => parse_bool_attr(value)
None => false
},
sort: match attr_value(tag, "sort") {
Some(value) => parse_bool_attr(value)
None => false
},
auto_filter: match attr_value(tag, "autoFilter") {
Some(value) => parse_bool_attr(value)
None => false
},
pivot_tables: match attr_value(tag, "pivotTables") {
Some(value) => parse_bool_attr(value)
None => false
},
select_unlocked_cells: match attr_value(tag, "selectUnlockedCells") {
Some(value) => parse_bool_attr(value)
None => false
},
})
}
///|
fn parse_page_breaks(
xml : StringView,
tag_name : StringView,
) -> Array[PageBreak] raise XlsxError {
let breaks : Array[PageBreak] = []
let body = match extract_tag_body(xml, tag_name) {
Some(value) => value
None => return breaks
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None =>
match text.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="brk tag not closed")
}
}
let tag = text[:end]
let id = match attr_value(tag, "id") {
Some(value) =>
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="brk id invalid")
}
None => raise InvalidXml(msg="brk id missing")
}
let min = match attr_value(tag, "min") {
Some(value) =>
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="brk min invalid")
}
None => 0
}
let max = match attr_value(tag, "max") {
Some(value) =>
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="brk max invalid")
}
None => 0
}
let (id_limit, span_limit) = match tag_name {
"rowBreaks" => (max_page_break_rows, max_page_break_cols)
"colBreaks" => (max_page_break_cols, max_page_break_rows)
_ => raise InvalidXml(msg="page break axis invalid")
}
if id < 0 ||
id > id_limit ||
min < 0 ||
min > span_limit ||
max < 0 ||
max > span_limit ||
min > max {
raise InvalidXml(msg="brk coordinate invalid")
}
let manual = match attr_value(tag, "man") {
Some(value) => parse_bool_attr(value)
None => false
}
breaks.push({ id, min, max, manual })
}
breaks
}
///|
fn parse_picture_rel_id(xml : StringView) -> String? raise XlsxError {
let tag = match tag_attributes_in(xml, "picture") {
Some(value) => value
None => return None
}
match attr_value(tag, "r:id") {
Some(value) => Some(value.to_string())
None => raise InvalidXml(msg="picture relationship missing")
}
}
///|
fn parse_auto_filter_columns(
body : StringView,
min_col : Int,
max_col : Int,
) -> Array[AutoFilterColumn] raise XlsxError {
let columns : Array[AutoFilterColumn] = []
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None => raise InvalidXml(msg="filterColumn tag not closed")
}
let tag = text[:end]
let col_id = match attr_value(tag, "colId") {
Some(value) =>
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="filterColumn colId invalid")
}
None => raise InvalidXml(msg="filterColumn colId missing")
}
if col_id < 0 || col_id > max_col - min_col {
raise InvalidXml(msg="filterColumn colId out of range")
}
let col = min_col + col_id
let mut filters : Array[String]? = None
let mut custom_filters : AutoFilterCustomFilters? = None
if !tag.has_suffix("/") {
let rest = text[end + 1:]
let body_end = match rest.find("") {
Some(pos) => pos
None => 0
}
let column_body = if body_end > 0 { rest[:body_end] } else { "" }
match extract_tag_body_from(column_body, "filters") {
Some(filters_body) => {
let values : Array[String] = []
let mut first_filter = true
for part in filters_body.split("") {
Some(pos) => pos
None =>
match filter_text.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="filter tag not closed")
}
}
let filter_tag = filter_text[:filter_end]
match attr_value(filter_tag, "val") {
Some(value) => values.push(unescape_xml_text(value))
None => values.push("")
}
}
filters = Some(values)
}
None => ()
}
match extract_tag_body_from(column_body, "customFilters") {
Some(custom_body) => {
let and_value = match
tag_attributes_in(column_body, "customFilters") {
Some(custom_tag) =>
match attr_value(custom_tag, "and") {
Some(value) => parse_bool_attr(value)
None => false
}
None => false
}
let entries : Array[AutoFilterCustomFilter] = []
let mut first_custom = true
for part in custom_body.split("") {
Some(pos) => pos
None =>
match custom_text.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="customFilter tag not closed")
}
}
let custom_tag = custom_text[:custom_end]
let operator = match attr_value(custom_tag, "operator") {
Some(value) => value
None => "equal"
}
let val = match attr_value(custom_tag, "val") {
Some(value) => unescape_xml_text(value)
None => ""
}
entries.push({ operator, value: val })
}
custom_filters = Some({ and_filter: and_value, filters: entries })
}
None => ()
}
}
columns.push({ col, filters, custom_filters })
}
columns
}
///|
fn parse_auto_filter(xml : StringView) -> AutoFilter? raise XlsxError {
let tag = match tag_attributes_in(xml, "autoFilter") {
Some(value) => value
None => return None
}
let ref_value = match attr_value(tag, "ref") {
Some(value) => value
None => raise InvalidXml(msg="autoFilter ref missing")
}
let range_ref = normalize_range_ref(ref_value)
let (_min_row, min_col, _max_row, max_col) = parse_range_ref(range_ref)
let self_closing = tag.trim().has_suffix("/")
let columns = if self_closing {
[]
} else {
match extract_tag_body(xml, "autoFilter") {
Some(body) => parse_auto_filter_columns(body, min_col, max_col)
None => []
}
}
Some({ range_ref, columns })
}
///|
priv struct HyperlinkElement {
reference : String
r_id : String?
location : String?
display : String?
tooltip : String?
}
///|
fn parse_hyperlink_elements(
xml : StringView,
) -> Array[HyperlinkElement] raise XlsxError {
let links : Array[HyperlinkElement] = []
let body = match extract_tag_body(xml, "hyperlinks") {
Some(value) => value
None => return links
}
let mut first = true
for chunk in body.split(" pos
None => raise InvalidXml(msg="hyperlink tag not closed")
}
let tag = text[:end]
let raw_ref = match attr_value(tag, "ref") {
Some(value) => value
None => raise InvalidXml(msg="hyperlink ref missing")
}
let reference = normalize_cell_or_range_ref(unescape_xml_text(raw_ref)) catch {
_ => raise InvalidXml(msg="hyperlink reference invalid")
}
let location = match attr_value(tag, "location") {
Some(value) => Some(unescape_xml_text(value))
None => None
}
let display = match attr_value(tag, "display") {
Some(value) => Some(unescape_xml_text(value))
None => None
}
let tooltip = match attr_value(tag, "tooltip") {
Some(value) => Some(unescape_xml_text(value))
None => None
}
let r_id = match attr_value(tag, "r:id") {
Some(value) => Some(value.to_string())
None => None
}
links.push({ reference, r_id, location, display, tooltip })
}
links
}
///|
fn parse_sheet_drawing_rel_ids(
xml : StringView,
root_local_name : StringView,
budget? : ReadBudget,
cancelled? : () -> Bool = () => false,
) -> (String?, String?, String?) raise XlsxError {
match budget {
Some(value) => {
value.checkpoint()
value.charge_work(xml.length())
}
None => check_read_cancelled(cancelled)
}
let scanner = @ooxml.XmlStartTagScanner::new(xml, cancelled~)
if !workbook_scanner_next(scanner) ||
scanner.depth() != 1 ||
scanner.local_name() != root_local_name ||
(
scanner.namespace_uri() != transitional_spreadsheet_namespace &&
scanner.namespace_uri() != strict_spreadsheet_namespace
) {
raise InvalidXml(
msg="\{root_local_name.to_owned()} document element is invalid",
)
}
let spreadsheet_namespace = scanner.namespace_uri().to_owned()
let (relationship_namespace, other_relationship_namespace) = if spreadsheet_namespace ==
strict_spreadsheet_namespace {
(
strict_relationship_attribute_namespace, transitional_relationship_attribute_namespace,
)
} else {
(
transitional_relationship_attribute_namespace, strict_relationship_attribute_namespace,
)
}
let mut drawing : String? = None
let mut legacy_drawing : String? = None
let mut legacy_drawing_hf : String? = None
while workbook_scanner_next(scanner) {
let local_name = scanner.local_name()
let selected = local_name == "drawing" ||
(
root_local_name == "worksheet" &&
(local_name == "legacyDrawing" || local_name == "legacyDrawingHF")
)
if !selected ||
(
scanner.namespace_uri() != transitional_spreadsheet_namespace &&
scanner.namespace_uri() != strict_spreadsheet_namespace
) {
continue
}
if scanner.namespace_uri() != spreadsheet_namespace ||
scanner.depth() != 2 ||
scanner.parent_local_name() != Some(root_local_name) ||
scanner.parent_namespace_uri() != Some(spreadsheet_namespace) {
raise InvalidXml(
msg="\{local_name.to_owned()} relationship element is misplaced",
)
}
if workbook_scanner_attribute(scanner, other_relationship_namespace, "id")
is Some(_) {
raise InvalidXml(
msg="\{local_name.to_owned()} relationship namespace is invalid",
)
}
let id = match
workbook_scanner_attribute(scanner, relationship_namespace, "id") {
Some(value) if value != "" => value
_ =>
raise InvalidXml(
msg="\{local_name.to_owned()} relationship id is missing",
)
}
if local_name == "drawing" {
if drawing is Some(_) {
raise InvalidXml(msg="drawing relationship element is duplicated")
}
drawing = Some(id)
} else if local_name == "legacyDrawing" {
if legacy_drawing is Some(_) {
raise InvalidXml(msg="legacyDrawing relationship element is duplicated")
}
legacy_drawing = Some(id)
} else {
if legacy_drawing_hf is Some(_) {
raise InvalidXml(
msg="legacyDrawingHF relationship element is duplicated",
)
}
legacy_drawing_hf = Some(id)
}
}
(drawing, legacy_drawing, legacy_drawing_hf)
}
///|
test "sheet drawing relationships require direct dialect-matched roots" {
let transitional =
#|
let (drawing, legacy, header_footer) = parse_sheet_drawing_rel_ids(
transitional, "worksheet",
)
assert_true(drawing == Some("drawing"))
assert_true(legacy == Some("legacy"))
assert_true(header_footer == Some("header-footer"))
let strict =
#|
let (strict_drawing, strict_legacy, strict_header_footer) = parse_sheet_drawing_rel_ids(
strict, "chartsheet",
)
assert_true(strict_drawing == Some("strict-drawing"))
assert_true(strict_legacy is None)
assert_true(strict_header_footer is None)
let invalid : Array[(String, String, String)] = [
(
(
#|
),
"worksheet",
"drawing relationship element is misplaced",
),
(
(
#|
),
"worksheet",
"drawing relationship element is misplaced",
),
(
(
#|
),
"chartsheet",
"drawing relationship namespace is invalid",
),
(
(
#|
),
"chartsheet",
"drawing relationship element is duplicated",
),
]
for entry in invalid {
let (xml, root, expected) = entry
try parse_sheet_drawing_rel_ids(xml, root) catch {
InvalidXml(msg~) => assert_eq(msg, expected)
_ => fail("unexpected sheet drawing relationship error")
} noraise {
_ => fail("expected invalid sheet drawing relationship rejection")
}
}
}
///|
fn parse_bool_attr(value : StringView) -> Bool {
match value {
"1" | "true" | "TRUE" => true
_ => false
}
}
///|
fn parse_inline_string(rest : StringView) -> InlineStringEntry raise XlsxError {
let text = rest.to_owned()
let start = match text.find(" pos
None => return { text: "", runs: None }
}
let after_start = text[start:]
let open_end = match after_start.find(">") {
Some(pos) => pos
None => raise InvalidXml(msg="inline string not closed")
}
let body_start = start + open_end + 1
let end = match find_xml_close_tag_from(text, "is", body_start) {
Some((pos, _)) => pos
None => raise InvalidXml(msg="inline string end missing")
}
let body = text[body_start:end]
let runs = if body.contains(" [] }
if parsed.length() > 0 {
Some(parsed)
} else {
None
}
} else {
None
}
{ text: parse_text_nodes(body), runs }
}
///|
fn parse_row_dimensions(
xml : StringView,
style_count : Int,
materialized_dimensions : Ref[Int],
max_materialized_dimensions : Int,
dimension_work : Ref[Int],
max_dimension_work : Int,
) -> Map[Int, RowDimension] raise XlsxError {
let dims : Map[Int, RowDimension] = Map([])
let mut first = true
for chunk in xml.split("") {
Some(pos) => pos
None => raise InvalidXml(msg="row tag not closed")
}
let tag = text[:end]
let row_text = match attr_value(tag, "r") {
Some(value) => value
None => raise InvalidXml(msg="row index missing")
}
let row = @string.parse_int(row_text, base=10) catch {
_ => raise InvalidXml(msg="row index invalid")
}
if row <= 0 || row > cell_ref_max_rows {
raise InvalidXml(msg="row index invalid")
}
let height = match attr_value(tag, "ht") {
Some(value) =>
Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="row height invalid")
},
)
None => None
}
let hidden = match attr_value(tag, "hidden") {
Some(value) => parse_bool_attr(value)
None => false
}
let outline_level = match attr_value(tag, "outlineLevel") {
Some(value) =>
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="row outline level invalid")
}
None => 0
}
let style_attribute = attr_value(tag, "s")
let style_id = match style_attribute {
Some(value) =>
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="row style id invalid")
}
None => 0
}
if style_id < 0 || style_id >= style_count {
raise InvalidStyleId(index=style_id)
}
// `s` is only an active row format when `customFormat` is true. Keep
// `Some(0)` for that case: an explicit default row format is a real
// inheritance boundary and must block a nonzero column style.
let custom_format = match attr_value(tag, "customFormat") {
Some(value) => parse_bool_attr(value)
None => false
}
let style = if custom_format { Some(style_id) } else { None }
if height is None && !hidden && outline_level == 0 && style is None {
continue
}
if dimension_work.val >= max_dimension_work {
raise ResourceLimitExceeded(
kind="row_column_dimension_work",
limit=max_dimension_work,
actual=bounded_actual_above_limit(max_dimension_work),
)
}
dimension_work.val = dimension_work.val + 1
if !dims.contains(row) {
if materialized_dimensions.val >= max_materialized_dimensions {
raise ResourceLimitExceeded(
kind="materialized_row_column_dimensions",
limit=max_materialized_dimensions,
actual=bounded_actual_above_limit(max_materialized_dimensions),
)
}
let next = materialized_dimensions.val + 1
materialized_dimensions.val = next
}
dims[row] = { height, hidden, outline_level, style_id: style }
}
dims
}
///|
fn parse_col_dimensions(
xml : StringView,
style_count : Int,
materialized_dimensions : Ref[Int],
max_materialized_dimensions : Int,
dimension_work : Ref[Int],
max_dimension_work : Int,
) -> Map[Int, ColDimension] raise XlsxError {
let dims : Map[Int, ColDimension] = Map([])
let body = match extract_tag_body(xml, "cols") {
Some(value) => value
None => return dims
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None => raise InvalidXml(msg="col tag not closed")
}
let tag = text[:end]
let min_text = match attr_value(tag, "min") {
Some(value) => value
None => raise InvalidXml(msg="col min missing")
}
let max_text = match attr_value(tag, "max") {
Some(value) => value
None => raise InvalidXml(msg="col max missing")
}
let min_col = @string.parse_int(min_text, base=10) catch {
_ => raise InvalidXml(msg="col min invalid")
}
let max_col = @string.parse_int(max_text, base=10) catch {
_ => raise InvalidXml(msg="col max invalid")
}
if min_col <= 0 ||
max_col <= 0 ||
max_col < min_col ||
min_col > cell_ref_max_cols ||
max_col > cell_ref_max_cols {
raise InvalidXml(msg="col range invalid")
}
let width = match attr_value(tag, "width") {
Some(value) =>
Some(
@string.parse_double(value) catch {
_ => raise InvalidXml(msg="col width invalid")
},
)
None => None
}
let hidden = match attr_value(tag, "hidden") {
Some(value) => parse_bool_attr(value)
None => false
}
let outline_level = match attr_value(tag, "outlineLevel") {
Some(value) =>
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="col outline level invalid")
}
None => 0
}
let style_attribute = attr_value(tag, "style")
let style_id = match style_attribute {
Some(value) =>
@string.parse_int(value, base=10) catch {
_ => raise InvalidXml(msg="col style id invalid")
}
None => 0
}
if style_id < 0 || style_id >= style_count {
raise InvalidStyleId(index=style_id)
}
// Unlike rows, columns have no `customFormat` gate. Attribute presence is
// therefore the semantic distinction, including an explicit style 0.
let style = match style_attribute {
Some(_) => Some(style_id)
None => None
}
if width is None && !hidden && outline_level == 0 && style is None {
continue
}
let span = max_col - min_col + 1
if span > max_dimension_work - dimension_work.val {
raise ResourceLimitExceeded(
kind="row_column_dimension_work",
limit=max_dimension_work,
actual=bounded_actual_above_limit(max_dimension_work),
)
}
dimension_work.val = dimension_work.val + span
let dim = { width, hidden, outline_level, style_id: style }
let mut col = min_col
while col <= max_col {
if !dims.contains(col) {
if materialized_dimensions.val >= max_materialized_dimensions {
raise ResourceLimitExceeded(
kind="materialized_row_column_dimensions",
limit=max_materialized_dimensions,
actual=bounded_actual_above_limit(max_materialized_dimensions),
)
}
let next = materialized_dimensions.val + 1
materialized_dimensions.val = next
}
dims[col] = dim
col = col + 1
}
}
dims
}
///|
fn is_xml_package_part(name : StringView) -> Bool {
let lower = name.to_owned().to_lower()
lower.has_suffix(".xml") ||
lower.has_suffix(".rels") ||
lower.has_suffix(".vml")
}
///|
fn enforce_archive_limits(
archive : @zip.Archive,
limits : ReadLimits,
cancelled? : () -> Bool = () => false,
) -> Unit raise XlsxError {
check_read_cancelled(cancelled)
let mut total_size = 0
for entry in archive.entries() {
check_read_cancelled(cancelled)
let size = entry.data().length()
if size > limits.max_entry_uncompressed_bytes {
raise ResourceLimitExceeded(
kind="entry_uncompressed_bytes",
limit=limits.max_entry_uncompressed_bytes,
actual=size,
)
}
total_size = total_size + size
if total_size > limits.max_total_uncompressed_bytes {
raise ResourceLimitExceeded(
kind="total_uncompressed_bytes",
limit=limits.max_total_uncompressed_bytes,
actual=total_size,
)
}
if is_xml_package_part(entry.name()) && size > limits.max_xml_part_bytes {
raise ResourceLimitExceeded(
kind="xml_part_bytes",
limit=limits.max_xml_part_bytes,
actual=size,
)
}
let actual_crc = @zip.crc32_cancellable(entry.data(), cancelled~) catch {
ReadCancelled => raise ReadCancelled
_ => raise InvalidPackage(msg="ZIP entry CRC verification failed")
}
if actual_crc != entry.crc32() {
raise InvalidPackage(msg="ZIP entry CRC mismatch")
}
check_read_cancelled(cancelled)
}
}
///|
fn require_bounded_archive(
archive : @zip.Archive,
limits : ReadLimits,
cancelled? : () -> Bool = () => false,
) -> Unit raise XlsxError {
check_read_cancelled(cancelled)
match
archive.bounded_source_package_size(
max_entries=limits.max_archive_entries,
max_entry_uncompressed_bytes=limits.max_entry_uncompressed_bytes,
max_total_uncompressed_bytes=limits.max_total_uncompressed_bytes,
max_total_preserved_source_bytes=limits.max_total_preserved_source_bytes,
) {
Some(package_bytes) =>
if package_bytes > limits.max_package_bytes {
raise ResourceLimitExceeded(
kind="package_bytes",
limit=limits.max_package_bytes,
actual=package_bytes,
)
}
None => raise UnboundedArchive
}
if archive.entries().length() > limits.max_archive_entries {
raise ResourceLimitExceeded(
kind="entry_count",
limit=limits.max_archive_entries,
actual=archive.entries().length(),
)
}
enforce_archive_limits(archive, limits, cancelled~)
}
///|
fn read_limited_archive(
bytes : BytesView,
limits : ReadLimits,
cancelled? : () -> Bool = () => false,
) -> @zip.Archive raise XlsxError {
check_read_cancelled(cancelled)
@zip.read_limited(
bytes,
max_package_bytes=limits.max_package_bytes,
max_entries=limits.max_archive_entries,
max_entry_uncompressed_bytes=limits.max_entry_uncompressed_bytes,
max_total_uncompressed_bytes=limits.max_total_uncompressed_bytes,
// The ZIP package preserves exact source records for lossless rewrites.
// Bound that second compressed representation independently from inflation.
max_total_preserved_source_bytes=limits.max_total_preserved_source_bytes,
cancelled~,
) catch {
ResourceLimitExceeded(kind~, limit~, actual~) =>
raise ResourceLimitExceeded(kind~, limit~, actual~)
ReadCancelled => raise ReadCancelled
_ => raise InvalidPackage(msg="unreadable ZIP package")
}
}
///|
fn check_source_package_size(
bytes : BytesView,
limits : ReadLimits,
) -> Unit raise XlsxError {
if bytes.length() > limits.max_package_bytes {
raise ResourceLimitExceeded(
kind="package_bytes",
limit=limits.max_package_bytes,
actual=bytes.length(),
)
}
}
///|
fn first_existing_rel_target_path(
targets : Map[String, String],
source_part : StringView,
part_names : Map[String, String],
cancelled? : () -> Bool = () => false,
) -> String? raise XlsxError {
for _, target in targets {
let path = actual_relationship_target_path(
source_part,
target,
part_names,
cancelled~,
)
// `path` is the physical ZIP spelling when the target exists. The archive
// index is keyed in logical OPC PartName space, so restore percent-encoded
// Unicode before testing identity.
let logical_path = logical_archive_part_path(path)
if part_names.contains(@ooxml.package_part_name_key(logical_path)) {
return Some(path)
}
}
None
}
///|
fn first_existing_workbook_rel_target_path(
targets : Map[String, String],
workbook_part : StringView,
part_names : Map[String, String],
cancelled? : () -> Bool = () => false,
) -> String? raise XlsxError {
for _, target in targets {
let path = resolve_part_rel_target(
logical_archive_part_path(workbook_part),
target,
cancelled~,
)
match part_names.get(@ooxml.package_part_name_key(path)) {
Some(actual) => return Some(actual)
None => ()
}
}
None
}
///|
fn budgeted_package_part_name_key(
name : StringView,
budget : ReadBudget,
) -> String raise XlsxError {
budget.checkpoint()
budget.charge_work(name.length())
@ooxml.package_part_name_key_cancellable(name, cancelled=budget.cancelled) catch {
InvalidXml(msg~) => raise InvalidPackage(msg~)
ReadCancelled => raise ReadCancelled
}
}
///|
fn budgeted_logical_opc_part_name(
name : StringView,
budget : ReadBudget,
) -> String? raise XlsxError {
budget.checkpoint()
budget.charge_work(name.length())
@ooxml.logical_opc_part_name_from_zip_item_name_cancellable(
name,
cancelled=budget.cancelled,
) catch {
InvalidXml(msg~) => raise InvalidPackage(msg~)
ReadCancelled => raise ReadCancelled
}
}
///|
fn budgeted_register_opc_part_name(
registry : @ooxml.OpcPartNameRegistry,
name : StringView,
display : String,
budget : ReadBudget,
) -> @ooxml.OpcPartNameConflict? raise XlsxError {
budget.checkpoint()
// Count bounded identity nodes without allocating them, then reserve their
// cumulative parser capacity before registration can materialize either its
// token array or a detached trie suffix.
budget.charge_work(name.length())
let identity_nodes = @ooxml.opc_part_name_identity_node_count_cancellable(
name,
cancelled=budget.cancelled,
) catch {
InvalidXml(msg~) => raise InvalidPackage(msg~)
ReadCancelled => raise ReadCancelled
}
budget.charge_items(identity_nodes)
// Registration performs a tokenization pass followed by a mutation-free
// lookup/allocation pass. Charge both before either can run.
budget.charge_work(name.length())
budget.charge_work(name.length())
registry.register_cancellable(name, display, cancelled=budget.cancelled) catch {
InvalidXml(msg~) => raise InvalidPackage(msg~)
ReadCancelled => raise ReadCancelled
}
}
///|
fn archive_part_name_index(
archive : @zip.Archive,
budget? : ReadBudget,
) -> Map[String, String] raise XlsxError {
let budget = budget.unwrap_or(ReadBudget::new(ReadLimits::new()))
let names : Map[String, String] = Map([])
let registry = @ooxml.OpcPartNameRegistry::new(
maximum_identity_nodes=budget.limits.max_parser_items,
)
let content_types_key = @ooxml.package_part_name_key(content_types_part_path)
for entry in archive.entries() {
budget.checkpoint()
let physical_name = entry.name()
if physical_name.has_suffix("/") {
continue
}
budget.charge_work(physical_name.length())
let logical_name = if physical_name == content_types_part_path {
physical_name
} else if budgeted_package_part_name_key(physical_name, budget) ==
content_types_key {
raise InvalidPackage(
msg="reserved content-types part name has invalid spelling",
)
} else {
match budgeted_logical_opc_part_name(physical_name, budget) {
Some(value) => value
None => raise InvalidPackage(msg="invalid OPC ZIP item name")
}
}
let key = budgeted_package_part_name_key(logical_name, budget)
if logical_name != content_types_part_path {
match
budgeted_register_opc_part_name(
registry, logical_name, physical_name, budget,
) {
Some(Equivalent(_)) =>
raise InvalidPackage(msg="duplicate or equivalent OPC part name")
Some(Derivable(existing)) =>
raise InvalidPackage(
msg="OPC part names are derivable: \{existing} and \{physical_name}",
)
None => ()
}
}
budget.charge_work(key.length())
match names.get(key) {
Some(_) =>
raise InvalidPackage(msg="duplicate or equivalent OPC part name")
None => ()
}
budget.charge_work(key.length())
names[key] = physical_name
}
names
}
///|
test "read archive index maps physical Unicode OPC names to logical identity" {
let archive = @zip.Archive::new()
archive.add(content_types_part_path, b"manifest")
archive.add("custom/%E6%96%87.xml", b"workbook")
let names = archive_part_name_index(archive)
debug_inspect(
names.get(@ooxml.package_part_name_key("custom/文.xml")),
content="Some(\"custom/%E6%96%87.xml\")",
)
}
///|
test "relationship selection recognizes physical Unicode chart parts" {
let archive = @zip.Archive::new()
archive.add(content_types_part_path, b"manifest")
archive.add("xl/charts/%E6%96%87.xml", b"unicode chart")
archive.add("xl/charts/chart2.xml", b"other chart")
let part_names = archive_part_name_index(archive)
let targets : Map[String, String] = Map([
("unicode", "../charts/文.xml"),
("other", "../charts/chart2.xml"),
])
debug_inspect(
first_existing_rel_target_path(
targets, "xl/drawings/drawing1.xml", part_names,
),
content="Some(\"xl/charts/%E6%96%87.xml\")",
)
}
///|
test "read archive index rejects ambiguous and misspelled reserved entries" {
let duplicate = @zip.Archive::new()
duplicate.add(content_types_part_path, b"first")
duplicate.add(content_types_part_path, b"second")
try archive_part_name_index(duplicate) catch {
InvalidPackage(msg~) =>
inspect(msg, content="duplicate or equivalent OPC part name")
_ => fail("unexpected duplicate OPC part error")
} noraise {
_ => fail("expected duplicate OPC part rejection")
}
let misspelled = @zip.Archive::new()
misspelled.add("[content_types].xml", b"manifest")
try archive_part_name_index(misspelled) catch {
InvalidPackage(msg~) =>
inspect(
msg,
content="reserved content-types part name has invalid spelling",
)
_ => fail("unexpected reserved OPC part error")
} noraise {
_ => fail("expected reserved OPC part spelling rejection")
}
}
///|
test "read archive index rejects OPC-derivable parts in either order" {
for
entries in [
["custom/a.xml", "custom/a.xml/child.xml"],
["custom/a.xml/child.xml", "custom/a.xml"],
] {
let archive = @zip.Archive::new()
archive.add(content_types_part_path, b"manifest")
for name in entries {
archive.add(name, b"part")
}
try archive_part_name_index(archive) catch {
InvalidPackage(msg~) =>
assert_true(msg.contains("OPC part names are derivable"))
_ => fail("unexpected derivable OPC part error")
} noraise {
_ => fail("expected derivable OPC part rejection")
}
}
}
///|
test "read archive index charges physical OPC name work before decoding" {
let archive = @zip.Archive::new()
archive.add("custom/" + "a".repeat(64) + ".xml", b"part")
let budget = ReadBudget::new(
ReadLimits::with_values(max_parser_work_units=32),
)
try archive_part_name_index(archive, budget~) catch {
ResourceLimitExceeded(kind~, limit~, actual~) => {
inspect(kind, content="parser_work_units")
assert_eq(limit, 32)
assert_true(actual > limit)
}
_ => fail("unexpected archive name budget error")
} noraise {
_ => fail("expected archive name work limit")
}
}
///|
test "read archive index charges OPC identity nodes as parser items" {
let archive = @zip.Archive::new()
archive.add("a/b/c.xml", b"part")
let budget = ReadBudget::new(ReadLimits::with_values(max_parser_items=2))
try archive_part_name_index(archive, budget~) catch {
ResourceLimitExceeded(kind~, limit~, actual~) => {
inspect(kind, content="parser_items")
assert_eq(limit, 2)
assert_true(actual > limit)
}
_ => fail("unexpected archive identity-node budget error")
} noraise {
_ => fail("expected archive identity-node parser limit")
}
assert_eq(budget.parser_items, 0)
}
///|
test "read archive index polls cancellation inside long OPC names" {
let archive = @zip.Archive::new()
archive.add("custom/" + "a".repeat(9000) + ".xml", b"part")
let checks = [0]
let budget = ReadBudget::new(ReadLimits::new(), cancelled=() => {
checks[0] = checks[0] + 1
checks[0] >= 4
})
try archive_part_name_index(archive, budget~) catch {
ReadCancelled => assert_true(checks[0] >= 4)
_ => fail("unexpected archive name cancellation error")
} noraise {
_ => fail("expected archive name cancellation")
}
}
///|
fn actual_archive_part_path(
part_names : Map[String, String],
requested : String,
) -> String {
part_names.get(@ooxml.package_part_name_key(requested)).unwrap_or(requested)
}
///|
/// Returns the logical OPC name for either a logical PartName or its physical
/// ASCII ZIP projection. Relationship resolution must never use the physical
/// `%HH` spelling because relationships and content types live in the logical
/// PartName space.
fn logical_archive_part_path(path : StringView) -> String {
if path == content_types_part_path {
content_types_part_path
} else {
@ooxml.logical_opc_part_name_from_zip_item_name(path).unwrap_or(
path.to_owned(),
)
}
}
///|
fn actual_relationship_part_path(
source_part : StringView,
part_names : Map[String, String],
) -> String raise XlsxError {
actual_archive_part_path(
part_names,
rels_path_for_part(logical_archive_part_path(source_part)),
)
}
///|
fn actual_relationship_target_path(
source_part : StringView,
target : StringView,
part_names : Map[String, String],
cancelled? : () -> Bool = () => false,
) -> String raise XlsxError {
let source_relative = resolve_part_rel_target(
logical_archive_part_path(source_part),
target,
cancelled~,
)
part_names
.get(@ooxml.package_part_name_key(source_relative))
.unwrap_or(source_relative)
}
///|
test "relationship targets never fall back from source-relative to package root" {
let root_media = "xl/media/image1.png"
let part_names : Map[String, String] = Map([
(@ooxml.package_part_name_key(root_media), root_media),
])
inspect(
actual_relationship_target_path(
"xl/drawings/drawing1.xml", root_media, part_names,
),
content="xl/drawings/xl/media/image1.png",
)
inspect(
actual_relationship_target_path(
"xl/drawings/drawing1.xml", "../media/image1.png", part_names,
),
content="xl/media/image1.png",
)
}
///|
fn workbook_related_part_path(
workbook_rels : StringView,
relationship_type : StringView,
workbook_part : StringView,
fallback : StringView,
part_names : Map[String, String],
budget? : ReadBudget,
cancelled? : () -> Bool = () => false,
) -> String? raise XlsxError {
let targets = parse_internal_relationship_targets(
workbook_rels,
relationship_type,
budget?,
cancelled~,
)
match
first_existing_workbook_rel_target_path(
targets,
workbook_part,
part_names,
cancelled~,
) {
Some(path) => Some(path)
None =>
match first_relationship_target(targets) {
Some(target) =>
Some(
actual_archive_part_path(
part_names,
resolve_part_rel_target(
logical_archive_part_path(workbook_part),
target,
cancelled~,
),
),
)
None => part_names.get(@ooxml.package_part_name_key(fallback))
}
}
}
///|
fn read_zip_archive_core_unchecked(
archive : @zip.Archive,
options : Options,
limits : ReadLimits,
cancelled : () -> Bool,
transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
let read_budget = ReadBudget::new(limits, cancelled~)
let decoded_xml_bytes = Ref(0)
let xml_markup_tokens = Ref(0)
let decode_source = (value : BytesView) => {
if value.length() > limits.max_xml_part_bytes {
raise ResourceLimitExceeded(
kind="xml_part_bytes",
limit=limits.max_xml_part_bytes,
actual=value.length(),
)
}
if value.length() > limits.max_total_xml_bytes - decoded_xml_bytes.val {
raise ResourceLimitExceeded(
kind="total_xml_bytes",
limit=limits.max_total_xml_bytes,
actual=bounded_actual_above_limit(limits.max_total_xml_bytes),
)
}
let next_xml_bytes = decoded_xml_bytes.val + value.length()
decoded_xml_bytes.val = next_xml_bytes
for byte in value {
if byte == b'<' {
if xml_markup_tokens.val >= limits.max_xml_markup_tokens {
raise ResourceLimitExceeded(
kind="xml_markup_tokens",
limit=limits.max_xml_markup_tokens,
actual=bounded_actual_above_limit(limits.max_xml_markup_tokens),
)
}
let next_token_count = xml_markup_tokens.val + 1
xml_markup_tokens.val = next_token_count
}
}
let decoded = decode_utf8(value, transcoder)
read_budget.scan_xml(decoded)
decoded
}
preflight_archive_relationship_limits(
archive,
limits,
decode=decode_source,
budget=read_budget,
cancelled~,
)
let part_names = archive_part_name_index(archive, budget=read_budget)
let materialized_cells = Ref(0)
let materialized_dimensions = Ref(0)
let dimension_work = Ref(0)
let drawing_media_cache : Map[String, Bytes] = Map([])
let decode = (value : BytesView) => {
let decoded = decode_source(value)
read_budget.charge_work(decoded.length())
let canonical = canonicalize_xlsx_xml(
decoded,
max_output_chars=limits.max_xml_part_bytes,
cancelled~,
)
read_budget.checkpoint()
read_budget.charge_work(canonical.length())
canonical
}
let (workbook_logical_part_path, content_types) = resolve_workbook_xml_part_path(
archive,
part_names,
decode,
budget=read_budget,
cancelled~,
)
let workbook_xml_part_path = actual_archive_part_path(
part_names, workbook_logical_part_path,
)
let workbook_bytes = match archive.get(workbook_xml_part_path) {
Some(value) => value
None => raise MissingPart(path=workbook_xml_part_path)
}
let workbook_raw_xml = decode_source(workbook_bytes)
read_budget.charge_work(workbook_raw_xml.length())
let workbook_source_xml = project_xlsx_markup_compatibility(
workbook_raw_xml,
limits.max_xml_part_bytes,
cancelled~,
)
if workbook_source_xml != workbook_raw_xml {
read_budget.charge_work(workbook_source_xml.length())
}
let workbook_xml = canonicalize_xlsx_xml(
workbook_source_xml,
max_output_chars=limits.max_xml_part_bytes,
cancelled~,
)
read_budget.checkpoint()
read_budget.charge_work(workbook_xml.length())
let workbook_core_xml = match
xlsx_core_source_without_foreign(
workbook_source_xml,
"workbook",
cancelled~,
) {
Some(filtered) => {
read_budget.charge_work(filtered.length())
let canonical = canonicalize_xlsx_xml(
filtered,
max_output_chars=limits.max_xml_part_bytes,
cancelled~,
)
read_budget.checkpoint()
read_budget.charge_work(canonical.length())
canonical
}
None => workbook_xml
}
let workbook_rels_path = actual_relationship_part_path(
workbook_xml_part_path, part_names,
)
let workbook_rels = decode(
load_relationship_part(
archive,
content_types,
workbook_rels_path,
"workbook relationships",
cancelled~,
),
)
let sheets = parse_workbook_sheets(
workbook_source_xml,
limits.max_workbook_sheets,
cancelled~,
)
let sheet_names : Array[String] = []
for entry in sheets {
sheet_names.push(entry.name)
}
let defined_names = parse_defined_names(workbook_core_xml, sheet_names)
let active_index = match parse_active_sheet_index(workbook_core_xml) {
Some(value) => value
None => 0
}
let safe_active_index = if active_index < 0 || active_index >= sheets.length() {
0
} else {
active_index
}
let shared_strings_path = workbook_related_part_path(
workbook_rels,
rel_shared_strings,
workbook_xml_part_path,
shared_strings_part_path,
part_names,
budget=read_budget,
cancelled~,
)
let shared_strings = match shared_strings_path {
Some(path) => {
let value = load_typed_part(
archive,
content_types,
path,
[ct_shared_strings],
"shared strings",
cancelled~,
)
let raw_source = decode_source(value)
read_budget.charge_work(raw_source.length())
let source = project_xlsx_markup_compatibility(
raw_source,
limits.max_xml_part_bytes,
cancelled~,
)
if source != raw_source {
read_budget.charge_work(source.length())
}
let core_source = match
xlsx_core_source_without_foreign(source, "sst", cancelled~) {
Some(filtered) => filtered
None => source
}
if core_source != source {
read_budget.charge_work(core_source.length())
}
let canonical = canonicalize_xlsx_xml(
core_source,
max_output_chars=limits.max_xml_part_bytes,
cancelled~,
)
read_budget.checkpoint()
read_budget.charge_work(canonical.length())
parse_shared_strings(canonical, budget=read_budget)
}
None => []
}
let styles_path = workbook_related_part_path(
workbook_rels,
rel_styles,
workbook_xml_part_path,
styles_part_path,
part_names,
budget=read_budget,
cancelled~,
)
let (
styles,
conditional_styles,
default_font,
default_table_style,
default_pivot_style,
indexed_colors,
mru_colors_xml,
styles_ext_lst_xml,
) = match styles_path {
Some(path) => {
let value = load_typed_part(
archive,
content_types,
path,
[ct_styles],
"styles",
cancelled~,
)
let styles_raw_xml = decode_source(value)
read_budget.charge_work(styles_raw_xml.length())
let styles_source_xml = project_xlsx_markup_compatibility(
styles_raw_xml,
limits.max_xml_part_bytes,
cancelled~,
)
if styles_source_xml != styles_raw_xml {
read_budget.charge_work(styles_source_xml.length())
}
let styles_xml = canonicalize_xlsx_xml(
styles_source_xml,
max_output_chars=limits.max_xml_part_bytes,
cancelled~,
)
read_budget.checkpoint()
read_budget.charge_work(styles_xml.length())
let styles_core_xml = match
xlsx_core_source_without_foreign(
styles_source_xml,
"styleSheet",
cancelled~,
) {
Some(filtered) => {
read_budget.charge_work(filtered.length())
let canonical = canonicalize_xlsx_xml(
filtered,
max_output_chars=limits.max_xml_part_bytes,
cancelled~,
)
read_budget.checkpoint()
read_budget.charge_work(canonical.length())
canonical
}
None => styles_xml
}
let (styles, conditional_styles) = parse_styles(styles_core_xml)
let (default_table_style, default_pivot_style) = parse_table_styles_defaults(
styles_core_xml,
)
let indexed_colors = parse_indexed_colors(styles_core_xml)
let mru_colors_xml = parse_mru_colors_xml(styles_core_xml)
let styles_ext_lst_xml = parse_styles_ext_lst_xml(styles_xml)
(
styles,
conditional_styles,
parse_default_font(styles_core_xml),
default_table_style,
default_pivot_style,
indexed_colors,
mru_colors_xml,
styles_ext_lst_xml,
)
}
None =>
(
[Style::new()],
[],
"Calibri",
"TableStyleMedium9",
"PivotStyleLight16",
None,
None,
None,
)
}
let theme_path = workbook_related_part_path(
workbook_rels,
rel_theme,
workbook_xml_part_path,
theme_part_path,
part_names,
budget=read_budget,
cancelled~,
)
let theme_xml = match theme_path {
Some(path) => {
let value = load_typed_part(
archive,
content_types,
path,
[ct_theme],
"theme",
cancelled~,
)
Some(decode(value))
}
None => None
}
let theme_colors = match theme_xml {
Some(xml) => parse_theme_colors(xml)
None => None
}
let core_properties = match archive.get("docProps/core.xml") {
Some(value) => parse_core_properties(decode(value))
None => CoreProperties::new()
}
let app_properties = match archive.get("docProps/app.xml") {
Some(value) => parse_app_properties(decode(value))
None => AppProperties::new()
}
let custom_properties = match archive.get("docProps/custom.xml") {
Some(value) => parse_custom_properties(decode(value))
None => []
}
let workbook_props = parse_workbook_props(workbook_core_xml)
let calc_props = parse_calc_props(workbook_core_xml)
let workbook_protection = parse_workbook_protection(workbook_core_xml)
let style_count = styles.length()
let cell_images_part = actual_archive_part_path(
part_names, "xl/cellimages.xml",
)
let cell_images = match archive.get(cell_images_part) {
Some(value) => {
let cell_images_rels_path = actual_relationship_part_path(
cell_images_part, part_names,
)
let rels_xml = match
load_optional_relationship_part(
archive,
content_types,
cell_images_rels_path,
"cell image relationships",
cancelled~,
) {
Some(rels) => decode(rels)
None => ""
}
parse_cell_images(
decode(value),
rels_xml,
cell_images_part,
part_names,
content_types,
archive,
budget=read_budget,
cancelled~,
)
}
None => []
}
let rich_value_images = parse_rich_value_images(
archive,
part_names,
content_types,
decode,
budget=read_budget,
cancelled~,
)
let rich_value_media : Map[String, Bytes] = Map([])
match rich_value_images {
Some(data) => {
for _, target in data.rel_targets {
match archive.get(target) {
Some(_) => {
let (bytes, identity) = load_image_part(
archive,
content_types,
target,
"rich value image",
cancelled~,
)
rich_value_media[target] = bytes.to_owned()
data.media_identities[target] = identity
}
None => ()
}
}
for _, target in data.web_rel_targets {
match archive.get(target) {
Some(_) => {
let (bytes, identity) = load_image_part(
archive,
content_types,
target,
"rich value web image",
cancelled~,
)
rich_value_media[target] = bytes.to_owned()
data.media_identities[target] = identity
}
None => ()
}
}
}
None => ()
}
let workbook : Workbook = {
worksheet_owner_token: [()],
sheets: [],
chart_sheets: [],
sheet_order: [],
styles,
conditional_styles,
defined_names,
core_properties,
app_properties,
custom_properties,
vba_project: None,
workbook_props,
calc_props,
default_font,
default_table_style,
default_pivot_style,
theme_colors,
theme_xml,
indexed_colors,
mru_colors_xml,
styles_ext_lst_xml,
io_context: workbook_io_context_with_transcoder(
empty_workbook_io_context(),
transcoder,
),
options,
workbook_protection,
active_sheet_index: safe_active_index,
cell_images,
rich_value_images,
rich_value_media,
package_features: detect_package_features(part_names, name => {
match (try? content_types.content_type_for(name)) {
Ok(Some(value)) => Some(value)
_ => None
}
}),
}
let slicer_cache_defs = parse_slicer_cache_definitions(
workbook_rels,
workbook_xml_part_path,
part_names,
content_types,
archive,
decode,
budget=read_budget,
cancelled~,
)
let vba_targets = parse_internal_relationship_targets(
workbook_rels,
rel_vba_project,
budget=read_budget,
cancelled~,
)
let vba_path = match
first_existing_workbook_rel_target_path(
vba_targets,
workbook_xml_part_path,
part_names,
cancelled~,
) {
Some(path) => Some(path)
None =>
match first_relationship_target(vba_targets) {
Some(target) =>
Some(
actual_archive_part_path(
part_names,
resolve_part_rel_target(
logical_archive_part_path(workbook_xml_part_path),
target,
cancelled~,
),
),
)
None => None
}
}
match vba_path {
Some(path) => {
let vba_bytes = load_typed_part(
archive,
content_types,
path,
[ct_vba_project],
"VBA project",
cancelled~,
)
workbook.vba_project = Some(vba_bytes.to_owned())
}
None => ()
}
let worksheet_targets = parse_internal_relationship_targets(
workbook_rels,
rel_worksheet,
budget=read_budget,
cancelled~,
)
let chartsheet_targets = parse_internal_relationship_targets(
workbook_rels,
rel_chartsheet,
budget=read_budget,
cancelled~,
)
let seen_sheet_parts : Set[String] = Set([])
let resolved_sheet_parts : Map[String, String] = Map([])
for entry in sheets {
let rel_id = entry.rel_id
let is_worksheet = worksheet_targets.contains(rel_id)
let is_chartsheet = chartsheet_targets.contains(rel_id)
if is_worksheet == is_chartsheet {
raise InvalidXml(msg="sheet relationship missing or ambiguous")
}
let target = if is_worksheet {
match worksheet_targets.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="worksheet target missing")
}
} else {
match chartsheet_targets.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="chartsheet target missing")
}
}
let path = actual_archive_part_path(
part_names,
resolve_part_rel_target(
logical_archive_part_path(workbook_xml_part_path),
target,
cancelled~,
),
)
let part_key = @ooxml.package_part_name_key(path)
if seen_sheet_parts.contains(part_key) {
raise InvalidXml(msg="sheet part referenced more than once")
}
seen_sheet_parts.add(part_key)
resolved_sheet_parts[rel_id] = path
}
for entry in sheets {
let name = entry.name
let state = entry.state
let rel_id = entry.rel_id
let is_worksheet = worksheet_targets.contains(rel_id)
if is_worksheet {
let sheet_path = match resolved_sheet_parts.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="worksheet target missing")
}
let sheet_bytes = load_typed_part(
archive,
content_types,
sheet_path,
[ct_worksheet],
"worksheet",
cancelled~,
)
let sheet_raw_xml = decode_source(sheet_bytes)
read_budget.charge_work(sheet_raw_xml.length())
let sheet_source_xml = project_xlsx_markup_compatibility(
sheet_raw_xml,
limits.max_xml_part_bytes,
cancelled~,
)
if sheet_source_xml != sheet_raw_xml {
read_budget.charge_work(sheet_source_xml.length())
}
let sheet_xml = canonicalize_xlsx_xml(
sheet_source_xml,
max_output_chars=limits.max_xml_part_bytes,
cancelled~,
)
read_budget.checkpoint()
read_budget.charge_work(sheet_xml.length())
let sheet_core_xml = canonicalize_xlsx_worksheet_core(
sheet_source_xml,
sheet_xml,
limits.max_xml_part_bytes,
cancelled~,
)
read_budget.checkpoint()
if sheet_core_xml != sheet_xml {
read_budget.charge_work(sheet_core_xml.length())
}
let sheet_feature_xml = canonicalize_xlsx_worksheet_features(
sheet_source_xml,
limits.max_xml_part_bytes,
cancelled~,
)
read_budget.checkpoint()
read_budget.charge_work(sheet_feature_xml.length())
let (
cells,
shared_formula_masters_index,
merged_cells,
auto_filter,
row_dimensions,
col_dimensions,
page_margins,
page_layout,
header_footer,
sheet_protection,
row_breaks,
col_breaks,
sheet_views,
sheet_props,
dimension_ref,
picture_rel_id,
) = parse_worksheet(
sheet_core_xml,
shared_strings,
style_count,
materialized_cells,
limits.max_materialized_cells,
materialized_dimensions,
limits.max_materialized_row_column_dimensions,
dimension_work,
limits.max_row_column_dimension_work,
budget=read_budget,
)
let hyperlink_elements = parse_hyperlink_elements(sheet_core_xml)
let table_part_ids = parse_table_part_ids(sheet_core_xml)
let pivot_part_ids = parse_pivot_table_part_ids(sheet_core_xml)
let slicer_rel_ids = parse_sheet_slicer_rel_ids(sheet_feature_xml)
let (drawing_rel_id, legacy_drawing_rel_id, legacy_drawing_hf_rel_id) = parse_sheet_drawing_rel_ids(
sheet_source_xml,
"worksheet",
budget=read_budget,
cancelled~,
)
let sparkline_groups = parse_sparkline_groups(sheet_feature_xml)
let data_validations = parse_data_validations(
sheet_core_xml,
budget=read_budget,
)
for
dv in parse_data_validations_x14(sheet_feature_xml, budget=read_budget) {
data_validations.push(dv)
}
let conditional_formats = parse_conditional_formats(
// Base data-bar rules carry their x14 correlation id in a nested
// extLst, so this parser needs the preserved extension view. Cell and
// row parsing above still uses the extension-free structural view.
sheet_feature_xml,
budget=read_budget,
)
for
cf in parse_conditional_formats_x14(
sheet_feature_xml,
budget=read_budget,
) {
conditional_formats.push(cf)
}
let x14_data_bars = parse_x14_data_bars(
sheet_feature_xml,
budget=read_budget,
)
let unknown_ext_blocks = parse_unknown_worksheet_ext_blocks(sheet_xml)
let ignored_errors = parse_ignored_errors(
sheet_core_xml,
budget=read_budget,
)
let mut needs_hyperlink_rels = false
for link in hyperlink_elements {
if link.r_id is Some(_) {
needs_hyperlink_rels = true
break
}
}
let needs_rels = table_part_ids.length() > 0 ||
pivot_part_ids.length() > 0 ||
slicer_rel_ids.length() > 0 ||
needs_hyperlink_rels ||
drawing_rel_id is Some(_) ||
legacy_drawing_rel_id is Some(_) ||
legacy_drawing_hf_rel_id is Some(_) ||
picture_rel_id is Some(_)
let rels_path = actual_relationship_part_path(sheet_path, part_names)
let rels_xml = match
load_optional_relationship_part(
archive,
content_types,
rels_path,
"worksheet relationships",
cancelled~,
) {
Some(rels_bytes) => decode(rels_bytes)
None => if needs_rels { raise MissingPart(path=rels_path) } else { "" }
}
let slicer_part_entries = if slicer_rel_ids.length() == 0 {
[]
} else {
if rels_xml == "" {
raise InvalidXml(msg="slicer relationship missing")
}
let rel_targets = parse_internal_relationship_targets(
rels_xml,
rel_slicer,
budget=read_budget,
cancelled~,
)
let parsed : Array[ParsedSlicerPartEntry] = []
let seen_paths : Map[String, Bool] = Map([])
for rel_id in slicer_rel_ids {
let target = match rel_targets.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="slicer relationship missing")
}
let slicer_path = actual_relationship_target_path(
sheet_path,
target,
part_names,
cancelled~,
)
require_part_content_type(
content_types,
logical_archive_part_path(slicer_path),
[ct_slicer],
"slicer",
cancelled~,
)
if seen_paths.contains(slicer_path) {
continue
}
seen_paths[slicer_path] = true
let slicer_bytes = match archive.get(slicer_path) {
Some(value) => value
None => raise MissingPart(path=slicer_path)
}
let slicer_xml = decode(slicer_bytes)
parsed.append(parse_slicer_part_entries(slicer_xml))
}
parsed
}
let sheet_background = match picture_rel_id {
Some(rel_id) => {
if rels_xml == "" {
raise InvalidXml(msg="picture relationship missing")
}
let image_targets = parse_internal_relationship_targets(
rels_xml,
rel_image,
budget=read_budget,
cancelled~,
)
let target = match image_targets.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="picture target missing")
}
let image_path = actual_relationship_target_path(
sheet_path,
target,
part_names,
cancelled~,
)
let (image_bytes, identity) = load_image_part(
archive,
content_types,
image_path,
"sheet background image",
cancelled~,
)
Some({
data: image_bytes.to_owned(),
extension: identity.extension,
content_type: identity.content_type,
})
}
None => None
}
let hyperlinks = if hyperlink_elements.length() == 0 {
[]
} else {
let rel_targets = if needs_hyperlink_rels {
parse_external_relationship_targets(
rels_xml,
rel_hyperlink,
budget=read_budget,
cancelled~,
)
} else {
Map([])
}
let parsed_links : Array[Hyperlink] = []
for link in hyperlink_elements {
match link.r_id {
Some(rel_id) => {
let target = match rel_targets.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="hyperlink relationship missing")
}
parsed_links.push({
reference: link.reference,
target,
link_type: External,
location: link.location,
display: link.display,
tooltip: link.tooltip,
})
}
None =>
match link.location {
Some(location) =>
parsed_links.push({
reference: link.reference,
target: location,
link_type: Location,
location: Some(location),
display: link.display,
tooltip: link.tooltip,
})
None => raise InvalidXml(msg="hyperlink target missing")
}
}
}
canonicalize_loaded_hyperlinks_for_merges(
parsed_links, merged_cells, read_budget,
)
parsed_links
}
let tables = if table_part_ids.length() == 0 {
[]
} else {
let rel_targets = parse_internal_relationship_targets(
rels_xml,
rel_table,
budget=read_budget,
cancelled~,
)
let parsed_tables : Array[Table] = []
for rel_id in table_part_ids {
let target = match rel_targets.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="table relationship missing")
}
let table_path = actual_relationship_target_path(
sheet_path,
target,
part_names,
cancelled~,
)
require_part_content_type(
content_types,
logical_archive_part_path(table_path),
[ct_table],
"table",
cancelled~,
)
let table_bytes = match archive.get(table_path) {
Some(value) => value
None => raise MissingPart(path=table_path)
}
let table_xml = decode(table_bytes)
parsed_tables.push(parse_table_xml(table_xml))
}
parsed_tables
}
let pivot_tables = {
let rel_targets = if rels_xml == "" {
Map([])
} else {
parse_internal_relationship_targets(
rels_xml,
rel_pivot_table,
budget=read_budget,
cancelled~,
)
}
let pivot_rel_ids : Array[String] = []
if pivot_part_ids.length() > 0 {
for rel_id in pivot_part_ids {
pivot_rel_ids.push(rel_id)
}
} else {
for rel_id, _target in rel_targets {
pivot_rel_ids.push(rel_id)
}
}
if pivot_rel_ids.length() == 0 {
[]
} else {
let parsed_pivots : Array[PivotTable] = []
for rel_id in pivot_rel_ids {
let target = match rel_targets.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="pivot table relationship missing")
}
let pivot_path = actual_relationship_target_path(
sheet_path,
target,
part_names,
cancelled~,
)
require_part_content_type(
content_types,
logical_archive_part_path(pivot_path),
[ct_pivot_table],
"pivot table",
cancelled~,
)
let pivot_bytes = match archive.get(pivot_path) {
Some(value) => value
None => raise MissingPart(path=pivot_path)
}
let pivot_xml = decode(pivot_bytes)
let pivot_rels_path = actual_relationship_part_path(
pivot_path, part_names,
)
let pivot_rels_bytes = load_relationship_part(
archive,
content_types,
pivot_rels_path,
"pivot table relationships",
cancelled~,
)
let pivot_rels_xml = decode(pivot_rels_bytes)
let cache_targets = parse_internal_relationship_targets(
pivot_rels_xml,
rel_pivot_cache,
budget=read_budget,
cancelled~,
)
let cache_path = match
first_existing_rel_target_path(
cache_targets,
pivot_path,
part_names,
cancelled~,
) {
Some(path) => path
None => {
let cache_target = match
first_relationship_target(cache_targets) {
Some(value) => value
None =>
raise InvalidXml(msg="pivot cache relationship missing")
}
actual_relationship_target_path(
pivot_path,
cache_target,
part_names,
cancelled~,
)
}
}
require_part_content_type(
content_types,
logical_archive_part_path(cache_path),
[ct_pivot_cache_def],
"pivot cache definition",
cancelled~,
)
let cache_bytes = match archive.get(cache_path) {
Some(value) => value
None => raise MissingPart(path=cache_path)
}
let cache_xml = decode(cache_bytes)
let cache_id = parse_id_from_path(
cache_path, "xl/pivotCache/pivotCacheDefinition",
)
let pivot_id = parse_id_from_path(
pivot_path, "xl/pivotTables/pivotTable",
)
let cache_rels_path = actual_relationship_part_path(
cache_path, part_names,
)
let cache_records_xml = match
load_optional_relationship_part(
archive,
content_types,
cache_rels_path,
"pivot cache relationships",
cancelled~,
) {
Some(value) => {
let cache_rels_xml = decode(value)
let record_targets = parse_internal_relationship_targets(
cache_rels_xml,
rel_pivot_cache_records,
budget=read_budget,
cancelled~,
)
let record_path = match
first_existing_rel_target_path(
record_targets,
cache_path,
part_names,
cancelled~,
) {
Some(path) => Some(path)
None =>
match first_relationship_target(record_targets) {
Some(record_target) =>
Some(
actual_relationship_target_path(
cache_path,
record_target,
part_names,
cancelled~,
),
)
None => None
}
}
match record_path {
Some(path) => {
require_part_content_type(
content_types,
logical_archive_part_path(path),
[ct_pivot_cache_records],
"pivot cache records",
cancelled~,
)
let record_bytes = match archive.get(path) {
Some(value) => value
None => raise MissingPart(path~)
}
Some(decode(record_bytes))
}
None => None
}
}
None => None
}
parsed_pivots.push({
name: parse_pivot_table_name(pivot_xml),
table_id: pivot_id,
cache_id,
table_xml: pivot_xml,
cache_definition_xml: cache_xml,
cache_records_xml,
})
}
parsed_pivots
}
}
let vml_targets = if legacy_drawing_rel_id is Some(_) ||
legacy_drawing_hf_rel_id is Some(_) {
parse_internal_relationship_targets(
rels_xml,
rel_vml,
budget=read_budget,
cancelled~,
)
} else {
Map([])
}
let vml_drawing_xml = match legacy_drawing_rel_id {
Some(rel_id) => {
let target = match vml_targets.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="vml drawing relationship missing")
}
let vml_path = actual_relationship_target_path(
sheet_path,
target,
part_names,
cancelled~,
)
require_part_content_type(
content_types,
logical_archive_part_path(vml_path),
[ct_vml],
"VML drawing",
cancelled~,
)
let vml_bytes = match archive.get(vml_path) {
Some(value) => value
None => raise MissingPart(path=vml_path)
}
Some(decode(vml_bytes))
}
None => None
}
let vml_drawing_hf_xml = match legacy_drawing_hf_rel_id {
Some(rel_id) => {
let target = match vml_targets.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="vml drawing hf relationship missing")
}
let vml_path = actual_relationship_target_path(
sheet_path,
target,
part_names,
cancelled~,
)
require_part_content_type(
content_types,
logical_archive_part_path(vml_path),
[ct_vml],
"VML drawing",
cancelled~,
)
let vml_bytes = match archive.get(vml_path) {
Some(value) => value
None => raise MissingPart(path=vml_path)
}
Some((decode(vml_bytes), vml_path))
}
None => None
}
let (vml_drawing_hf_xml, vml_drawing_hf_path) = match vml_drawing_hf_xml {
Some((xml, path)) => (Some(xml), Some(path))
None => (None, None)
}
let header_footer_images = match
(vml_drawing_hf_xml, vml_drawing_hf_path) {
(Some(xml), Some(path)) => {
let vml_rels_xml = match
load_optional_relationship_part(
archive,
content_types,
actual_relationship_part_path(path, part_names),
"VML relationships",
cancelled~,
) {
Some(value) => Some(decode(value))
None => None
}
parse_header_footer_images_from_vml(
xml,
vml_rels_xml,
path,
part_names,
content_types,
archive,
budget=read_budget,
cancelled~,
)
}
_ => []
}
let comments = if rels_xml == "" {
[]
} else {
let comment_targets = parse_internal_relationship_targets(
rels_xml,
rel_comments,
budget=read_budget,
cancelled~,
)
let parsed_comments : Array[Comment] = []
for _rel_id, target in comment_targets {
let comment_path = actual_relationship_target_path(
sheet_path,
target,
part_names,
cancelled~,
)
require_part_content_type(
content_types,
logical_archive_part_path(comment_path),
[ct_comments],
"comments",
cancelled~,
)
let comment_bytes = match archive.get(comment_path) {
Some(value) => value
None => raise MissingPart(path=comment_path)
}
let comment_xml = decode(comment_bytes)
let entries = parse_comments_xml(comment_xml)
parsed_comments.append(entries)
}
parsed_comments
}
let form_controls = match vml_drawing_xml {
Some(xml) => parse_form_controls_vml(xml)
None => []
}
let cell_index = build_bounded_worksheet_cell_index(cells, read_budget)
let sheet = {
workbook_owner_token: Some(workbook.worksheet_owner_token),
name,
sheet_views,
dimension_ref,
cells,
cell_index,
cell_index_valid: true,
shared_formula_masters_index,
shared_formula_masters_index_valid: true,
merged_cells,
auto_filter,
page_margins,
page_layout,
header_footer,
sheet_protection,
sheet_props,
sheet_background,
row_breaks,
col_breaks,
hyperlinks,
tables,
sparkline_groups,
pivot_tables,
vml_drawing_xml,
vml_drawing_hf_xml,
images: [],
header_footer_images,
charts: [],
shapes: [],
form_controls,
slicers: [],
data_validations,
conditional_formats,
x14_data_bars,
unknown_ext_blocks,
x14_cf_rule_id_counter: 1,
ignored_errors,
comments,
row_dimensions,
col_dimensions,
state,
stream_state: Idle,
next_drawing_order: 0,
preserved_drawing_anchors: [],
preserved_drawing_relationships: [],
preserved_drawing_parts: [],
cell_vm: if rich_value_images is Some(_) {
parse_cell_vm_map(sheet_xml)
} else {
Map([])
},
}
let mut slicer_anchor_info : Map[String, SlicerAnchorInfo] = Map([])
match drawing_rel_id {
Some(rel_id) => {
if rels_xml == "" {
raise InvalidXml(msg="drawing relationship missing")
}
let drawing_targets = parse_internal_relationship_targets(
rels_xml,
rel_drawing,
budget=read_budget,
cancelled~,
)
let drawing_target = match drawing_targets.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="drawing target missing")
}
let drawing_path = actual_relationship_target_path(
sheet_path,
drawing_target,
part_names,
cancelled~,
)
require_part_content_type(
content_types,
logical_archive_part_path(drawing_path),
[ct_drawing],
"drawing",
cancelled~,
)
let drawing_bytes = match archive.get(drawing_path) {
Some(value) => value
None => raise MissingPart(path=drawing_path)
}
let drawing_source_xml = decode_source(drawing_bytes)
let drawing_xml = canonicalize_xlsx_drawing_xml(
drawing_source_xml,
limits.max_xml_part_bytes,
budget=read_budget,
cancelled~,
)
let drawing_metrics = drawing_metrics_for_sheet(sheet)
let drawing_anchors = parse_drawing_anchors(
drawing_xml,
budget=read_budget,
)
sheet.next_drawing_order = drawing_anchors.length()
if slicer_part_entries.length() > 0 {
slicer_anchor_info = parse_drawing_slicer_anchors(
drawing_anchors,
drawing_metrics,
budget=read_budget,
)
}
let drawing_rels_xml = match
load_optional_relationship_part(
archive,
content_types,
actual_relationship_part_path(drawing_path, part_names),
"drawing relationships",
cancelled~,
) {
Some(value) => decode(value)
None => ""
}
let (preserved_anchors, preserved_relationships, preserved_parts) = preserved_unsupported_drawing_state(
drawing_anchors,
drawing_rels_xml,
drawing_path,
archive,
part_names,
content_types,
decode,
budget=read_budget,
cancelled~,
)
sheet.preserved_drawing_anchors.append(preserved_anchors)
sheet.preserved_drawing_relationships.append(preserved_relationships)
sheet.preserved_drawing_parts.append(preserved_parts)
let drawing_images = parse_drawing_images(
drawing_anchors,
drawing_rels_xml,
drawing_path,
part_names,
content_types,
drawing_metrics,
archive,
media_cache=drawing_media_cache,
budget=read_budget,
cancelled~,
)
sheet.images.append(drawing_images)
let drawing_charts = parse_drawing_charts(
drawing_anchors,
drawing_rels_xml,
drawing_path,
part_names,
content_types,
drawing_metrics,
archive,
decode,
budget=read_budget,
cancelled~,
)
sheet.charts.append(drawing_charts)
let drawing_shapes = parse_drawing_shapes(
drawing_anchors,
drawing_metrics,
budget=read_budget,
)
sheet.shapes.append(drawing_shapes)
}
None => ()
}
for entry in slicer_part_entries {
let cache_def = slicer_cache_defs.get(entry.cache)
let source_name = match cache_def {
Some(def) =>
if def.source_name != "" {
def.source_name
} else {
entry.name
}
None => entry.name
}
let item_desc = match cache_def {
Some(def) => def.item_desc
None => false
}
let mut cell = ""
let mut width = 200
let mut height = 200
let mut macro_name = ""
let format = GraphicOptions::new()
match slicer_anchor_info.get(entry.name) {
Some(info) => {
cell = info.cell
if info.width > 0 {
width = info.width
}
if info.height > 0 {
height = info.height
}
macro_name = info.macro_name
if info.offset_x != 0 {
format.offset_x = Some(info.offset_x)
}
if info.offset_y != 0 {
format.offset_y = Some(info.offset_y)
}
if info.alt_text != "" {
format.alt_text = Some(info.alt_text)
}
format.positioning = Some(info.positioning)
format.locked = Some(info.locked)
format.print_object = Some(info.print_object)
}
None => {
format.positioning = Some(TwoCell)
format.locked = Some(true)
format.print_object = Some(true)
}
}
sheet.slicers.push({
name: entry.name,
cache: entry.cache,
source_name,
cell,
table_sheet: "",
table_name: "",
caption: entry.caption,
macro_name,
width,
height,
display_header: entry.display_header,
item_desc,
format,
drawing_offset_x_emu: match slicer_anchor_info.get(entry.name) {
Some(info) => Some(info.offset_x_emu)
None => None
},
drawing_offset_y_emu: match slicer_anchor_info.get(entry.name) {
Some(info) => Some(info.offset_y_emu)
None => None
},
drawing_width_emu: match slicer_anchor_info.get(entry.name) {
Some(info) => Some(info.width_emu)
None => None
},
drawing_height_emu: match slicer_anchor_info.get(entry.name) {
Some(info) => Some(info.height_emu)
None => None
},
drawing_order: match slicer_anchor_info.get(entry.name) {
Some(info) => Some(info.order)
None => None
},
})
}
workbook.sheets.push(sheet)
workbook.sheet_order.push(Worksheet(workbook.sheets.length() - 1))
} else {
let chartsheet_path = match resolved_sheet_parts.get(rel_id) {
Some(value) => value
None => raise InvalidXml(msg="chartsheet target missing")
}
let chartsheet_bytes = load_typed_part(
archive,
content_types,
chartsheet_path,
[ct_chartsheet],
"chartsheet",
cancelled~,
)
let chartsheet_raw_xml = decode_source(chartsheet_bytes)
read_budget.charge_work(chartsheet_raw_xml.length())
let chartsheet_source_xml = project_xlsx_markup_compatibility(
chartsheet_raw_xml,
limits.max_xml_part_bytes,
cancelled~,
)
if chartsheet_source_xml != chartsheet_raw_xml {
read_budget.charge_work(chartsheet_source_xml.length())
}
let chartsheet_xml = canonicalize_xlsx_xml(
chartsheet_source_xml,
max_output_chars=limits.max_xml_part_bytes,
cancelled~,
)
read_budget.checkpoint()
read_budget.charge_work(chartsheet_xml.length())
let (drawing_rel_id, _, _) = parse_sheet_drawing_rel_ids(
chartsheet_source_xml,
"chartsheet",
budget=read_budget,
cancelled~,
)
let drawing_rel = match drawing_rel_id {
Some(value) => value
None => raise InvalidXml(msg="chartsheet drawing missing")
}
let chartsheet_rels_path = actual_relationship_part_path(
chartsheet_path, part_names,
)
let chartsheet_rels_bytes = load_relationship_part(
archive,
content_types,
chartsheet_rels_path,
"chartsheet relationships",
cancelled~,
)
let chartsheet_rels_xml = decode(chartsheet_rels_bytes)
let drawing_targets = parse_internal_relationship_targets(
chartsheet_rels_xml,
rel_drawing,
budget=read_budget,
cancelled~,
)
let drawing_target = match drawing_targets.get(drawing_rel) {
Some(value) => value
None => raise InvalidXml(msg="drawing target missing")
}
let drawing_path = actual_relationship_target_path(
chartsheet_path,
drawing_target,
part_names,
cancelled~,
)
let drawing_bytes = load_typed_part(
archive,
content_types,
drawing_path,
[ct_drawing],
"drawing",
cancelled~,
)
let drawing_source_xml = decode_source(drawing_bytes)
let drawing_xml = canonicalize_xlsx_drawing_xml(
drawing_source_xml,
limits.max_xml_part_bytes,
budget=read_budget,
cancelled~,
)
let drawing_anchors = parse_drawing_anchors(
drawing_xml,
budget=read_budget,
)
let chart_rel_id = single_drawing_chart_rel_id(
drawing_anchors,
budget=read_budget,
)
let drawing_rels_path = actual_relationship_part_path(
drawing_path, part_names,
)
let drawing_rels_bytes = load_relationship_part(
archive,
content_types,
drawing_rels_path,
"drawing relationships",
cancelled~,
)
let drawing_rels_xml = decode(drawing_rels_bytes)
let chart_targets = parse_internal_relationship_targets(
drawing_rels_xml,
rel_chart,
budget=read_budget,
cancelled~,
)
let chart_target = match chart_targets.get(chart_rel_id) {
Some(value) => value
None => raise InvalidXml(msg="chartsheet chart target missing")
}
let chart_path = actual_relationship_target_path(
drawing_path,
chart_target,
part_names,
cancelled~,
)
let chart_bytes = load_typed_part(
archive,
content_types,
chart_path,
[ct_chart],
"chart",
cancelled~,
)
let chart_xml = decode(chart_bytes)
let chart_sheet = ChartSheet::new(name, chart_xml)
chart_sheet.state = state
workbook.chart_sheets.push(chart_sheet)
workbook.sheet_order.push(ChartSheet(workbook.chart_sheets.length() - 1))
}
}
resolve_slicer_sources_after_read(workbook, slicer_cache_defs)
read_budget.checkpoint()
workbook
}
///|
fn read_zip_archive_core(
archive : @zip.Archive,
options : Options,
limits : ReadLimits,
cancelled : () -> Bool,
transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
let workbook = match transcoder {
Some(decode) =>
read_zip_archive_core_unchecked(
archive,
options,
limits,
cancelled,
transcoder=decode,
) catch {
InvalidCellRef(value~) =>
raise InvalidXml(msg="coordinate reference invalid: \{value}")
other => raise other
}
None =>
read_zip_archive_core_unchecked(archive, options, limits, cancelled) catch {
InvalidCellRef(value~) =>
raise InvalidXml(msg="coordinate reference invalid: \{value}")
other => raise other
}
}
check_read_cancelled(cancelled)
workbook
}
///|
fn checked_read_workbook(
workbook : Workbook,
cancelled : () -> Bool,
) -> Workbook raise XlsxError {
check_read_cancelled(cancelled)
workbook
}
///|
fn read_zip_bytes(
bytes : BytesView,
options? : Options = Options::new(),
limits? : ReadLimits = ReadLimits::new(),
cancelled? : () -> Bool = () => false,
transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
check_read_cancelled(cancelled)
let archive = read_limited_archive(bytes, limits, cancelled~)
require_bounded_archive(archive, limits, cancelled~)
let workbook = match transcoder {
Some(value) =>
read_zip_archive_core(
archive,
options,
limits,
cancelled,
transcoder=value,
)
None => read_zip_archive_core(archive, options, limits, cancelled)
}
checked_read_workbook(workbook, cancelled)
}
///|
/// Reads a workbook from an already-inflated ZIP archive without a second
/// decompression pass. The archive must be pristine and carry non-forgeable
/// `zip.read_limited` provenance at least as strict as `limits`; constructed,
/// compatibility-read, or mutated archives fail with `UnboundedArchive`.
pub fn read_bounded_archive(
archive : @zip.Archive,
options? : Options = Options::new(),
limits? : ReadLimits = ReadLimits::new(),
cancelled? : () -> Bool = () => false,
transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
check_read_cancelled(cancelled)
require_bounded_archive(archive, limits, cancelled~)
let workbook = match transcoder {
Some(value) =>
read_zip_archive_core(
archive,
options,
limits,
cancelled,
transcoder=value,
)
None => read_zip_archive_core(archive, options, limits, cancelled)
}
checked_read_workbook(workbook, cancelled)
}
///|
/// Reads an XLSX package under a fail-closed resource policy.
///
/// `limits` applies to the compressed source, ZIP structure and expansion, and
/// individual XML-like OOXML parts. Encrypted packages require
/// `read_with_password` or an `Options` value containing a password.
pub fn read(
bytes : BytesView,
options? : Options = Options::new(),
limits? : ReadLimits = ReadLimits::new(),
cancelled? : () -> Bool = () => false,
transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
check_read_cancelled(cancelled)
check_source_package_size(bytes, limits)
if options.password != "" {
match transcoder {
Some(value) =>
return checked_read_workbook(
read_with_password(
bytes,
options.password,
options~,
limits~,
cancelled~,
transcoder=value,
),
cancelled,
)
None =>
return checked_read_workbook(
read_with_password(
bytes,
options.password,
options~,
limits~,
cancelled~,
),
cancelled,
)
}
}
if is_encrypted_package(bytes) {
raise EncryptedPackage
}
let workbook = match transcoder {
Some(value) =>
read_zip_bytes(bytes, options~, limits~, cancelled~, transcoder=value)
None => read_zip_bytes(bytes, options~, limits~, cancelled~)
}
checked_read_workbook(workbook, cancelled)
}
///|
/// Reads an XLSX package with an explicit password under a fail-closed resource
/// policy. Resource failures remain `ResourceLimitExceeded`; a verified
/// password preserves malformed-package errors, while an unverified payload
/// that is not a valid ZIP is classified as `InvalidPassword`.
pub fn read_with_password(
bytes : BytesView,
password : String,
options? : Options = Options::new(),
limits? : ReadLimits = ReadLimits::new(),
cancelled? : () -> Bool = () => false,
transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
check_read_cancelled(cancelled)
check_source_package_size(bytes, limits)
let resolved_options = options_with_password(options, password)
let workbook = if is_encrypted_package(bytes) {
let archive = decrypt_encrypted_archive(
bytes,
password,
limits~,
cancelled~,
)
require_bounded_archive(archive, limits, cancelled~)
match transcoder {
Some(value) =>
read_zip_archive_core(
archive,
resolved_options,
limits,
cancelled,
transcoder=value,
)
None =>
read_zip_archive_core(archive, resolved_options, limits, cancelled)
}
} else {
match transcoder {
Some(value) =>
read_zip_bytes(
bytes,
options=resolved_options,
limits~,
cancelled~,
transcoder=value,
)
None =>
read_zip_bytes(bytes, options=resolved_options, limits~, cancelled~)
}
}
checked_read_workbook(workbook, cancelled)
}
///|
fn is_invalid_xml_error(
result : Result[Font, XlsxError],
expected_msg : String,
) -> Bool {
match result {
Err(InvalidXml(msg~)) => msg == expected_msg
_ => false
}
}
///|
fn is_invalid_xml_fill_error(
result : Result[Fill, XlsxError],
expected_msg : String,
) -> Bool {
match result {
Err(InvalidXml(msg~)) => msg == expected_msg
_ => false
}
}
///|
fn is_invalid_xml_alignment_error(
result : Result[Alignment?, XlsxError],
expected_msg : String,
) -> Bool {
match result {
Err(InvalidXml(msg~)) => msg == expected_msg
_ => false
}
}
///|
fn has_border_type(borders : Array[Border], typ : String) -> Bool {
for border in borders {
if border.typ == typ {
return true
}
}
false
}
///|
fn border_color_by_type(borders : Array[Border], typ : String) -> String? {
for border in borders {
if border.typ == typ {
return border.color
}
}
None
}
///|
fn is_invalid_xml_string_array_error(
result : Result[Array[String], XlsxError],
expected_msg : String,
) -> Bool {
match result {
Err(InvalidXml(msg~)) => msg == expected_msg
_ => false
}
}
///|
fn is_invalid_xml_defined_name_error(
result : Result[Array[DefinedName], XlsxError],
expected_msg : String,
) -> Bool {
match result {
Err(InvalidXml(msg~)) => msg == expected_msg
_ => false
}
}
///|
test "read wb: xml encoding parser guard paths" {
inspect(xml_encoding(b"abc") is None, content="true")
inspect(xml_encoding(b"") is None, content="true")
inspect(
xml_encoding(b"") is None,
content="true",
)
let with_bom_single_quote = b"\xEF\xBB\xBF"
inspect(
match xml_encoding(with_bom_single_quote) {
Some(value) => value == "UTF-16"
None => false
},
content="true",
)
inspect(
xml_encoding(b"") is None,
content="true",
)
inspect(
xml_encoding(b"") is None,
content="true",
)
}
///|
test "read wb: decode_utf8 invalid paths" {
let requires_transcoder = Ok(
decode_utf8(b"", None),
) catch {
e => Err(e)
}
inspect(
match requires_transcoder {
Err(InvalidXml(msg~)) => msg.contains("requires charset transcoder")
_ => false
},
content="true",
)
let invalid_utf8 = Ok(decode_utf8(b"\xFF\xFF\xFF", None)) catch {
e => Err(e)
}
inspect(
match invalid_utf8 {
Err(InvalidXml(msg~)) => msg == "invalid utf8"
_ => false
},
content="true",
)
}
///|
test "read wb: parse_default_font fallback matrix" {
inspect(parse_default_font(""), content="Calibri")
inspect(parse_default_font(""), content="Calibri")
inspect(parse_default_font("[^<>]+?)"\>(?[^\/<>]+?)\<\/a\>/g
},
hljs.COMMENT(
'//', // begin
'\n', // end
)
]
}
}
hljs.registerLanguage('moonbit', moonbitLanguageFn);
hljs.highlightAll();
hljs.initLineNumbersOnLoad();
const number = window.location.href.split('#')[1];
function waitForLineNumbers() {
setTimeout(function () {
const target = document.querySelector(`.hljs-ln-line[data-line-number="${number}"]`);
if (target == null) waitForLineNumbers();
else target.scrollIntoView();
}, 50);
}
waitForLineNumbers()