///|
fn bounded_actual_above_limit(limit : Int) -> Int {
if limit < 0x7fff_ffff {
limit + 1
} else {
limit
}
}
///|
fn bounded_int64_actual(value : Int64, limit : Int) -> Int {
if value < 0L {
bounded_actual_above_limit(limit)
} else if value > 0x7fff_ffffL {
0x7fff_ffff
} else {
value.to_int()
}
}
///|
test "bounded file-size telemetry saturates instead of truncating" {
inspect(
bounded_int64_actual(2_147_483_648L, 128 * 1024 * 1024),
content="2147483647",
)
inspect(bounded_actual_above_limit(0x7fff_ffff), content="2147483647")
}
///|
async fn[R : @async/io.Reader] read_all_bytes(
reader : R,
maximum? : Int = default_max_package_bytes,
resource? : String = "file_bytes",
) -> Bytes {
let output : Array[Byte] = []
while true {
// Keep one fixed bounded read window so a pipe producer can complete its
// current write before a limit error tears down the surrounding task group.
let chunk = reader.read_some(max_len=64 * 1024) catch {
error if @async.is_being_cancelled() => raise error
error =>
raise InvalidSheetOperation(
msg="reader failed: \{bounded_xlsx_error_text(error.to_string())}",
)
}
match chunk {
Some(bytes) => {
if bytes.length() == 0 {
raise InvalidSheetOperation(
msg="reader returned an empty non-EOF chunk",
)
}
if bytes.length() > maximum - output.length() {
raise ResourceLimitExceeded(
kind=resource,
limit=maximum,
actual=bounded_actual_above_limit(maximum),
)
}
for byte in bytes {
output.push(byte)
}
}
None => break
}
}
Bytes::from_array(output)
}
///|
#cfg(any(target="native", target="wasm"))
async fn read_bounded_package_file(
path : String,
maximum? : Int = default_max_package_bytes,
resource? : String = "file_bytes",
) -> Bytes {
let kind = @async/fs.kind(path) catch {
error if @async.is_being_cancelled() => raise error
error =>
raise InvalidSheetOperation(
msg="inspect file failed: \{bounded_xlsx_error_text(error.to_string())}",
)
}
if !(kind is Regular) {
raise InvalidSheetOperation(msg="input must be a regular file")
}
let file = @async/fs.open(
path,
mode=ReadOnly,
create_mode=OpenExisting,
sync=NoSync,
) catch {
error if @async.is_being_cancelled() => raise error
error =>
raise InvalidSheetOperation(
msg="open file failed: \{bounded_xlsx_error_text(error.to_string())}",
)
}
defer file.close()
if !(file.kind() is Regular) {
raise InvalidSheetOperation(msg="input must remain a regular file")
}
let size = file.size() catch {
error if @async.is_being_cancelled() => raise error
error =>
raise InvalidSheetOperation(
msg="size file failed: \{bounded_xlsx_error_text(error.to_string())}",
)
}
if size < 0L || size > maximum.to_int64() {
raise ResourceLimitExceeded(
kind=resource,
limit=maximum,
actual=bounded_int64_actual(size, maximum),
)
}
let length = size.to_int()
let buffer = FixedArray::make(length, b'\x00')
let mut offset = 0
while offset < length {
let count = file.read_at(
buffer,
position=offset.to_int64(),
offset~,
len=length - offset,
) catch {
error if @async.is_being_cancelled() => raise error
error =>
raise InvalidSheetOperation(
msg="read file failed: \{bounded_xlsx_error_text(error.to_string())}",
)
}
if count <= 0 || count > length - offset {
raise InvalidSheetOperation(msg="file changed while being read")
}
offset += count
}
let final_size = file.size() catch {
error if @async.is_being_cancelled() => raise error
error =>
raise InvalidSheetOperation(
msg="size file failed: \{bounded_xlsx_error_text(error.to_string())}",
)
}
if final_size != size {
raise InvalidSheetOperation(msg="file changed while being read")
}
@async.pause()
buffer.unsafe_reinterpret_as_bytes()
}
///|
#cfg(target="native")
async fn read_file_bytes(path : String) -> Bytes raise XlsxError {
let data = @async/fs.read_file(path) catch {
error =>
raise InvalidSheetOperation(
msg="read file failed: \{bounded_xlsx_error_text(error.to_string())}",
)
}
data.binary()
}
///|
fn bounded_xlsx_error_text(value : String) -> String {
let output = StringBuilder::new()
let mut count = 0
for character in value {
if character == '\n' || character == '\r' {
break
}
if count >= 240 {
output.write_char('…') |> ignore
break
}
output.write_char(character) |> ignore
count += 1
}
output.to_string()
}
///|
fn resolve_write_options(workbook : Workbook, options : Options?) -> Options {
match options {
Some(value) => value
None => workbook.options
}
}
///|
fn resolve_write_io_context(
workbook : Workbook,
file_path_override : String?,
) -> WorkbookIOContext {
let io_context = workbook_io_context(workbook)
match file_path_override {
Some(value) => workbook_io_context_with_file_path(io_context, Some(value))
None => io_context
}
}
///|
fn write_bytes_with_io(
workbook : Workbook,
options : Options?,
file_path_override : String?,
) -> Bytes raise XlsxError {
let resolved = resolve_write_options(workbook, options)
let io_context = resolve_write_io_context(workbook, file_path_override)
if resolved.password == "" {
write_with_io(workbook, io_context)
} else {
let raw = write_with_io(workbook, io_context)
if resolved.password.length() == 0 {
raw
} else {
encrypt_package(raw, resolved.password)
}
}
}
///|
fn write_bytes(
workbook : Workbook,
options : Options?,
) -> Bytes raise XlsxError {
write_bytes_with_io(workbook, options, None)
}
///|
fn read_io_context(
transcoder : ((String, Bytes) -> String raise XlsxError)?,
) -> WorkbookIOContext {
workbook_io_context_with_transcoder(empty_workbook_io_context(), transcoder)
}
///|
fn read_workbook_from_bytes(
bytes : Bytes,
password : String,
options : Options,
limits : ReadLimits,
io_context : WorkbookIOContext,
) -> Workbook raise XlsxError {
let resolved_password = if password == "" {
options.password
} else {
password
}
let resolved_options = options_with_password(options, resolved_password)
if resolved_password == "" {
match io_context.charset_transcoder {
Some(value) =>
read_zip_bytes(
bytes,
options=resolved_options,
limits~,
transcoder=value,
)
None => read_zip_bytes(bytes, options=resolved_options, limits~)
}
} else {
match io_context.charset_transcoder {
Some(value) =>
read_with_password(
bytes,
resolved_password,
options=resolved_options,
limits~,
transcoder=value,
)
None =>
read_with_password(
bytes,
resolved_password,
options=resolved_options,
limits~,
)
}
}
}
///|
/// Reads an XLSX package asynchronously from a reader, stopping before the
/// configured compressed package limit is exceeded. Reader I/O is cancellable;
/// the subsequent resource-bounded semantic parse is currently synchronous.
pub async fn[R : @async/io.Reader] open_reader(
reader : R,
password? : String = "",
options? : Options = Options::new(),
limits? : ReadLimits = ReadLimits::new(),
transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook {
let bytes = read_all_bytes(
reader,
maximum=limits.max_package_bytes,
resource="package_bytes",
)
// Give scheduled peers and cancellation one explicit turn even when a custom
// in-memory Reader completed every read immediately. Mid-parse scheduler
// cooperation requires the shared resumable parser tracked in issue #174.
@async.pause()
let io_context = read_io_context(transcoder)
let workbook = read_workbook_from_bytes(
bytes, password, options, limits, io_context,
)
workbook.set_io_context(io_context)
workbook
}
///|
/// Alias for `open_reader`, retained as the streaming XLSX read entry point.
pub async fn[R : @async/io.Reader] read_zip_reader(
reader : R,
password? : String = "",
options? : Options = Options::new(),
limits? : ReadLimits = ReadLimits::new(),
transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook {
match transcoder {
Some(value) =>
open_reader(reader, password~, options~, limits~, transcoder=value)
None => open_reader(reader, password~, options~, limits~)
}
}
///|
/// Replaces this workbook with one read asynchronously from a reader while
/// preserving its read options and transcoder when no overrides are supplied.
pub async fn[R : @async/io.Reader] Workbook::read_zip_reader(
self : Workbook,
reader : R,
password? : String = "",
options? : Options,
limits? : ReadLimits = ReadLimits::new(),
transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook {
let resolved_options = match options {
Some(value) => value
None => self.options
}
let base_io_context = workbook_io_context(self)
let resolved_transcoder = match transcoder {
Some(value) => Some(value)
None => base_io_context.charset_transcoder
}
let io_context = read_io_context(resolved_transcoder)
let bytes = read_all_bytes(
reader,
maximum=limits.max_package_bytes,
resource="package_bytes",
)
@async.pause()
let workbook = read_workbook_from_bytes(
bytes, password, resolved_options, limits, io_context,
)
workbook.set_io_context(io_context)
workbook
}
///|
#cfg(any(target="native", target="wasm"))
/// Opens an XLSX package asynchronously after checking its regular-file type
/// and compressed size before allocation. A file that changes during the read
/// is rejected.
pub async fn open_file(
path : String,
password? : String = "",
options? : Options = Options::new(),
limits? : ReadLimits = ReadLimits::new(),
transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook {
let bytes = read_bounded_package_file(
path,
maximum=limits.max_package_bytes,
resource="package_bytes",
)
let io_context = workbook_io_context_with_file_path(
read_io_context(transcoder),
Some(path),
)
let workbook = read_workbook_from_bytes(
bytes, password, options, limits, io_context,
)
workbook.set_io_context(io_context)
workbook
}
///|
#cfg(target="native")
pub async fn Workbook::set_sheet_background_from_file(
self : Workbook,
sheet_name : StringView,
path : String,
) -> Unit raise XlsxError {
let extension = match extension_from_path(path) {
Some(value) => value
None => raise InvalidSheetBackground(msg="image extension missing")
}
let bytes = read_file_bytes(path)
self.set_sheet_background(sheet_name, bytes, extension)
}
///|
#cfg(target="native")
pub async fn Workbook::save(
self : Workbook,
options? : Options,
) -> Unit raise XlsxError {
match workbook_io_context(self).file_path {
Some(path) =>
match options {
Some(value) => self.save_as(path, options=value)
None => self.save_as(path)
}
None => raise InvalidSheetOperation(msg="workbook path not set")
}
}
///|
#cfg(target="native")
pub async fn Workbook::save_as(
self : Workbook,
path : String,
options? : Options,
) -> Unit raise XlsxError {
let extension = match extension_from_path(path) {
Some(value) => value
None => raise InvalidSheetOperation(msg="workbook file extension missing")
}
if extension != "xlsx" &&
extension != "xlsm" &&
extension != "xlam" &&
extension != "xltm" &&
extension != "xltx" {
raise InvalidSheetOperation(msg="unsupported workbook file format")
}
let bytes = write_bytes_with_io(self, options, Some(path))
@async/fs.write_file(
path,
bytes,
create_mode=CreateOrTruncate,
permission=0o644,
) catch {
err => raise InvalidSheetOperation(msg="write file failed: \{err}")
}
self.set_io_context(
workbook_io_context_with_file_path(workbook_io_context(self), Some(path)),
)
}
///|
pub fn Workbook::write_to_buffer(
self : Workbook,
options? : Options,
) -> Bytes raise XlsxError {
write_bytes(self, options)
}
///|
pub async fn[W : @async/io.Writer] Workbook::write(
self : Workbook,
writer : W,
options? : Options,
) -> Unit raise XlsxError {
match options {
Some(value) => ignore(self.write_to(writer, options=value))
None => ignore(self.write_to(writer))
}
}
///|
pub async fn[W : @async/io.Writer] Workbook::write_to(
self : Workbook,
writer : W,
options? : Options,
) -> Int raise XlsxError {
let bytes = write_bytes(self, options)
writer.write(bytes) catch {
err => raise InvalidSheetOperation(msg="writer failed: \{err}")
}
bytes.length()
}