1use std::collections::{BTreeSet, HashMap};
2use std::fs;
3use std::io::Write;
4use std::path::Path;
5use zip::write::SimpleFileOptions;
6
7use super::archive_edit::{self, ArchiveOp};
8use crate::dtype::TrxScalar;
9use crate::error::{Result, TrxError};
10use crate::header::Header;
11use crate::io::filename::TrxFilename;
12use crate::mmap_backing::vec_to_bytes;
13use crate::trx_file::{DataArray, DataPerGroup, TrxFile};
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub(crate) enum OffsetsDtype {
21 U32,
22 #[allow(dead_code)]
26 U64,
27}
28
29impl OffsetsDtype {
30 pub(crate) fn pick_for(offsets: &[u32]) -> Self {
32 let _ = offsets;
36 OffsetsDtype::U32
37 }
38
39 pub(crate) fn suffix(self) -> &'static str {
41 match self {
42 OffsetsDtype::U32 => "uint32",
43 OffsetsDtype::U64 => "uint64",
44 }
45 }
46
47 pub(crate) fn encode(self, offsets: &[u32]) -> Vec<u8> {
49 match self {
50 OffsetsDtype::U32 => crate::mmap_backing::vec_to_bytes(offsets.to_vec()),
51 OffsetsDtype::U64 => {
52 let widened: Vec<u64> = offsets.iter().map(|&o| o as u64).collect();
53 crate::mmap_backing::vec_to_bytes(widened)
54 }
55 }
56 }
57}
58
59#[derive(Debug, Default)]
60struct TrxArchiveIndex {
61 dps: HashMap<String, String>,
62 dpv: HashMap<String, String>,
63 groups: HashMap<String, String>,
64 dpg: HashMap<String, HashMap<String, String>>,
65}
66
67pub fn load_from_zip<P: TrxScalar>(path: &Path) -> Result<TrxFile<P>> {
73 let file = fs::File::open(path)?;
74 let mut archive = zip::ZipArchive::new(file)?;
75
76 let tempdir = tempfile::TempDir::new()?;
77 let temp_path = tempdir.path().to_path_buf();
78
79 for i in 0..archive.len() {
80 let mut entry = archive.by_index(i)?;
81 let entry_path = temp_path.join(entry.name());
82
83 if entry.is_dir() {
84 fs::create_dir_all(&entry_path)?;
85 } else {
86 if let Some(parent) = entry_path.parent() {
87 fs::create_dir_all(parent)?;
88 }
89 let mut out_file = fs::File::create(&entry_path)?;
90 std::io::copy(&mut entry, &mut out_file)?;
91 }
92 }
93
94 crate::io::directory::load_from_directory(&temp_path, Some(tempdir))
95}
96
97pub fn save_to_zip<P: TrxScalar>(trx: &TrxFile<P>, path: &Path) -> Result<()> {
105 save_to_zip_with(trx, path, zip::CompressionMethod::Stored)
106}
107
108pub fn save_to_zip_with<P: TrxScalar>(
112 trx: &TrxFile<P>,
113 path: &Path,
114 groups_compression: zip::CompressionMethod,
115) -> Result<()> {
116 let offsets_dtype = OffsetsDtype::pick_for(trx.offsets());
117 let file = fs::File::create(path)?;
118 let mut zip = zip::ZipWriter::new(file);
119 let stored = SimpleFileOptions::default()
120 .compression_method(zip::CompressionMethod::Stored)
121 .large_file(true);
122 let groups_opts = SimpleFileOptions::default()
123 .compression_method(groups_compression)
124 .large_file(true);
125
126 let header_json = trx.header().to_json()?;
128 zip.start_file("header.json", stored)?;
129 zip.write_all(header_json.as_bytes())?;
130
131 let pos_filename = format!("positions.3.{}", P::DTYPE.name());
133 zip.start_file(&pos_filename, stored)?;
134 zip.write_all(trx.positions_bytes())?;
135
136 let offsets_filename = format!("offsets.{}", offsets_dtype.suffix());
138 zip.start_file(&offsets_filename, stored)?;
139 let offsets_bytes = offsets_dtype.encode(trx.offsets());
140 zip.write_all(&offsets_bytes)?;
141
142 write_data_map(&mut zip, "dps", trx.dps_arrays(), stored)?;
144 write_data_map(&mut zip, "dpv", trx.dpv_arrays(), stored)?;
145
146 write_data_map(&mut zip, "groups", trx.group_arrays(), groups_opts)?;
148
149 write_dpg_map(&mut zip, "dpg", trx.dpg_arrays(), stored)?;
151
152 zip.finish()?;
153 Ok(())
154}
155
156pub fn append_dps_to_zip(
158 path: &Path,
159 dps: &HashMap<String, DataArray>,
160 compression: zip::CompressionMethod,
161 overwrite: bool,
162) -> Result<()> {
163 let header = read_header_from_zip(path)?;
164 validate_row_count("DPS", dps, header.nb_streamlines as usize)?;
165 let index = build_archive_index(path)?;
166 let mut ops = Vec::new();
167 for (name, arr) in dps {
168 let target = data_entry_path("dps", name, arr);
169 plan_data_write(
170 &index.dps,
171 name,
172 target,
173 arr,
174 overwrite,
175 compression,
176 &mut ops,
177 )?;
178 }
179 archive_edit::apply_archive_ops(path, ops)
180}
181
182pub fn append_dpv_to_zip(
184 path: &Path,
185 dpv: &HashMap<String, DataArray>,
186 compression: zip::CompressionMethod,
187 overwrite: bool,
188) -> Result<()> {
189 let header = read_header_from_zip(path)?;
190 validate_row_count("DPV", dpv, header.nb_vertices as usize)?;
191 let index = build_archive_index(path)?;
192 let mut ops = Vec::new();
193 for (name, arr) in dpv {
194 let target = data_entry_path("dpv", name, arr);
195 plan_data_write(
196 &index.dpv,
197 name,
198 target,
199 arr,
200 overwrite,
201 compression,
202 &mut ops,
203 )?;
204 }
205 archive_edit::apply_archive_ops(path, ops)
206}
207
208pub fn append_groups_to_zip(
210 path: &Path,
211 groups: &HashMap<String, Vec<u32>>,
212 compression: zip::CompressionMethod,
213 overwrite: bool,
214) -> Result<()> {
215 let header = read_header_from_zip(path)?;
216 let index = build_archive_index(path)?;
217 let mut ops = Vec::new();
218 for (name, members) in groups {
219 validate_group_members(name, members, header.nb_streamlines as usize)?;
220 let target = format!("groups/{name}.uint32");
221 let bytes = vec_to_bytes(members.clone());
222 plan_bytes_write(
223 index.groups.get(name),
224 target,
225 bytes,
226 overwrite,
227 compression,
228 &mut ops,
229 )?;
230 }
231 archive_edit::apply_archive_ops(path, ops)
232}
233
234pub fn append_dpg_to_zip(
236 path: &Path,
237 dpg: &DataPerGroup,
238 compression: zip::CompressionMethod,
239 overwrite: bool,
240) -> Result<()> {
241 let index = build_archive_index(path)?;
242 let available_groups: BTreeSet<&str> = index.groups.keys().map(String::as_str).collect();
243 let mut ops = Vec::new();
244 for (group, arrays) in dpg {
245 if !available_groups.contains(group.as_str()) {
246 return Err(TrxError::Argument(format!(
247 "cannot add DPG entries for missing group '{group}'"
248 )));
249 }
250 let existing = index.dpg.get(group);
251 for (name, arr) in arrays {
252 let target = format!("dpg/{group}/{}", filename_for_array(name, arr));
253 let existing_path = existing.and_then(|entries| entries.get(name));
254 plan_bytes_write(
255 existing_path,
256 target,
257 arr.as_bytes().to_vec(),
258 overwrite,
259 compression,
260 &mut ops,
261 )?;
262 }
263 }
264 archive_edit::apply_archive_ops(path, ops)
265}
266
267pub fn delete_dps_from_zip(path: &Path, names: &[&str]) -> Result<()> {
269 let index = build_archive_index(path)?;
270 let mut ops = Vec::new();
271 for name in names {
272 if let Some(entry_path) = index.dps.get(*name) {
273 ops.push(ArchiveOp::Delete {
274 path: entry_path.clone(),
275 });
276 }
277 }
278 archive_edit::apply_archive_ops(path, ops)
279}
280
281pub fn delete_dpv_from_zip(path: &Path, names: &[&str]) -> Result<()> {
283 let index = build_archive_index(path)?;
284 let mut ops = Vec::new();
285 for name in names {
286 if let Some(entry_path) = index.dpv.get(*name) {
287 ops.push(ArchiveOp::Delete {
288 path: entry_path.clone(),
289 });
290 }
291 }
292 archive_edit::apply_archive_ops(path, ops)
293}
294
295pub fn delete_groups_from_zip(path: &Path, names: &[&str]) -> Result<()> {
297 let index = build_archive_index(path)?;
298 let mut ops = Vec::new();
299 for name in names {
300 if let Some(entry_path) = index.groups.get(*name) {
301 ops.push(ArchiveOp::Delete {
302 path: entry_path.clone(),
303 });
304 }
305 ops.push(ArchiveOp::DeletePrefix {
306 prefix: format!("dpg/{name}"),
307 });
308 }
309 archive_edit::apply_archive_ops(path, ops)
310}
311
312pub fn delete_dpg_from_zip(path: &Path, group: &str, names: Option<&[&str]>) -> Result<()> {
317 let index = build_archive_index(path)?;
318 let mut ops = Vec::new();
319 match names {
320 None => ops.push(ArchiveOp::DeletePrefix {
321 prefix: format!("dpg/{group}"),
322 }),
323 Some([]) => ops.push(ArchiveOp::DeletePrefix {
324 prefix: format!("dpg/{group}"),
325 }),
326 Some(names) => {
327 if let Some(entries) = index.dpg.get(group) {
328 for name in names {
329 if let Some(entry_path) = entries.get(*name) {
330 ops.push(ArchiveOp::Delete {
331 path: entry_path.clone(),
332 });
333 }
334 }
335 }
336 }
337 }
338 archive_edit::apply_archive_ops(path, ops)
339}
340
341fn read_header_from_zip(path: &Path) -> Result<Header> {
342 let bytes = archive_edit::read_archive_entry(path, "header.json")?;
343 Ok(serde_json::from_slice(&bytes)?)
344}
345
346fn build_archive_index(path: &Path) -> Result<TrxArchiveIndex> {
347 let entries = archive_edit::archive_entry_names(path)?;
348 let mut index = TrxArchiveIndex::default();
349
350 for entry in entries {
351 if let Some(rest) = entry.strip_prefix("dps/") {
352 index_entry(&mut index.dps, &entry, rest)?;
353 } else if let Some(rest) = entry.strip_prefix("dpv/") {
354 index_entry(&mut index.dpv, &entry, rest)?;
355 } else if let Some(rest) = entry.strip_prefix("groups/") {
356 index_entry(&mut index.groups, &entry, rest)?;
357 } else if let Some(rest) = entry.strip_prefix("dpg/") {
358 if let Some((group, file_name)) = rest.split_once('/') {
359 let parsed = TrxFilename::parse(file_name)?;
360 let group_entries = index.dpg.entry(group.to_string()).or_default();
361 if group_entries.insert(parsed.name, entry.clone()).is_some() {
362 return Err(TrxError::Format(format!(
363 "duplicate DPG entry path for group '{group}'"
364 )));
365 }
366 }
367 }
368 }
369
370 Ok(index)
371}
372
373fn index_entry(
374 index: &mut HashMap<String, String>,
375 full_path: &str,
376 file_name: &str,
377) -> Result<()> {
378 if file_name.ends_with('/') {
379 return Ok(());
380 }
381 let parsed = TrxFilename::parse(file_name)?;
382 if index.insert(parsed.name, full_path.to_string()).is_some() {
383 return Err(TrxError::Format(format!(
384 "duplicate archive entry for '{full_path}'"
385 )));
386 }
387 Ok(())
388}
389
390fn validate_row_count(
391 kind: &str,
392 arrays: &HashMap<String, DataArray>,
393 expected_rows: usize,
394) -> Result<()> {
395 for (name, arr) in arrays {
396 if arr.nrows() != expected_rows {
397 return Err(TrxError::Format(format!(
398 "{kind} '{name}' has {} rows, expected {expected_rows}",
399 arr.nrows()
400 )));
401 }
402 }
403 Ok(())
404}
405
406fn validate_group_members(name: &str, members: &[u32], nb_streamlines: usize) -> Result<()> {
407 for &member in members {
408 if member as usize >= nb_streamlines {
409 return Err(TrxError::Format(format!(
410 "group '{name}' contains streamline index {member}, but NB_STREAMLINES is {nb_streamlines}"
411 )));
412 }
413 }
414 Ok(())
415}
416
417fn data_entry_path(prefix: &str, name: &str, arr: &DataArray) -> String {
418 format!("{prefix}/{}", filename_for_array(name, arr))
419}
420
421fn filename_for_array(name: &str, arr: &DataArray) -> String {
422 TrxFilename {
423 name: name.to_string(),
424 ncols: arr.ncols(),
425 dtype: arr.dtype(),
426 }
427 .to_filename()
428}
429
430fn plan_data_write(
431 existing: &HashMap<String, String>,
432 logical_name: &str,
433 target_path: String,
434 arr: &DataArray,
435 overwrite: bool,
436 compression: zip::CompressionMethod,
437 ops: &mut Vec<ArchiveOp>,
438) -> Result<()> {
439 plan_bytes_write(
440 existing.get(logical_name),
441 target_path,
442 arr.as_bytes().to_vec(),
443 overwrite,
444 compression,
445 ops,
446 )
447}
448
449fn plan_bytes_write(
450 existing_path: Option<&String>,
451 target_path: String,
452 bytes: Vec<u8>,
453 overwrite: bool,
454 compression: zip::CompressionMethod,
455 ops: &mut Vec<ArchiveOp>,
456) -> Result<()> {
457 match existing_path {
458 None => ops.push(ArchiveOp::Add {
459 path: target_path,
460 bytes,
461 compression,
462 }),
463 Some(_) if !overwrite => {}
464 Some(existing) if existing == &target_path => ops.push(ArchiveOp::Replace {
465 path: target_path,
466 bytes,
467 compression,
468 }),
469 Some(existing) => {
470 ops.push(ArchiveOp::Delete {
471 path: existing.clone(),
472 });
473 ops.push(ArchiveOp::Add {
474 path: target_path,
475 bytes,
476 compression,
477 });
478 }
479 }
480 Ok(())
481}
482
483fn write_data_map<W: Write + std::io::Seek>(
484 zip: &mut zip::ZipWriter<W>,
485 prefix: &str,
486 arrays: &HashMap<String, DataArray>,
487 options: SimpleFileOptions,
488) -> Result<()> {
489 for (name, arr) in arrays {
490 let filename = filename_for_array(name, arr);
491 let entry_name = format!("{prefix}/{filename}");
492 zip.start_file(&entry_name, options)?;
493 zip.write_all(arr.as_bytes())?;
494 }
495 Ok(())
496}
497
498fn write_dpg_map<W: Write + std::io::Seek>(
499 zip: &mut zip::ZipWriter<W>,
500 prefix: &str,
501 groups: &DataPerGroup,
502 options: SimpleFileOptions,
503) -> Result<()> {
504 for (group, arrays) in groups {
505 for (name, arr) in arrays {
506 let filename = filename_for_array(name, arr);
507 let entry_name = format!("{prefix}/{group}/{filename}");
508 zip.start_file(&entry_name, options)?;
509 zip.write_all(arr.as_bytes())?;
510 }
511 }
512 Ok(())
513}