Write a packed partition file from individual column files. When `kek` is `Some`, the columns + sparse index payload region is wrapped in a `SEGT` AES-256-GCM envelope. The NDPK footer remains in plaintext to allow footer-only range requests without decryption. The `column_ranges` in the footer record offsets within the *decrypted payload blob, not the on-disk ciphertext, so `read_column` can pa
(
output_path: &Path,
columns: &[(String, Vec<u8>)], // (column_name, compressed_bytes)
sparse_index: Option<&[u8]>,
schema: &[PackedColumnSchema],
meta: &PartitionMeta,
kek: O
| 194 | /// payload blob, not the on-disk ciphertext, so `read_column` can pass them |
| 195 | /// through after decryption. |
| 196 | pub fn write_packed( |
| 197 | output_path: &Path, |
| 198 | columns: &[(String, Vec<u8>)], // (column_name, compressed_bytes) |
| 199 | sparse_index: Option<&[u8]>, |
| 200 | schema: &[PackedColumnSchema], |
| 201 | meta: &PartitionMeta, |
| 202 | kek: Option<&WalEncryptionKey>, |
| 203 | ) -> Result<u64, PackedError> { |
| 204 | let mut payload = Vec::new(); |
| 205 | let mut column_ranges = HashMap::new(); |
| 206 | |
| 207 | // Build the payload (columns + sparse index). |
| 208 | for (name, data) in columns { |
| 209 | let offset = payload.len() as u64; |
| 210 | payload.extend_from_slice(data); |
| 211 | column_ranges.insert(name.clone(), (offset, data.len() as u64)); |
| 212 | } |
| 213 | |
| 214 | let sparse_index_range = sparse_index.map(|idx_data| { |
| 215 | let offset = payload.len() as u64; |
| 216 | payload.extend_from_slice(idx_data); |
| 217 | (offset, idx_data.len() as u64) |
| 218 | }); |
| 219 | |
| 220 | // Optionally encrypt the payload. |
| 221 | let payload_encrypted = kek.is_some(); |
| 222 | let payload_on_disk: Vec<u8> = if let Some(key) = kek { |
| 223 | encrypt_file(key, &payload).map_err(PackedError::from)? |
| 224 | } else { |
| 225 | payload |
| 226 | }; |
| 227 | |
| 228 | // Build footer (plaintext). |
| 229 | let footer = PackedFooter { |
| 230 | column_ranges, |
| 231 | sparse_index_range, |
| 232 | schema: schema.to_vec(), |
| 233 | meta: meta.clone(), |
| 234 | payload_encrypted, |
| 235 | }; |
| 236 | let footer_body = zerompk::to_msgpack_vec(&footer) |
| 237 | .map_err(|e| PackedError::Io(format!("footer encode: {e}")))?; |
| 238 | |
| 239 | // Compose the final file. |
| 240 | let mut buf = Vec::with_capacity(payload_on_disk.len() + footer_body.len() + PACKED_TAIL_SIZE); |
| 241 | buf.extend_from_slice(&payload_on_disk); |
| 242 | buf.extend_from_slice(&footer_body); |
| 243 | |
| 244 | let footer_len = footer_body.len() as u32; |
| 245 | let crc = crc32c::crc32c(&footer_body); |
| 246 | |
| 247 | // Tail: footer_len (4B) | crc (4B) | version (2B) | magic (4B) |
| 248 | buf.extend_from_slice(&footer_len.to_le_bytes()); |
| 249 | buf.extend_from_slice(&crc.to_le_bytes()); |
| 250 | buf.extend_from_slice(&PACKED_VERSION.to_le_bytes()); |
| 251 | buf.extend_from_slice(&PACKED_MAGIC); |
| 252 | |
| 253 | let total_size = buf.len() as u64; |