From b2a4b2c3f06a9ad208cf9dc8945dc5251ce01f3f Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Mon, 12 May 2025 08:57:32 -0700 Subject: [PATCH] work --- rust/Cargo.lock | 428 +++++++++++++++++- rust/doc/src/spv/structure.md | 4 +- rust/pspp/Cargo.toml | 2 + rust/pspp/src/format/mod.rs | 50 ++- rust/pspp/src/message.rs | 10 +- rust/pspp/src/output/mod.rs | 33 +- rust/pspp/src/output/pivot/mod.rs | 40 +- rust/pspp/src/output/spv.rs | 700 ++++++++++++++++++++++++++++++ 8 files changed, 1235 insertions(+), 32 deletions(-) create mode 100644 rust/pspp/src/output/spv.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9e6c650845..89b5d190b1 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -96,6 +107,15 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "array-init" version = "2.1.0" @@ -192,11 +212,20 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" @@ -204,12 +233,37 @@ version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.20.7" @@ -239,6 +293,8 @@ version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -283,6 +339,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.16" @@ -339,12 +405,42 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -354,6 +450,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -367,6 +479,32 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -394,6 +532,17 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "either" version = "1.13.0" @@ -613,6 +762,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.3.2" @@ -620,9 +779,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -753,6 +914,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "httparse" version = "1.9.4" @@ -808,6 +978,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -829,12 +1008,22 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -874,9 +1063,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lsp-types" @@ -891,6 +1080,27 @@ dependencies = [ "url", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "matrixmultiply" version = "0.3.9" @@ -976,6 +1186,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -1119,6 +1335,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1178,6 +1404,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1249,6 +1481,8 @@ dependencies = [ "unicode-linebreak", "unicode-width", "windows-sys 0.48.0", + "xmlwriter", + "zip", ] [[package]] @@ -1389,6 +1623,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" version = "1.0.18" @@ -1453,6 +1693,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1468,6 +1719,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -1508,6 +1765,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1588,6 +1851,25 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + [[package]] name = "tinyvec" version = "1.8.0" @@ -1770,6 +2052,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicase" version = "2.7.0" @@ -1865,24 +2153,24 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn 2.0.87", @@ -1891,9 +2179,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1901,9 +2189,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -1914,9 +2202,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "winapi" @@ -2121,6 +2412,21 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "zerocopy" version = "0.8.24" @@ -2140,3 +2446,91 @@ dependencies = [ "quote", "syn 2.0.87", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zip" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "flate2", + "getrandom", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/rust/doc/src/spv/structure.md b/rust/doc/src/spv/structure.md index b915eac6bb..85c515ad4f 100644 --- a/rust/doc/src/spv/structure.md +++ b/rust/doc/src/spv/structure.md @@ -208,7 +208,7 @@ children of the root `heading` elements in all structure members in an SPV file are siblings. That is, the root `heading` in all of the structure members conceptually represent the same node. The root heading's `label` is ignored (see [the `label` -element)(#the-label-element)). The root heading in the first +element](#the-label-element)). The root heading in the first structure member in the Zip file may contain a `pageSetup` element. The schema implies that any `heading` may contain a sequence of any @@ -598,7 +598,7 @@ element](#the-graph-element), for more information. These elements have no attributes. -## The ‘pageSetup’ Element +## The `pageSetup` Element ``` pageSetup diff --git a/rust/pspp/Cargo.toml b/rust/pspp/Cargo.toml index ff7d59235c..66a8d36703 100644 --- a/rust/pspp/Cargo.toml +++ b/rust/pspp/Cargo.toml @@ -42,6 +42,8 @@ derive_more = { version = "2.0.1", features = ["debug"] } cairo-rs = { version = "0.20.7", features = ["ps", "png", "pdf", "svg"] } pango = "0.20.9" pangocairo = "0.20.7" +zip = "2.6.1" +xmlwriter = "0.1.0" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.48.0", features = ["Win32_Globalization"] } diff --git a/rust/pspp/src/format/mod.rs b/rust/pspp/src/format/mod.rs index e0eedf9d3e..e03fd714e9 100644 --- a/rust/pspp/src/format/mod.rs +++ b/rust/pspp/src/format/mod.rs @@ -332,7 +332,7 @@ impl Type { } } - pub fn as_string(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match self { Self::F => "F", Self::Comma => "COMMA", @@ -384,7 +384,7 @@ impl Type { impl Display for Type { fn fmt(&self, f: &mut Formatter) -> FmtResult { - write!(f, "{}", self.as_string()) + write!(f, "{}", self.as_str()) } } @@ -393,7 +393,7 @@ impl FromStr for Type { fn from_str(s: &str) -> Result { for type_ in all::() { - if type_.as_string().eq_ignore_ascii_case(s) { + if type_.as_str().eq_ignore_ascii_case(s) { return Ok(type_); } } @@ -655,6 +655,50 @@ impl TryFrom for Format { } } +impl From for u16 { + fn from(source: Type) -> Self { + match source { + Type::A => 1, + Type::AHex => 2, + Type::Comma => 3, + Type::Dollar => 4, + Type::F => 5, + Type::IB => 6, + Type::PIBHex => 7, + Type::P => 8, + Type::PIB => 9, + Type::PK => 10, + Type::RB => 11, + Type::RBHex => 12, + Type::Z => 15, + Type::N => 16, + Type::E => 17, + Type::Date => 20, + Type::Time => 21, + Type::DateTime => 22, + Type::ADate => 23, + Type::JDate => 24, + Type::DTime => 25, + Type::WkDay => 26, + Type::Month => 27, + Type::MoYr => 28, + Type::QYr => 29, + Type::WkYr => 30, + Type::Pct => 31, + Type::Dot => 32, + Type::CC(CC::A) => 33, + Type::CC(CC::B) => 34, + Type::CC(CC::C) => 35, + Type::CC(CC::D) => 36, + Type::CC(CC::E) => 37, + Type::EDate => 38, + Type::SDate => 39, + Type::MTime => 40, + Type::YmdHms => 41, + } + } +} + impl TryFrom for Type { type Error = Error; diff --git a/rust/pspp/src/message.rs b/rust/pspp/src/message.rs index b128b5ac0d..abc7f424f2 100644 --- a/rust/pspp/src/message.rs +++ b/rust/pspp/src/message.rs @@ -128,13 +128,21 @@ pub enum Severity { } impl Severity { - fn as_str(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match self { Severity::Error => "error", Severity::Warning => "warning", Severity::Note => "note", } } + + pub fn as_title_str(&self) -> &'static str { + match self { + Severity::Error => "Error", + Severity::Warning => "Warning", + Severity::Note => "Note", + } + } } impl Display for Severity { diff --git a/rust/pspp/src/output/mod.rs b/rust/pspp/src/output/mod.rs index 7f23a50cb9..ea3b4274ee 100644 --- a/rust/pspp/src/output/mod.rs +++ b/rust/pspp/src/output/mod.rs @@ -1,5 +1,5 @@ #![allow(dead_code)] -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; use pivot::PivotTable; @@ -14,6 +14,7 @@ pub mod html; pub mod page; pub mod pivot; pub mod render; +pub mod spv; pub mod table; pub mod text; pub mod text_line; @@ -51,6 +52,13 @@ impl Item { details, } } + + pub fn label(&self) -> Cow<'static, str> { + match &self.label { + Some(label) => Cow::from(label.clone()), + None => self.details.label(), + } + } } pub enum Details { @@ -82,6 +90,18 @@ impl Details { Details::Table(pivot_table) => pivot_table.command_c.as_ref(), } } + + pub fn label(&self) -> Cow<'static, str> { + match self { + Details::Chart => todo!(), + Details::Image => todo!(), + Details::Group(_) => Cow::from("Group"), + Details::Message(diagnostic) => Cow::from(diagnostic.severity.as_title_str()), + Details::PageBreak => Cow::from("Page Break"), + Details::Table(pivot_table) => Cow::from(pivot_table.label()), + Details::Text(text) => Cow::from(text.type_.as_str()), + } + } } pub struct Text { @@ -104,6 +124,17 @@ pub enum TextType { Log, } +impl TextType { + pub fn as_str(&self) -> &'static str { + match self { + TextType::PageTitle => "Page Title", + TextType::Title => "Title", + TextType::Syntax => "Log", + TextType::Log => "Log", + } + } +} + pub struct ItemCursor { cur: Option>, stack: Vec<(Arc, usize)>, diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index 14a6e51055..2d5d3683cc 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -885,6 +885,10 @@ impl Color { Self { alpha, ..self } } + pub const fn without_alpha(self) -> Self { + self.with_alpha(255) + } + pub fn display_css(&self) -> DisplayCss { DisplayCss(*self) } @@ -1291,34 +1295,34 @@ pub struct PivotTable { } impl PivotTable { - fn with_title(mut self, title: impl Into) -> Self { + pub fn with_title(mut self, title: impl Into) -> Self { self.title = Some(Box::new(title.into())); self.show_title = true; self } - fn with_caption(mut self, caption: Value) -> Self { + pub fn with_caption(mut self, caption: Value) -> Self { self.caption = Some(Box::new(caption)); self.show_caption = true; self } - fn with_corner_text(mut self, corner_text: Value) -> Self { + pub fn with_corner_text(mut self, corner_text: Value) -> Self { self.corner_text = Some(Box::new(corner_text)); self } - fn with_show_title(mut self, show_title: bool) -> Self { + pub fn with_show_title(mut self, show_title: bool) -> Self { self.show_title = show_title; self } - fn with_show_caption(mut self, show_caption: bool) -> Self { + pub fn with_show_caption(mut self, show_caption: bool) -> Self { self.show_caption = show_caption; self } - fn with_layer(mut self, layer: &[usize]) -> Self { + pub fn with_layer(mut self, layer: &[usize]) -> Self { debug_assert_eq!(layer.len(), self.current_layer.len()); if self.look.print_all_layers { self.look_mut().print_all_layers = false; @@ -1328,16 +1332,23 @@ impl PivotTable { self } - fn with_all_layers(mut self) -> Self { + pub fn with_all_layers(mut self) -> Self { if !self.look.print_all_layers { self.look_mut().print_all_layers = true; } self } - fn look_mut(&mut self) -> &mut Look { + pub fn look_mut(&mut self) -> &mut Look { Arc::make_mut(&mut self.look) } + + pub fn label(&self) -> String { + match &self.title { + Some(title) => title.display(self).to_string(), + None => String::from("Table"), + } + } } impl Default for PivotTable { @@ -1542,14 +1553,17 @@ impl Footnote { self.marker = Some(Box::new(marker.into())); self } + pub fn with_show(mut self, show: bool) -> Self { self.show = show; self } + pub fn with_index(mut self, index: usize) -> Self { self.index = index; self } + pub fn display_marker(&self, options: impl IntoValueOptions) -> DisplayMarker<'_> { DisplayMarker { footnote: self, @@ -1560,6 +1574,10 @@ impl Footnote { pub fn display_content(&self, options: impl IntoValueOptions) -> DisplayValue<'_> { self.content.display(options) } + + pub fn index(&self) -> usize { + self.index + } } pub struct DisplayMarker<'a> { @@ -2079,6 +2097,12 @@ pub struct ValueStyle { pub footnotes: Vec>, } +impl ValueStyle { + pub fn is_empty(&self) -> bool { + self.style.is_none() && self.subscripts.is_empty() && self.footnotes.is_empty() + } +} + impl ValueInner { // Returns an object that will format this value. Settings on `options` // control whether variable and value labels are included. diff --git a/rust/pspp/src/output/spv.rs b/rust/pspp/src/output/spv.rs new file mode 100644 index 0000000000..e421b97da3 --- /dev/null +++ b/rust/pspp/src/output/spv.rs @@ -0,0 +1,700 @@ +use core::f64; +use std::{ + borrow::Cow, + fmt::Write as _, + io::{Cursor, Result as IoResult, Seek, Write}, + sync::Arc, +}; + +use binrw::{BinWrite, Endian}; +use quick_xml::{ + events::{attributes::Attribute, BytesText}, + writer::Writer as XmlWriter, + ElementWriter, +}; +use serde::Serialize; +use smallstr::SmallString; +use zip::{result::ZipResult, write::SimpleFileOptions, ZipWriter}; + +use crate::{ + format::{Format, Type}, + output::{ + driver::Driver, + pivot::{ + Axis2, CellStyle, Color, FontStyle, HeadingRegion, HorzAlign, PivotTable, Value, + ValueInner, ValueStyle, VertAlign, + }, + Item, + }, + settings::Show, +}; + +fn light_table_name(table_id: u64) -> String { + format!("{:010}_lightTableData.bin", table_id) +} + +pub struct SpvWriter +where + W: Write + Seek, +{ + writer: ZipWriter, + needs_page_break: bool, + next_table_id: u64, +} + +impl SpvWriter +where + W: Write + Seek, +{ + pub fn new(writer: W) -> Self { + Self { + writer: ZipWriter::new(writer), + needs_page_break: false, + next_table_id: 1, + } + } + + pub fn close(mut self) -> ZipResult { + self.writer + .start_file("META-INF/MANIFEST.MF", SimpleFileOptions::default())?; + write!(&mut self.writer, "allowPivoting=true")?; + self.writer.finish() + } + + fn page_break_before(&mut self) -> bool { + let page_break_before = self.needs_page_break; + self.needs_page_break = false; + page_break_before + } + + fn write_table(&mut self, item: &Item, pivot_table: &PivotTable) -> Container { + let table_id = self.next_table_id; + self.next_table_id += 1; + + let mut content = Vec::new(); + let mut cursor = Cursor::new(&mut content); + Header::new(pivot_table).write_le(&mut cursor).unwrap(); + + self.writer + .start_file(light_table_name(table_id), SimpleFileOptions::default()) + .unwrap(); // XXX + self.writer.write_all(&content).unwrap(); // XXX + + Container { + page_break_before: self.page_break_before(), + label: Label(item.label().into_owned()), + show: item.show, + command_name: item.command_name.clone(), + content: Content::Table(Table { + table_properties: None, + table_structure: TableStructure, + table_id, + subtype: match &pivot_table.subtype { + Some(subtype) => subtype.display(pivot_table).to_string(), + None => String::from("unknown"), + }, + }), + } + } +} + +impl Driver for SpvWriter +where + W: Write + Seek, +{ + fn name(&self) -> Cow<'static, str> { + Cow::from("spv") + } + + fn write(&mut self, item: &Arc) { + match &item.details { + super::Details::Chart => todo!(), + super::Details::Image => todo!(), + super::Details::Group(items) => todo!(), + super::Details::Message(diagnostic) => todo!(), + super::Details::PageBreak => { + self.needs_page_break = true; + return; + } + super::Details::Table(pivot_table) => self.write_table(&*item, pivot_table), + super::Details::Text(text) => todo!(), + }; + todo!() + } +} + +struct Heading { + command_name: Option, + show: bool, + label: Label, + children: Vec, +} + +impl Heading { + fn emit(&self, writer: &mut XmlWriter) -> IoResult<()> + where + W: Write, + { + let mut element = writer.create_element("heading"); + if let Some(command_name) = &self.command_name { + element = element.with_attribute(("commandName", command_name.as_str())); + } + if !self.show { + element = element.with_attribute(("visibility", "collapsed")); + } + element.write_inner_content(|writer| { + self.label.emit(writer)?; + Ok(()) + })?; + Ok(()) + } +} + +enum Child { + Container(Container), + Heading(Box), +} + +impl Child { + fn emit(&self, writer: &mut XmlWriter) -> IoResult<()> + where + W: Write, + { + match self { + Child::Container(container) => container.emit(writer), + Child::Heading(heading) => heading.emit(writer), + } + } +} + +fn maybe_with_attribute<'a, 'b, W, I>( + element: ElementWriter<'a, W>, + attr: Option, +) -> ElementWriter<'a, W> +where + I: Into>, +{ + if let Some(attr) = attr { + element.with_attribute(attr) + } else { + element + } +} + +struct Container { + page_break_before: bool, + label: Label, + show: bool, + command_name: Option, + content: Content, +} + +impl Container { + fn emit(&self, writer: &mut XmlWriter) -> IoResult<()> + where + W: Write, + { + let mut element = writer + .create_element("container") + .with_attribute(("visibility", if self.show { "visible" } else { "hidden" })); + if self.page_break_before { + element = element.with_attribute(("page-break-before", "always")); + } + element.write_inner_content(|writer| { + self.label.emit(writer)?; + self.content + .emit(writer, self.command_name.as_ref().map(|name| name.as_str()))?; + Ok(()) + })?; + Ok(()) + } +} + +struct Label(String); + +impl Label { + fn emit(&self, writer: &mut XmlWriter) -> IoResult<()> + where + W: Write, + { + writer + .create_element("label") + .write_text_content(BytesText::new(&self.0))?; + Ok(()) + } +} + +enum Content { + Table(Table), +} + +impl Content { + fn emit(&self, writer: &mut XmlWriter, command_name: Option<&str>) -> IoResult<()> + where + W: Write, + { + match self { + Content::Table(table) => table.emit(writer, command_name), + } + } + + fn element<'a, W>( + writer: &'a mut XmlWriter, + name: &'static str, + command_name: Option<&str>, + ) -> ElementWriter<'a, W> { + let element = writer.create_element(name); + let element = maybe_with_attribute( + element, + command_name.map(|command_name| ("commandName", command_name)), + ); + element + } +} + +struct Table { + table_properties: Option<()>, + + table_structure: TableStructure, + table_id: u64, + subtype: String, +} + +impl Table { + fn emit(&self, writer: &mut XmlWriter, command_name: Option<&str>) -> IoResult<()> + where + W: Write, + { + Content::element(writer, "vtb:table", command_name) + .with_attribute(("type", "table")) + .with_attribute(("tableId", Cow::from(format!("{}", self.table_id)))) + .with_attribute(("subtype", self.subtype.as_str())) + .write_inner_content(|w| { + w.create_element("vtb:TableStructure") + .write_inner_content(|w| { + w.create_element("vtb:dataPath") + .write_text_content(BytesText::new(&light_table_name(self.table_id)))?; + Ok(()) + })?; + Ok(()) + })?; + Ok(()) + } +} + +#[derive(Serialize)] +struct TableStructure; + +struct Bool(bool); +impl BinWrite for Bool { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + (self.0 as u8).write_options(writer, endian, args) + } +} + +struct SpvString<'a>(&'a str); +impl<'a> SpvString<'a> { + fn optional(s: &'a Option) -> Self { + Self(s.as_ref().map_or("", |s| s.as_str())) + } +} +impl BinWrite for SpvString<'_> { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + let length = self.0.len() as u32; + (length, self.0.as_bytes()).write_options(writer, endian, args) + } +} + +#[derive(BinWrite)] +#[bw(little)] +struct Header { + #[bw(magic(1u16))] + version: u8, + + x0: Bool, + x1: Bool, + rotate_inner_column_labels: Bool, + rotate_outer_row_labels: Bool, + x2: Bool, + x3: u32, + min_col_heading_width: i32, + max_col_heading_width: i32, + min_row_heading_width: i32, + max_row_heading_width: i32, +} + +impl Header { + fn new(pivot_table: &PivotTable) -> Self { + Self { + version: 3, + x0: Bool(true), + x1: Bool(false), + rotate_inner_column_labels: Bool(pivot_table.rotate_inner_column_labels), + rotate_outer_row_labels: Bool(pivot_table.rotate_outer_row_labels), + x2: Bool(true), + x3: 0x15, + min_col_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Columns].start() + as i32, + max_col_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Columns].end() + as i32, + min_row_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Rows].start() + as i32, + max_row_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Rows].end() + as i32, + } + } +} + +impl Show { + fn as_spv(this: &Option) -> u8 { + match this { + None => 0, + Some(Show::Value) => 1, + Some(Show::Label) => 2, + Some(Show::Both) => 3, + } + } +} + +struct Count(u64); + +impl Count { + fn new(writer: &mut W) -> binrw::BinResult + where + W: Write + Seek, + { + 0u32.write_le(writer)?; + Ok(Self(writer.stream_position()?)) + } + + fn finish(self, writer: &mut W, endian: Endian) -> binrw::BinResult<()> + where + W: Write + Seek, + { + let saved_position = writer.stream_position()?; + let n_bytes = saved_position - self.0; + writer.seek(std::io::SeekFrom::Start(self.0 - 4))?; + (n_bytes as u32).write_options(writer, endian, ())?; + writer.seek(std::io::SeekFrom::Start(saved_position))?; + Ok(()) + } + + fn finish_le32(self, writer: &mut W) -> binrw::BinResult<()> + where + W: Write + Seek, + { + self.finish(writer, Endian::Little) + } + + fn finish_be32(self, writer: &mut W) -> binrw::BinResult<()> + where + W: Write + Seek, + { + self.finish(writer, Endian::Big) + } +} + +#[derive(Default)] +struct StylePair<'a> { + font_style: Option<&'a FontStyle>, + cell_style: Option<&'a CellStyle>, +} + +impl BinWrite for Color { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + let mut s = SmallString::<[u8; 16]>::new(); + write!(&mut s, "{}", self.without_alpha().display_css()).unwrap(); + SpvString(&s).write_options(writer, endian, args) + } +} + +impl BinWrite for FontStyle { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + let typeface = if self.font.is_empty() { + "SansSerif" + } else { + self.font.as_str() + }; + ( + Bool(self.bold), + Bool(self.italic), + Bool(self.underline), + Bool(true), + self.fg[0], + self.bg[0], + SpvString(typeface), + (self.size as f64 * 1.33).ceil() as u8, + ) + .write_options(writer, endian, args) + } +} + +impl HorzAlign { + fn as_spv(&self, decimal: u32) -> u32 { + match self { + HorzAlign::Right => 4, + HorzAlign::Left => 2, + HorzAlign::Center => 0, + HorzAlign::Decimal { .. } => decimal, + } + } + + fn decimal_offset(&self) -> Option { + match *self { + HorzAlign::Decimal { offset, .. } => Some(offset), + _ => None, + } + } +} + +impl VertAlign { + fn as_spv(&self) -> u32 { + match self { + VertAlign::Top => 1, + VertAlign::Middle => 0, + VertAlign::Bottom => 3, + } + } +} + +impl BinWrite for CellStyle { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + ( + self.horz_align + .map_or(0xffffffad, |horz_align| horz_align.as_spv(6)), + self.vert_align.as_spv(), + self.horz_align + .map(|horz_align| horz_align.decimal_offset()) + .unwrap_or_default(), + u16::try_from(self.margins[Axis2::X][0]).unwrap_or_default(), + u16::try_from(self.margins[Axis2::X][1]).unwrap_or_default(), + u16::try_from(self.margins[Axis2::Y][0]).unwrap_or_default(), + u16::try_from(self.margins[Axis2::Y][1]).unwrap_or_default(), + ) + .write_options(writer, endian, args) + } +} + +impl<'a> BinWrite for StylePair<'a> { + type Args<'b> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + if let Some(font_style) = self.font_style { + (0x31u8, font_style).write_options(writer, endian, args)?; + } else { + 0x58u8.write_options(writer, endian, args)?; + } + + if let Some(cell_style) = self.cell_style { + (0x31u8, cell_style).write_options(writer, endian, args)?; + } else { + 0x58u8.write_options(writer, endian, args)?; + } + + Ok(()) + } +} + +struct OptionalStyle<'a> { + style: &'a Option>, + template: Option<&'a str>, +} + +impl<'a> BinWrite for OptionalStyle<'a> { + type Args<'b> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + if self.style.as_ref().is_some_and(|style| !style.is_empty()) || self.template.is_some() { + 0x31u8.write_options(writer, endian, args)?; + let default_style = Default::default(); + let style = self.style.as_ref().unwrap_or(&default_style); + + (style.footnotes.len() as u32).write_options(writer, endian, args)?; + for footnote in &style.footnotes { + (footnote.index() as u16).write_options(writer, endian, args)?; + } + + (style.subscripts.len() as u32).write_options(writer, endian, args)?; + for subscript in &style.subscripts { + SpvString(subscript.as_str()).write_options(writer, endian, args)?; + } + let v3_start = Count::new(writer)?; + let template_string_start = Count::new(writer)?; + if let Some(template) = self.template { + Count::new(writer)?.finish_le32(writer)?; + (0x31u8, SpvString(template)).write_options(writer, endian, args)?; + } + template_string_start.finish_le32(writer)?; + style + .style + .as_ref() + .map_or_else( + || StylePair::default(), + |area_style| StylePair { + font_style: Some(&area_style.font_style), + cell_style: Some(&area_style.cell_style), + }, + ) + .write_options(writer, endian, args)?; + v3_start.finish_le32(writer) + } else { + 0x58u8.write_options(writer, endian, args) + } + } +} + +struct SpvFormat { + format: Format, + honor_small: bool, +} + +impl BinWrite for SpvFormat { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + let type_ = if self.format.type_() == Type::F && self.honor_small { + 40 + } else { + self.format.type_().into() + }; + (((type_ as u32) << 16) | ((self.format.w() as u32) << 8) | (self.format.d() as u32)) + .write_options(writer, endian, args) + } +} + +impl BinWrite for Value { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + match &self.inner { + ValueInner::Number { + show, + format, + honor_small, + value, + var_name, + value_label, + } => { + if var_name.is_some() || value_label.is_some() { + 2u8.write_options(writer, endian, args)?; + //write_optional_style(self.styling.as_ref(),writer, endian, args)?; + ( + SpvFormat { + format: *format, + honor_small: *honor_small, + }, + value.unwrap_or(-f64::MAX), + SpvString::optional(var_name), + SpvString::optional(value_label), + Show::as_spv(show), + ) + .write_options(writer, endian, args)?; + } else { + 1u8.write_options(writer, endian, args)?; + //write_optional_style(self.styling.as_ref(),writer, endian, args)?; + value + .unwrap_or(-f64::MAX) + .write_options(writer, endian, args)?; + Show::as_spv(show).write_options(writer, endian, args)?; + } + } + ValueInner::String { + show, + hex, + s, + var_name, + value_label, + } => todo!(), + ValueInner::Variable { + show, + var_name, + variable_label, + } => todo!(), + ValueInner::Text { + user_provided, + local, + c, + id, + } => todo!(), + ValueInner::Template { args, local, id } => todo!(), + ValueInner::Empty => todo!(), + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::output::spv::Heading; + + #[test] + fn serialize() { + let heading = Heading { + command_name: Some("foo".into()), + show: false, + label: super::Label("bar".into()), + children: Vec::new(), + }; + let mut output = Vec::new(); + let mut writer = quick_xml::writer::Writer::new(&mut output); + heading.emit(&mut writer).unwrap(); + drop(writer); + let output = String::from_utf8(output).unwrap(); + println!("{output}"); + } +} -- 2.30.2