mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-26 19:56:48 -06:00
Opening an .xlsm and saving it silently stripped xl/vbaProject.bin, turning a
macro-enabled workbook into a macro-free one — data loss for anyone whose work
depends on macros. GridCraft still does not run VBA, but it now carries the
project through unchanged.
- model: `Workbook.vba_project: Option<Vec<u8>>` (base64 in JSON via a new
`b64::opt` helper). `None` = no macros.
- xlsx read: capture the `vbaProject` relationship's bytes instead of warning
and dropping them; warn that macros are kept but not executed.
- xlsx write: when present, emit `xl/vbaProject.bin` byte-identical, add the
`.../relationships/vbaProject` relationship, switch the main part to the
macro-enabled content type (`...sheet.macroEnabled.main+xml`) and add the
`application/vnd.ms-office.vbaProject` override.
- engine: `view.macros` lists the preserved project alongside GridCraft's own
scripts (`{"kind":"vba","executed":false}`), so callers can see it is there.
- ROADMAP: scope now "preserve, don't execute" (was "out of scope").
Tests: round-trip byte-identical blob + correct content types/relationship;
capture from a foreign-produced .xlsm; a macro-free workbook stays clean.
82 lines
2.5 KiB
Rust
82 lines
2.5 KiB
Rust
//! Minimal base64 for serde (image bytes in JSON).
|
|
|
|
use serde::{Deserialize, Deserializer, Serializer};
|
|
|
|
const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
pub fn encode(data: &[u8]) -> String {
|
|
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
|
|
for chunk in data.chunks(3) {
|
|
let b = [chunk.first().copied().unwrap_or(0), chunk.get(1).copied().unwrap_or(0), chunk.get(2).copied().unwrap_or(0)];
|
|
let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
|
|
for i in 0..4 {
|
|
if i <= chunk.len() {
|
|
out.push(A[((n >> (18 - 6 * i)) & 63) as usize] as char);
|
|
} else {
|
|
out.push('=');
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
pub fn decode(s: &str) -> Option<Vec<u8>> {
|
|
let mut out = Vec::with_capacity(s.len() / 4 * 3);
|
|
let mut buf = 0u32;
|
|
let mut bits = 0;
|
|
for c in s.bytes() {
|
|
if c == b'=' || c.is_ascii_whitespace() {
|
|
continue;
|
|
}
|
|
let v = A.iter().position(|&x| x == c)? as u32;
|
|
buf = (buf << 6) | v;
|
|
bits += 6;
|
|
if bits >= 8 {
|
|
bits -= 8;
|
|
out.push((buf >> bits) as u8);
|
|
buf &= (1 << bits) - 1;
|
|
}
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
pub fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Error> {
|
|
s.serialize_str(&encode(v))
|
|
}
|
|
|
|
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
|
|
let s = String::deserialize(d)?;
|
|
decode(&s).ok_or_else(|| serde::de::Error::custom("invalid base64"))
|
|
}
|
|
|
|
/// Base64 for optional byte blobs (VBA projects): `null`/missing stays `None`.
|
|
pub mod opt {
|
|
use serde::{Deserialize, Deserializer, Serializer};
|
|
|
|
pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
|
|
match v {
|
|
Some(data) => s.serialize_str(&super::encode(data)),
|
|
None => s.serialize_none(),
|
|
}
|
|
}
|
|
|
|
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
|
|
let s = Option::<String>::deserialize(d)?;
|
|
match s {
|
|
None => Ok(None),
|
|
Some(s) => super::decode(&s).map(Some).ok_or_else(|| serde::de::Error::custom("invalid base64")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn roundtrip() {
|
|
for data in [&b""[..], b"a", b"ab", b"abc", b"hello world", &[0, 255, 128, 7]] {
|
|
assert_eq!(super::decode(&super::encode(data)).unwrap(), data);
|
|
}
|
|
assert_eq!(super::encode(b"Man"), "TWFu");
|
|
}
|
|
}
|