1use std::collections::{BTreeMap, HashSet};
5
6use serde::Serialize;
7
8use crate::clip::ClipFile;
9use crate::container::{Container, ContainerWriter, FILE_ID_LENGTH};
10use crate::database::{self, select_all, value_as_text};
11use crate::error::Result;
12use crate::offscreen::{Attribute, BlockData, PixelKind, compress};
13use crate::text;
14use crate::vector::VectorData;
15
16#[derive(Debug, Clone, Copy)]
18pub struct CheckOptions {
19 pub decode_pixels: bool,
21 pub reencode_pixels: bool,
23}
24
25impl Default for CheckOptions {
26 fn default() -> Self {
27 CheckOptions { decode_pixels: true, reencode_pixels: true }
28 }
29}
30
31#[derive(Debug, Clone, Default, Serialize)]
33pub struct ContainerReport {
34 pub findings: Vec<String>,
36 pub reassembles: bool,
38 pub writer_reproduces: bool,
40 pub external_chunks: usize,
42 pub external_offsets_match: bool,
44 pub external_ids_text: bool,
47}
48
49#[derive(Debug, Clone, Default, Serialize)]
51pub struct DatabaseReport {
52 pub tables: usize,
54 pub rows: usize,
56 pub unreadable_tables: Vec<String>,
58 pub param_scheme_rows: usize,
60 pub unregistered_columns: Vec<String>,
62}
63
64#[derive(Debug, Clone, Default, Serialize)]
66pub struct LayerReport {
67 pub layers: usize,
69 pub kinds: BTreeMap<String, usize>,
71 pub unreachable: usize,
73 pub tree_error: Option<String>,
75 pub blend_modes: BTreeMap<i64, usize>,
77}
78
79#[derive(Debug, Clone, Default, Serialize)]
81pub struct PixelReport {
82 pub offscreens: usize,
84 pub stored: usize,
86 pub kinds: BTreeMap<String, usize>,
88 pub attribute_errors: usize,
90 pub attribute_round_trip_failures: usize,
92 pub payload_errors: usize,
94 pub payload_round_trip_failures: usize,
96 pub block_size_table_mismatches: usize,
98 pub blocks: usize,
100 pub stored_blocks: usize,
102 pub checksum_mismatches: usize,
104 pub decode_errors: usize,
106 pub reencoded_identical: usize,
108 pub reencoded_identical_default: usize,
110 pub reencoded_different: usize,
112 pub first_error: Option<String>,
114}
115
116#[derive(Debug, Clone, Default, Serialize)]
118pub struct ReferenceReport {
119 pub references: usize,
121 pub not_stored: usize,
123 pub unreferenced_chunks: usize,
125 pub by_column: BTreeMap<String, (usize, usize)>,
127}
128
129#[derive(Debug, Clone, Default, Serialize)]
131pub struct VectorReport {
132 pub payloads: usize,
134 pub objects: usize,
136 pub points: usize,
138 pub header_sizes: BTreeMap<usize, usize>,
140 pub point_sizes: BTreeMap<usize, usize>,
142 pub parse_errors: usize,
144 pub round_trip_failures: usize,
146 pub first_error: Option<String>,
148}
149
150#[derive(Debug, Clone, Default, Serialize)]
152pub struct TextReport {
153 pub layers: usize,
155 pub blocks: usize,
157 pub records: usize,
159 pub record_ids: BTreeMap<u32, usize>,
161 pub parse_errors: usize,
163 pub round_trip_failures: usize,
165 pub first_error: Option<String>,
167}
168
169#[derive(Debug, Clone, Default, Serialize)]
171pub struct EffectReport {
172 pub blobs: usize,
174 pub attached: usize,
176 pub enabled: BTreeMap<String, usize>,
178 pub sections: BTreeMap<String, usize>,
180 pub parse_errors: usize,
182 pub round_trip_failures: usize,
184 pub first_error: Option<String>,
186}
187
188#[derive(Debug, Clone, Default, Serialize)]
190pub struct ClipReport {
191 pub container: ContainerReport,
193 pub database: DatabaseReport,
195 pub layers: LayerReport,
197 pub pixels: PixelReport,
199 pub references: ReferenceReport,
201 pub vectors: VectorReport,
203 pub texts: TextReport,
205 pub effects: EffectReport,
207 pub preview: Option<(u32, u32)>,
209 pub preview_error: Option<String>,
211 pub unchanged_save_identical: bool,
213 pub errors: Vec<String>,
215}
216
217impl ClipReport {
218 pub fn ok(&self) -> bool {
220 self.errors.is_empty()
221 && self.container.findings.is_empty()
222 && self.container.reassembles
223 && self.container.writer_reproduces
224 && self.container.external_offsets_match
225 && self.container.external_ids_text
226 && self.database.unreadable_tables.is_empty()
227 && self.layers.unreachable == 0
228 && self.layers.tree_error.is_none()
229 && self.pixels.attribute_errors == 0
230 && self.pixels.attribute_round_trip_failures == 0
231 && self.pixels.payload_errors == 0
232 && self.pixels.payload_round_trip_failures == 0
233 && self.pixels.block_size_table_mismatches == 0
234 && self.pixels.checksum_mismatches == 0
235 && self.pixels.decode_errors == 0
236 && self.pixels.reencoded_different == 0
237 && self.references.unreferenced_chunks == 0
238 && self.vectors.parse_errors == 0
239 && self.vectors.round_trip_failures == 0
240 && self.texts.parse_errors == 0
241 && self.texts.round_trip_failures == 0
242 && self.effects.parse_errors == 0
243 && self.effects.round_trip_failures == 0
244 && self.preview_error.is_none()
245 && self.unchanged_save_identical
246 }
247}
248
249pub fn check_clip(data: Vec<u8>, options: CheckOptions) -> ClipReport {
251 let mut report = ClipReport::default();
252 let container = match Container::parse(data) {
253 Ok(container) => container,
254 Err(error) => {
255 report.errors.push(format!("container: {error}"));
256 return report;
257 }
258 };
259 check_container(&container, &mut report.container);
260 match container.sqlite().and_then(database::open_bytes) {
263 Ok(connection) => check_database(&connection, &mut report.database),
264 Err(error) => report.errors.push(format!("database: {error}")),
265 }
266 let file = match ClipFile::from_container(container) {
267 Ok(file) => file,
268 Err(error) => {
269 report.errors.push(format!("open: {error}"));
270 return report;
271 }
272 };
273 check_layers(&file, &mut report.layers);
274 if let Err(error) = check_pixels(&file, options, &mut report.pixels) {
275 report.errors.push(format!("pixels: {error}"));
276 }
277 if let Err(error) = check_references(&file, &mut report.references) {
278 report.errors.push(format!("references: {error}"));
279 }
280 if let Err(error) = check_vectors(&file, &mut report.vectors) {
281 report.errors.push(format!("vectors: {error}"));
282 }
283 if let Err(error) = check_texts(&file, &mut report.texts) {
284 report.errors.push(format!("texts: {error}"));
285 }
286 if let Err(error) = check_effects(&file, &mut report.effects) {
287 report.errors.push(format!("effects: {error}"));
288 }
289 match file.preview() {
290 Ok(Some(image)) => report.preview = Some((image.width, image.height)),
291 Ok(None) => {}
292 Err(error) => report.preview_error = Some(error.to_string()),
293 }
294 report.unchanged_save_identical = !file.is_modified();
297 report
298}
299
300fn check_container(container: &Container, report: &mut ContainerReport) {
301 report.findings = container.findings();
302 report.reassembles = container.reassemble() == container.bytes();
303 report.external_chunks = container.external_chunks().count();
304 let file_id: [u8; FILE_ID_LENGTH] =
305 container.head.as_ref().and_then(|head| head.file_id.as_slice().try_into().ok()).unwrap_or([0; FILE_ID_LENGTH]);
306 let mut writer = ContainerWriter::new(file_id);
307 for chunk in container.external_chunks() {
308 writer.push_external(
309 chunk.external_id.clone().unwrap_or_default(),
310 &container.bytes()[chunk.payload_offset..chunk.body.end],
311 );
312 }
313 report.writer_reproduces = match container.sqlite() {
314 Ok(sqlite) => writer.assemble(sqlite).map(|bytes| bytes == container.bytes()).unwrap_or(false),
315 Err(_) => false,
316 };
317 report.external_offsets_match = (|| -> Result<bool> {
319 let connection = database::open_bytes(container.sqlite()?)?;
320 let rows = select_all(&connection, "ExternalChunk")?;
321 let mut matched = 0;
322 report.external_ids_text =
323 rows.iter().all(|row| matches!(row.get("ExternalID"), Some(rusqlite::types::Value::Text(_))));
324 for row in &rows {
325 let id = row.get("ExternalID").and_then(value_as_text).unwrap_or_default();
326 let chunk = container.external_chunk(&id);
327 if chunk.map(|chunk| chunk.header_offset as i64) == Some(row.int("Offset")) {
328 matched += 1;
329 }
330 }
331 Ok(matched == rows.len() && rows.len() == container.external_chunks().count())
332 })()
333 .unwrap_or(false);
334}
335
336fn check_database(connection: &rusqlite::Connection, report: &mut DatabaseReport) {
337 let tables = database::tables(connection).unwrap_or_default();
338 report.tables = tables.len();
339 let schema = crate::schema::Schema::read(connection).unwrap_or_default();
340 report.param_scheme_rows = schema.params.len();
341 let registered: HashSet<(String, String)> =
342 schema.params.iter().map(|param| (param.table.clone(), param.column.clone())).collect();
343 let bookkeeping = ["ExternalTableAndColumnName", "RemovedExternal", "ExternalChunk", "ElemScheme", "ParamScheme"];
344 for table in &tables {
345 match select_all(connection, table) {
346 Ok(rows) => report.rows += rows.len(),
347 Err(_) => report.unreadable_tables.push(table.clone()),
348 }
349 if table == "sqlite_sequence" || bookkeeping.contains(&table.as_str()) {
350 continue;
351 }
352 for (column, _) in database::columns(connection, table).unwrap_or_default() {
353 if column != "_PW_ID" && !registered.contains(&(table.clone(), column.clone())) {
354 report.unregistered_columns.push(format!("{table}.{column}"));
355 }
356 }
357 }
358}
359
360fn check_layers(file: &ClipFile, report: &mut LayerReport) {
361 report.layers = file.layers.layers.len();
362 report.unreachable = file.layers.unreachable.len();
363 for layer in &file.layers.layers {
364 *report.kinds.entry(layer.kind.name().to_owned()).or_default() += 1;
365 *report.blend_modes.entry(layer.blend).or_default() += 1;
366 }
367}
368
369fn check_pixels(file: &ClipFile, options: CheckOptions, report: &mut PixelReport) -> Result<()> {
370 let rows = select_all(file.database(), "Offscreen")?;
371 report.offscreens = rows.len();
372 let note = |report: &mut PixelReport, message: String| {
373 if report.first_error.is_none() {
374 report.first_error = Some(message);
375 }
376 };
377 for row in &rows {
378 let main_id = row.int("MainId");
379 let Some(raw_attribute) = row.blob("Attribute") else {
380 report.attribute_errors += 1;
381 continue;
382 };
383 let attribute = match Attribute::parse(raw_attribute) {
384 Ok(attribute) => attribute,
385 Err(error) => {
386 report.attribute_errors += 1;
387 note(report, format!("offscreen {main_id}: {error}"));
388 continue;
389 }
390 };
391 if attribute.to_bytes() != raw_attribute {
392 report.attribute_round_trip_failures += 1;
393 }
394 let parameter = match attribute.parameter() {
395 Ok(parameter) => parameter,
396 Err(error) => {
397 report.attribute_errors += 1;
398 note(report, format!("offscreen {main_id}: {error}"));
399 continue;
400 }
401 };
402 let kind = match parameter.kind() {
403 PixelKind::Color => "color",
404 PixelKind::Gray => "gray",
405 PixelKind::Monochrome => "monochrome",
406 PixelKind::Alpha => "alpha",
407 PixelKind::Unknown => "unknown",
408 };
409 *report.kinds.entry(kind.to_owned()).or_default() += 1;
410 report.blocks += parameter.block_count();
411 let Some(payload) = row.get("BlockData").and_then(value_as_text).and_then(|id| file.external(&id)) else {
412 continue;
413 };
414 report.stored += 1;
415 let data = match BlockData::parse(payload) {
416 Ok(data) => data,
417 Err(error) => {
418 report.payload_errors += 1;
419 note(report, format!("offscreen {main_id} blocks: {error}"));
420 continue;
421 }
422 };
423 if data.to_bytes() != payload {
424 report.payload_round_trip_failures += 1;
425 }
426 if data.blocks.len() != parameter.block_count() {
427 report.payload_errors += 1;
428 note(
429 report,
430 format!("offscreen {main_id}: {} blocks for a grid of {}", data.blocks.len(), parameter.block_count()),
431 );
432 }
433 if attribute.block_sizes() != Some(data.record_sizes().as_slice()) {
434 report.block_size_table_mismatches += 1;
435 }
436 report.checksum_mismatches += data.checksum_mismatches().len();
437 let expected = parameter.block_bytes();
438 for block in &data.blocks {
439 if block.zlib.is_none() {
440 continue;
441 }
442 report.stored_blocks += 1;
443 if !options.decode_pixels {
444 continue;
445 }
446 match block.inflate() {
447 Ok(Some(raw)) => {
448 if raw.len() != expected {
449 report.decode_errors += 1;
450 note(
451 report,
452 format!(
453 "offscreen {main_id} block {}: {} bytes, layout needs {expected}",
454 block.index,
455 raw.len()
456 ),
457 );
458 }
459 if options.reencode_pixels {
460 if block.zlib.as_deref() == Some(compress(&raw).as_slice()) {
461 report.reencoded_identical += 1;
462 } else if block.zlib.as_deref() == crate::zlib::compress_default(&raw).ok().as_deref() {
463 report.reencoded_identical_default += 1;
464 } else {
465 report.reencoded_different += 1;
466 note(report, format!("offscreen {main_id} block {}: re-encoding differs", block.index));
467 }
468 }
469 }
470 Ok(None) => {}
471 Err(error) => {
472 report.decode_errors += 1;
473 note(report, format!("offscreen {main_id}: {error}"));
474 }
475 }
476 }
477 }
478 Ok(())
479}
480
481fn check_references(file: &ClipFile, report: &mut ReferenceReport) -> Result<()> {
482 let references = file.external_references()?;
483 let mut referenced = HashSet::new();
484 for (table, column, id) in &references {
485 let stored = file.external(id).is_some();
486 let entry = report.by_column.entry(format!("{table}.{column}")).or_default();
487 entry.0 += 1;
488 if stored {
489 entry.1 += 1;
490 } else {
491 report.not_stored += 1;
492 }
493 referenced.insert(id.clone());
494 }
495 report.references = references.len();
496 report.unreferenced_chunks = file.external_ids().filter(|id| !referenced.contains(*id)).count();
497 Ok(())
498}
499
500fn check_vectors(file: &ClipFile, report: &mut VectorReport) -> Result<()> {
501 if !database::table_exists(file.database(), "VectorObjectList")? {
502 return Ok(());
503 }
504 for row in select_all(file.database(), "VectorObjectList")? {
505 let Some(id) = row.get("VectorData").and_then(value_as_text) else { continue };
506 let Some(payload) = file.external(&id) else { continue };
507 report.payloads += 1;
508 match VectorData::parse(payload) {
509 Ok(data) => {
510 for object in &data.objects {
511 report.objects += 1;
512 report.points += object.points.len();
513 *report.header_sizes.entry(object.header_size()).or_default() += 1;
514 *report.point_sizes.entry(object.point_size).or_default() += 1;
515 }
516 if data.to_bytes() != payload {
517 report.round_trip_failures += 1;
518 report.first_error.get_or_insert_with(|| format!("{id}: round trip differs"));
519 }
520 }
521 Err(error) => {
522 report.parse_errors += 1;
523 report.first_error.get_or_insert_with(|| format!("{id}: {error}"));
524 }
525 }
526 }
527 Ok(())
528}
529
530fn check_effects(file: &ClipFile, report: &mut EffectReport) -> Result<()> {
531 if !database::has_column(file.database(), "Layer", "LayerEffectInfo")? {
532 return Ok(());
533 }
534 for layer in &file.layers.layers {
535 let Some(blob) = layer.row.blob("LayerEffectInfo") else { continue };
536 report.blobs += 1;
537 match crate::effect::LayerEffects::parse(blob) {
538 Ok(effects) => {
539 for (name, _) in &effects.sections {
540 *report.sections.entry(name.clone()).or_default() += 1;
541 }
542 if layer.row.truthy("LayerEffectAttached") {
543 report.attached += 1;
544 for name in effects.enabled() {
545 *report.enabled.entry(name.to_owned()).or_default() += 1;
546 }
547 }
548 if effects.to_bytes() != blob {
549 report.round_trip_failures += 1;
550 }
551 }
552 Err(error) => {
553 report.parse_errors += 1;
554 report.first_error.get_or_insert_with(|| format!("layer {}: {error}", layer.main_id));
555 }
556 }
557 }
558 Ok(())
559}
560
561fn check_texts(file: &ClipFile, report: &mut TextReport) -> Result<()> {
562 if !database::has_column(file.database(), "Layer", "TextLayerAttributes")? {
563 return Ok(());
564 }
565 for row in select_all(file.database(), "Layer")? {
566 let mut blobs: Vec<&[u8]> = Vec::new();
567 let mut arrays_ok = true;
568 if let Some(blob) = row.blob("TextLayerAttributes") {
569 blobs.push(blob);
570 }
571 if let Some(array) = row.blob("TextLayerAttributesArray") {
572 match text::split_array(array) {
573 Ok(parts) => {
574 arrays_ok = text::join_array(&parts) == array;
575 blobs.extend(parts);
576 }
577 Err(error) => {
578 report.parse_errors += 1;
579 report.first_error.get_or_insert_with(|| format!("layer {}: {error}", row.int("MainId")));
580 }
581 }
582 }
583 if let Some(array) = row.blob("TextLayerStringArray") {
584 match text::split_array(array) {
585 Ok(parts) => arrays_ok &= text::join_array(&parts) == array,
586 Err(error) => {
587 report.parse_errors += 1;
588 report.first_error.get_or_insert_with(|| format!("layer {}: {error}", row.int("MainId")));
589 }
590 }
591 }
592 if blobs.is_empty() {
593 continue;
594 }
595 report.layers += 1;
596 if !arrays_ok {
597 report.round_trip_failures += 1;
598 }
599 for blob in blobs {
600 report.blocks += 1;
601 match text::Attributes::parse(blob) {
602 Ok(attributes) => {
603 report.records += attributes.records.len();
604 for record in &attributes.records {
605 *report.record_ids.entry(record.id).or_default() += 1;
606 }
607 if attributes.to_bytes() != blob {
608 report.round_trip_failures += 1;
609 }
610 }
611 Err(error) => {
612 report.parse_errors += 1;
613 report.first_error.get_or_insert_with(|| format!("layer {}: {error}", row.int("MainId")));
614 }
615 }
616 }
617 }
618 Ok(())
619}
620
621#[derive(Debug, Clone, Default, Serialize)]
623pub struct CmcReport {
624 pub nodes: usize,
626 pub pages: usize,
628 pub spreads: usize,
630 pub findings: Vec<String>,
632 pub unchanged_save_identical: bool,
634 pub edit_round_trip: bool,
637 pub errors: Vec<String>,
639}
640
641impl CmcReport {
642 pub fn ok(&self) -> bool {
644 self.errors.is_empty() && self.findings.is_empty() && self.unchanged_save_identical && self.edit_round_trip
645 }
646}
647
648pub fn check_cmc(data: Vec<u8>, folder: Option<&std::path::Path>) -> CmcReport {
650 let mut report = CmcReport::default();
651 let original = data.clone();
652 let mut file = match crate::cmc::CmcFile::from_bytes(data) {
653 Ok(file) => file,
654 Err(error) => {
655 report.errors.push(format!("open: {error}"));
656 return report;
657 }
658 };
659 report.nodes = file.nodes.len();
660 report.findings = file.findings(folder);
661 report.unchanged_save_identical = file.to_bytes().is_ok_and(|bytes| bytes == original);
662 let order: Vec<i64> = match file.pages() {
663 Ok(pages) => {
664 report.pages = pages.len();
665 report.spreads = pages.iter().filter(|node| node.spread).count();
666 pages.iter().map(|node| node.main_id).collect()
667 }
668 Err(error) => {
669 report.errors.push(format!("pages: {error}"));
670 return report;
671 }
672 };
673 let edited = file.reorder(&order).and_then(|()| file.to_bytes()).and_then(crate::cmc::CmcFile::from_bytes);
674 report.edit_round_trip = match edited {
675 Ok(again) => {
676 again.findings(None).is_empty()
677 && again.pages().is_ok_and(|pages| pages.iter().map(|node| node.main_id).eq(order.iter().copied()))
678 }
679 Err(error) => {
680 report.errors.push(format!("edit: {error}"));
681 false
682 }
683 };
684 report
685}