///|
let office_read_max_package_bytes : Int = 64 * 1024 * 1024
///|
let office_read_max_zip_entries : Int = 4096
///|
let office_read_max_zip_entry_bytes : Int = 32 * 1024 * 1024
///|
let office_read_max_zip_total_bytes : Int = 128 * 1024 * 1024
///|
let office_read_max_preserved_source_bytes : Int = 64 * 1024 * 1024 + 65_535
///|
let office_read_max_xml_part_bytes : Int = 16 * 1024 * 1024
///|
let office_read_max_xml_total_units : Int = 64 * 1024 * 1024
///|
let office_read_max_workbook_sheets : Int = 256
///|
let office_read_max_parser_items : Int = 500_000
///|
let office_read_max_parser_work_units : Int = 256 * 1024 * 1024
///|
priv struct OfficeReadPackage {
file : String
format : @lib.DocumentFormat
bytes : Bytes
archive : @zip.Archive
}
///|
/// Reports cancellation that was delivered at an async suspension boundary.
/// The bounded synchronous package phases do not themselves run the scheduler.
fn office_async_cancelled() -> Bool {
@async.is_being_cancelled()
}
///|
fn check_office_read_cancelled(cancelled : () -> Bool) -> Unit raise CliFailure {
if cancelled() {
raise CliFailure(
@lib.protocol_error("office.cancelled", "Office read was cancelled"),
)
}
}
///|
fn detect_office_archive_format(
file : String,
archive : @zip.Archive,
cancelled : () -> Bool,
) -> @lib.DocumentFormat raise CliFailure {
check_office_read_cancelled(cancelled)
let format = @lib.detect_archive_format(
file,
archive,
max_xml_part_bytes=office_read_max_xml_part_bytes,
max_xml_total_units=office_read_max_xml_total_units,
cancelled~,
) catch {
ResourceLimit(kind~, limit~, actual~) =>
raise CliFailure(
@lib.protocol_error(
"office.\{office_read_format_hint(file).name()}.resource_limit",
"Office \{bounded_text(kind, 80)} exceeds the configured limit",
details=Json::object({
"file": Json::string(bounded_text(file, 160)),
"resource": Json::string(bounded_text(kind, 80)),
"limit": Json::number(limit.to_double()),
"actual": Json::number(actual.to_double()),
}),
),
)
Cancelled => {
check_office_read_cancelled(cancelled)
raise CliFailure(
@lib.protocol_error("office.cancelled", "Office read was cancelled"),
)
}
error => raise CliFailure(identify_error(error, file))
}
check_office_read_cancelled(cancelled)
format
}
///|
fn office_read_format_hint(
file : String,
) -> @lib.DocumentFormat raise CliFailure {
let lower = file.to_lower()
if lower.has_suffix(".xlsx") {
Xlsx
} else if lower.has_suffix(".docx") {
Docx
} else {
raise CliFailure(identify_error(UnsupportedFileExtension(file), file))
}
}
///|
fn zip_limit_kind_name(kind : @zip.LimitKind) -> String {
match kind {
PackageBytes => "package_bytes"
Entries => "entry_count"
EntryUncompressedBytes => "entry_uncompressed_bytes"
TotalUncompressedBytes => "total_uncompressed_bytes"
PreservedSourceBytes => "total_preserved_source_bytes"
OutputBytes => "output_bytes"
}
}
///|
fn office_zip_read_failure(
error : @zip.ZipError,
file : String,
expected : @lib.DocumentFormat,
) -> CliFailure {
match error {
ZipError(LimitExceeded(OutputBytes, limit, _actual), _) =>
format_resource_failure(expected.name(), "ZIP output", limit)
ZipError(LimitExceeded(kind, limit, actual), _) =>
CliFailure(
@lib.protocol_error(
"office.\{expected.name()}.resource_limit",
"\{expected.name().to_upper()} ZIP \{bounded_text(zip_limit_kind_name(kind), 80)} exceeds the configured limit",
details=Json::object({
"file": Json::string(bounded_text(file, 160)),
"resource": Json::string(
bounded_text(zip_limit_kind_name(kind), 80),
),
"limit": Json::number(limit.to_double()),
"actual": Json::number(actual.to_double()),
}),
),
)
_ =>
CliFailure(
@lib.protocol_error(
"office.invalid_package",
"invalid Office package: archive is not a readable bounded ZIP",
details=Json::object({ "file": Json::string(bounded_text(file, 160)) }),
),
)
}
}
///|
fn office_xlsx_read_limits() -> @xlsx.ReadLimits raise @xlsx.XlsxError {
@xlsx.ReadLimits::with_values(
max_package_bytes=office_read_max_package_bytes,
max_archive_entries=office_read_max_zip_entries,
max_entry_uncompressed_bytes=office_read_max_zip_entry_bytes,
max_total_uncompressed_bytes=office_read_max_zip_total_bytes,
max_total_preserved_source_bytes=office_read_max_preserved_source_bytes,
max_xml_part_bytes=office_read_max_xml_part_bytes,
max_workbook_sheets=office_read_max_workbook_sheets,
max_parser_items=office_read_max_parser_items,
max_parser_work_units=office_read_max_parser_work_units,
)
}
///|
async fn read_office_package(
file : String,
cancelled? : () -> Bool = () => false,
) -> OfficeReadPackage {
check_office_read_cancelled(cancelled)
let expected = office_read_format_hint(file)
let data = read_bounded_file(
file,
office_read_max_package_bytes,
"office.file_read_failed",
"input package",
limit_code="office.\{expected.name()}.resource_limit",
)
let archive = @zip.read(
data,
limits=@zip.ReadLimits::default()
.with_package_limit(office_read_max_package_bytes)
.with_entries_limit(office_read_max_zip_entries)
.with_entry_limit(office_read_max_zip_entry_bytes)
.with_total_limit(office_read_max_zip_total_bytes)
.with_preserved_source_limit(office_read_max_preserved_source_bytes),
cancelled~,
) catch {
error if cancelled() => raise error
error => raise office_zip_read_failure(error, file, expected)
}
let format = detect_office_archive_format(file, archive, cancelled)
{ file, format, bytes: data, archive, }
}
///|
fn xlsx_read_failure(error : @xlsx.XlsxError, file : String) -> CliFailure {
fn invalid_package(message : String) -> CliFailure {
CliFailure(
@lib.protocol_error(
"office.invalid_package",
"invalid XLSX package: " + bounded_text(message, 320),
details=Json::object({ "file": Json::string(bounded_text(file, 160)) }),
),
)
}
match error {
ResourceLimitExceeded(kind~, limit~, actual~) =>
CliFailure(
@lib.protocol_error(
"office.xlsx.resource_limit",
"XLSX \{bounded_text(kind, 80)} exceeds the configured limit",
details=Json::object({
"file": Json::string(bounded_text(file, 160)),
"resource": Json::string(bounded_text(kind, 80)),
"limit": Json::number(limit.to_double()),
"actual": Json::number(actual.to_double()),
}),
),
)
InvalidPackage(msg~) | InvalidXml(msg~) => invalid_package(msg)
MissingPart(path~) => invalid_package("missing required part: \{path}")
InvalidSharedString(index~) =>
invalid_package("shared string index is out of range: \{index}")
InvalidStyleId(index~) =>
invalid_package("cell style index is out of range: \{index}")
ReadCancelled =>
CliFailure(
@lib.protocol_error("office.cancelled", "XLSX read was cancelled"),
)
_ =>
CliFailure(
@lib.protocol_error(
"office.xlsx.read_failed",
"could not read XLSX workbook",
details=Json::object({ "file": Json::string(bounded_text(file, 160)) }),
),
)
}
}
///|
fn open_xlsx_read_package(
source : OfficeReadPackage,
cancelled? : () -> Bool = () => false,
) -> @xlsx.Workbook raise {
if source.format is Docx {
raise CliFailure(
@lib.protocol_error(
"office.xlsx.format_mismatch", "XLSX reader received a validated DOCX package",
),
)
}
let limits = office_xlsx_read_limits() catch {
error => raise xlsx_read_failure(error, source.file)
}
@xlsx.read_bounded_archive(source.archive, limits~, cancelled~) catch {
ReadCancelled as error => raise error
error => raise xlsx_read_failure(error, source.file)
}
}