///|
/// Ceiling on the dump document `office replay` will read.
let office_replay_max_input_bytes : Int = 64 * 1024 * 1024
///|
/// Ceiling on the workbook `office replay` will publish.
let office_replay_max_output_bytes : Int = 64 * 1024 * 1024
///|
async fn run_replay(matches : @argparse.Matches) -> Unit {
let file = required_value(matches, "file")
let output = required_value(matches, "output")
let overwrite = matches.flags.get_or_default("overwrite", false)
let mode = office_validate_output_mode(matches)
let bytes = read_bounded_file(
file,
office_replay_max_input_bytes,
"office.replay.dump_read_failed",
"dump document",
limit_code="office.replay.resource_limit",
)
let text = @utf8.decode(bytes, ignore_bom=true) catch {
_ =>
raise CliFailure(
@lib.protocol_error(
"office.replay.invalid_dump", "dump document is not valid UTF-8",
),
)
}
let document = @json.parse(text) catch {
_ =>
raise CliFailure(
@lib.protocol_error(
"office.replay.invalid_dump", "dump document is not valid JSON",
),
)
}
let envelope = @dump.parse_dump_envelope(document) catch {
DumpParseError(reason) =>
raise CliFailure(
@lib.protocol_error(
"office.replay.invalid_dump",
"malformed dump: " + bounded_text(reason, 200),
),
)
}
// the destination extension must match the replayed format so the
// published artifact is unambiguous
let expected_suffix = match envelope.format {
"xlsx" => ".xlsx"
"docx" => ".docx"
_ => ""
}
if expected_suffix != "" && !output.to_lower().has_suffix(expected_suffix) {
raise CliFailure(
@lib.protocol_error(
"office.invalid_arguments",
"--output must end in \{expected_suffix} for a \{envelope.format} dump",
),
)
}
let payload = match envelope.format {
"xlsx" => {
let workbook = @dump.replay_xlsx_dump(envelope) catch {
error => raise replay_engine_failure(error)
}
@xlsx.write(workbook) catch {
_ =>
raise CliFailure(
@lib.protocol_error(
"office.replay.write_failed", "could not serialize the replayed workbook",
),
)
}
}
"docx" =>
@dump.replay_docx_dump(envelope) catch {
error => raise replay_engine_failure(error)
}
_ =>
raise CliFailure(
@lib.protocol_error(
"office.replay.unsupported_format",
"replay supports XLSX and DOCX dumps",
details=Json::object({
"format": Json::string(bounded_text(envelope.format, 40)),
}),
),
)
}
if payload.length() > office_replay_max_output_bytes {
raise CliFailure(
@lib.protocol_error(
"office.replay.resource_limit",
"replayed workbook exceeds the configured limit",
details=Json::object({
"resource": Json::string("replay_output_bytes"),
"limit": Json::number(office_replay_max_output_bytes.to_double()),
"actual": Json::number(payload.length().to_double()),
}),
),
)
}
if overwrite {
@afs.remove(output) catch {
_ => ()
}
}
@transaction.atomic_write_new(output, payload) catch {
@transaction.TransactionError(..) as error =>
raise transaction_failure(error)
error => raise error
}
let report = Json::object({
"schema": Json::string(@lib.SCHEMA_REPLAY_RESULT),
"format": Json::string(envelope.format),
"output": Json::string(bounded_text(output, 160)),
"bytes_written": Json::number(payload.length().to_double()),
"ops_applied": Json::number(envelope.ops.length().to_double()),
})
match mode {
JsonDocument => println(@lib.output_success(report).stringify(indent=2))
JsonLines => println(@lib.output_success(report).stringify())
Human =>
println(
"replayed \{envelope.ops.length()} op(s) to " +
human_text(output, 160) +
" (\{payload.length()} bytes)",
)
}
}
///|
fn replay_command() -> @argparse.Command {
let summary = match @lib.find_capability_command("replay") {
Some(command) => command.summary
None => "Replay an office.dump/1 document into an XLSX workbook"
}
Command(
"replay",
about=summary,
positionals=[
PositionArg(
"file",
about="path to an office.dump/1 JSON document",
num_args=@argparse.ValueRange::single(),
),
],
options=[
OptionArg(
"output",
long="output",
about="destination path (.xlsx or .docx, matching the dump)",
),
],
flags=[
FlagArg(
"overwrite",
long="overwrite",
about="replace an existing destination",
),
FlagArg("json", long="json", about="print office.output/1 JSON"),
FlagArg("jsonl", long="jsonl", about="print one office.output/1 line"),
],
)
}
///|
/// Maps a replay engine error to a bounded protocol failure, shared by
/// both format paths.
fn replay_engine_failure(error : @dump.ReplayError) -> CliFailure {
match error {
ReplayInvalidFormat(reason) =>
CliFailure(
@lib.protocol_error(
"office.replay.invalid_dump",
"dump cannot be replayed: " + bounded_text(reason, 200),
),
)
ReplayFailed(reason) =>
CliFailure(
@lib.protocol_error(
"office.replay.failed",
"replay failed: " + bounded_text(reason, 200),
),
)
}
}