Skip to main content

mediagit_compression/
smart_compressor.rs

1// MediaGit - Git for Media Files
2// Copyright (C) 2025 MediaGit Contributors
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published
6// by the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13
14//! Smart compression with per-object-type strategy selection
15//!
16//! Automatically selects optimal compression based on file type and content.
17
18use crate::error::CompressionResult;
19use crate::{BrotliCompressor, CompressionLevel, Compressor, ZlibCompressor, ZstdCompressor};
20use std::fmt;
21use std::path::Path;
22
23/// Object/File type classification for compression strategy selection
24#[allow(missing_docs)]
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum ObjectType {
27    // Already compressed image formats (lossy)
28    Jpeg,
29    Png,
30    Gif,
31    Webp,
32    Avif,
33    Heic,
34
35    // GPU-compressed texture formats (game dev)
36    GpuTexture,
37
38    // Uncompressed/lossless image formats
39    Tiff,
40    Bmp,
41    Raw,
42    Exr,
43    Hdr,
44
45    // Video formats (typically already compressed)
46    Mp4,
47    Mov,
48    Avi,
49    Mkv,
50    Webm,
51    Flv,
52    Wmv,
53    Mpg,
54
55    // Audio formats (compressed)
56    Mp3,
57    Aac,
58    Ogg,
59    Opus,
60
61    // Audio formats (uncompressed/lossless)
62    Flac,
63    Wav,
64    Aiff,
65    Alac,
66
67    // Document formats
68    Pdf,
69    Svg,
70    Eps,
71
72    // Text/Code
73    Text,
74    Json,
75    Xml,
76    Yaml,
77    Toml,
78    Csv,
79
80    // Archives (already compressed)
81    Zip,
82    Tar,
83    Gz,
84    SevenZ,
85    Rar,
86
87    // ML/Data formats (already internally compressed)
88    Parquet,
89
90    // ML data formats (arrays, tensors)
91    MlData,
92
93    // ML model weights (PyTorch, TensorFlow, etc.)
94    MlModel,
95
96    // ML deployment formats (ONNX, TFLite, etc.)
97    MlDeployment,
98
99    // ML training checkpoints (large, frequent)
100    MlCheckpoint,
101
102    // ML inference models (optimized, archived)
103    MlInference,
104
105    // Creative project files - Adobe Creative Cloud
106    AdobePhotoshop,    // .psd, .psb
107    AdobeIllustrator,  // .ai
108    AdobeIndesign,     // .indd, .idml
109    AdobeAfterEffects, // .aep
110    AdobePremiere,     // .prproj
111
112    // Creative project files - Video editing
113    DavinciResolve,    // .drp
114    FinalCutPro,       // .fcpbundle, .fcpxml
115    AvidMediaComposer, // .avb
116
117    // 3D interchange/exchange formats (mesh/scene data)
118    Model3D, // .stl, .obj, .fbx, .glb, .gltf, .ply, .dae, .abc, .3ds, .usd, .usda, .usdc
119
120    // Creative project files - 3D/DCC
121    Blender,    // .blend
122    Maya,       // .ma, .mb
123    ThreeDsMax, // .max
124    Cinema4D,   // .c4d
125    Houdini,    // .hip, .hipnc
126
127    // Creative project files - Audio DAWs
128    ProTools,    // .ptx
129    AbletonLive, // .als
130    FLStudio,    // .flp
131    LogicPro,    // .logic, .logicx
132
133    // Creative project files - CAD
134    AutoCad,  // .dwg, .dxf
135    SketchUp, // .skp
136    Revit,    // .rvt
137
138    // Creative project files - Game engines
139    UnityProject,  // .unity, .prefab, .asset
140    UnrealProject, // .uasset, .umap
141    GodotProject,  // .tscn, .tres
142
143    // Office documents (modern XML-based)
144    WordDocument,           // .docx, .doc
145    ExcelSpreadsheet,       // .xlsx, .xls
146    PowerpointPresentation, // .pptx, .ppt
147    OpenDocument,           // .odt, .ods, .odp
148
149    // Database formats
150    SqliteDatabase, // .sqlite, .db, .db3
151
152    // Compressed text/logs
153    CompressedLog, // .log.gz, .log.bz2
154
155    // Git object types (for interoperability)
156    GitBlob,
157    GitTree,
158    GitCommit,
159
160    // Unknown/binary
161    Unknown,
162}
163
164impl ObjectType {
165    /// Detect object type from file extension
166    pub fn from_extension(ext: &str) -> Self {
167        match ext.to_lowercase().as_str() {
168            // Already compressed images
169            "jpg" | "jpeg" => ObjectType::Jpeg,
170            "png" => ObjectType::Png,
171            "gif" => ObjectType::Gif,
172            "webp" => ObjectType::Webp,
173            "avif" => ObjectType::Avif,
174            "heic" | "heif" => ObjectType::Heic,
175
176            // GPU-compressed textures (game dev)
177            "dds" | "ktx" | "ktx2" | "astc" | "pvr" | "basis" => ObjectType::GpuTexture,
178
179            // Uncompressed images
180            "tif" | "tiff" => ObjectType::Tiff,
181            "bmp" | "dib" => ObjectType::Bmp,
182            "psd" | "psb" => ObjectType::AdobePhotoshop, // Moved to creative projects
183            "raw" | "cr2" | "cr3" | "nef" | "arw" | "dng" | "orf" | "rw2" => ObjectType::Raw,
184            "exr" => ObjectType::Exr,
185            "hdr" | "pic" => ObjectType::Hdr,
186
187            // Video
188            "mp4" | "m4v" => ObjectType::Mp4,
189            "mov" | "qt" => ObjectType::Mov,
190            "avi" => ObjectType::Avi,
191            "mkv" => ObjectType::Mkv,
192            "webm" => ObjectType::Webm,
193            "flv" | "f4v" => ObjectType::Flv,
194            "wmv" | "asf" => ObjectType::Wmv,
195            "mpg" | "mpeg" | "m2v" => ObjectType::Mpg,
196
197            // Audio (compressed)
198            "mp3" => ObjectType::Mp3,
199            "aac" => ObjectType::Aac,
200            "m4a" => ObjectType::Aac, // Default m4a to AAC
201            "ogg" | "oga" => ObjectType::Ogg,
202            "opus" => ObjectType::Opus,
203
204            // Audio (uncompressed/lossless)
205            "flac" => ObjectType::Flac,
206            "wav" => ObjectType::Wav,
207            "aiff" | "aif" | "aifc" => ObjectType::Aiff,
208            "alac" => ObjectType::Alac,
209
210            // Documents
211            "pdf" => ObjectType::Pdf,
212            "svg" | "svgz" => ObjectType::Svg,
213            "eps" => ObjectType::Eps, // "ai" moved to AdobeIllustrator
214
215            // Text/Code
216            "txt" | "md" | "markdown" | "rst" | "adoc" | "rs" | "js" | "ts" | "jsx" | "tsx"
217            | "py" | "go" | "c" | "cpp" | "cc" | "cxx" | "h" | "hpp" | "hh" | "hxx" | "java"
218            | "kt" | "swift" | "rb" | "php" | "sh" | "bash" | "zsh" | "fish" | "vim" | "lua"
219            | "pl" | "r" | "m" => ObjectType::Text,
220            "json" | "json5" | "jsonc" => ObjectType::Json,
221            "xml" | "html" | "xhtml" | "htm" | "xsl" | "xslt" => ObjectType::Xml,
222            "yml" | "yaml" => ObjectType::Yaml,
223            "toml" => ObjectType::Toml,
224            "csv" | "tsv" | "psv" => ObjectType::Csv,
225
226            // Archives
227            "zip" | "zipx" => ObjectType::Zip,
228            "tar" => ObjectType::Tar,
229            "gz" | "gzip" => ObjectType::Gz,
230            "7z" => ObjectType::SevenZ,
231            "rar" => ObjectType::Rar,
232
233            // ML/Data formats (internally compressed)
234            "parquet" | "arrow" | "feather" | "orc" | "avro" => ObjectType::Parquet,
235
236            // ML data formats (arrays, tensors)
237            "hdf5" | "h5" | "nc" | "netcdf" | "npy" | "npz" | "tfrecords" | "petastorm" => {
238                ObjectType::MlData
239            }
240
241            // ML model weights (general)
242            "pb" | "safetensors" | "pkl" | "joblib" => ObjectType::MlModel,
243
244            // ML training checkpoints (large, frequent saves during training)
245            // Note: .pt/.pth/.bin can be either checkpoints or inference models
246            // We default to checkpoint for aggressive compression since they're more common
247            "ckpt" | "pt" | "pth" | "bin" => ObjectType::MlCheckpoint,
248
249            // ML inference models (optimized for deployment)
250            "onnx" | "gguf" | "ggml" | "tflite" | "mlmodel" | "coreml" | "keras" | "pte"
251            | "mleap" | "pmml" | "llamafile" => ObjectType::MlInference,
252
253            // Creative projects - Adobe Creative Cloud
254            "ai" | "ait" => ObjectType::AdobeIllustrator,
255            "indd" | "idml" | "indt" => ObjectType::AdobeIndesign,
256            "aep" | "aet" => ObjectType::AdobeAfterEffects,
257            "prproj" | "psq" => ObjectType::AdobePremiere,
258
259            // Creative projects - Video editing
260            "drp" | "drp_proxies" => ObjectType::DavinciResolve,
261            "fcpbundle" | "fcpxml" | "fcpxmld" => ObjectType::FinalCutPro,
262            "avb" | "avp" | "avs" => ObjectType::AvidMediaComposer,
263
264            // 3D interchange/exchange formats (mesh/scene data)
265            // Note: usdz is a ZIP container → maps to Zip (Store strategy)
266            "stl" | "obj" | "fbx" | "glb" | "gltf" | "ply" | "dae" | "abc" | "3ds" | "usd"
267            | "usda" | "usdc" => ObjectType::Model3D,
268            "usdz" => ObjectType::Zip,
269
270            // Creative projects - 3D/DCC
271            "blend" | "blend1" => ObjectType::Blender,
272            "ma" | "mb" => ObjectType::Maya,
273            "max" => ObjectType::ThreeDsMax,
274            "c4d" => ObjectType::Cinema4D,
275            "hip" | "hipnc" | "hiplc" => ObjectType::Houdini,
276
277            // Creative projects - Audio DAWs
278            "ptx" | "ptf" => ObjectType::ProTools,
279            "als" => ObjectType::AbletonLive,
280            "flp" => ObjectType::FLStudio,
281            "logic" | "logicx" => ObjectType::LogicPro,
282
283            // Creative projects - CAD
284            "dwg" | "dxf" => ObjectType::AutoCad,
285            "skp" => ObjectType::SketchUp,
286            "rvt" | "rfa" | "rte" => ObjectType::Revit,
287
288            // Creative projects - Game engines
289            "unity" | "prefab" | "asset" | "unity3d" => ObjectType::UnityProject,
290            "uasset" | "umap" | "upk" => ObjectType::UnrealProject,
291            "tscn" | "tres" | "godot" => ObjectType::GodotProject,
292
293            // Office documents
294            "docx" | "doc" | "docm" | "dot" | "dotx" => ObjectType::WordDocument,
295            "xlsx" | "xls" | "xlsm" | "xlsb" | "xlt" | "xltx" => ObjectType::ExcelSpreadsheet,
296            "pptx" | "ppt" | "pptm" | "pot" | "potx" => ObjectType::PowerpointPresentation,
297            "odt" | "ods" | "odp" | "odg" | "odf" => ObjectType::OpenDocument,
298
299            // Database formats
300            "sqlite" | "sqlite3" | "db" | "db3" | "s3db" => ObjectType::SqliteDatabase,
301
302            // Special handling for compound extensions (must check before generic extensions)
303            // This is handled in from_path() with better logic
304            _ => ObjectType::Unknown,
305        }
306    }
307
308    /// Detect object type from file path
309    pub fn from_path<P: AsRef<Path>>(path: P) -> Self {
310        path.as_ref()
311            .extension()
312            .and_then(|ext| ext.to_str())
313            .map(Self::from_extension)
314            .unwrap_or(ObjectType::Unknown)
315    }
316
317    /// Detect object type from magic bytes
318    pub fn from_magic_bytes(data: &[u8]) -> Self {
319        if data.len() < 4 {
320            return ObjectType::Unknown;
321        }
322
323        // JPEG: FF D8 FF
324        if data.len() >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
325            return ObjectType::Jpeg;
326        }
327
328        // PNG: 89 50 4E 47
329        if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
330            return ObjectType::Png;
331        }
332
333        // GIF: 47 49 46 38
334        if data.starts_with(b"GIF8") {
335            return ObjectType::Gif;
336        }
337
338        // WEBP: RIFF....WEBP
339        if data.len() >= 12 && data.starts_with(b"RIFF") && &data[8..12] == b"WEBP" {
340            return ObjectType::Webp;
341        }
342
343        // TIFF: 49 49 2A 00 (little-endian) or 4D 4D 00 2A (big-endian)
344        if data.starts_with(&[0x49, 0x49, 0x2A, 0x00])
345            || data.starts_with(&[0x4D, 0x4D, 0x00, 0x2A])
346        {
347            return ObjectType::Tiff;
348        }
349
350        // BMP: 42 4D
351        if data.starts_with(&[0x42, 0x4D]) {
352            return ObjectType::Bmp;
353        }
354
355        // PDF: 25 50 44 46
356        if data.starts_with(b"%PDF") {
357            return ObjectType::Pdf;
358        }
359
360        // MP4: ftyp at offset 4
361        if data.len() >= 12 && &data[4..8] == b"ftyp" {
362            return ObjectType::Mp4;
363        }
364
365        // ZIP: 50 4B 03 04 or 50 4B 05 06
366        if data.starts_with(&[0x50, 0x4B, 0x03, 0x04])
367            || data.starts_with(&[0x50, 0x4B, 0x05, 0x06])
368        {
369            return ObjectType::Zip;
370        }
371
372        // GZIP: 1F 8B
373        if data.starts_with(&[0x1F, 0x8B]) {
374            return ObjectType::Gz;
375        }
376
377        ObjectType::Unknown
378    }
379
380    /// Check if this type is already compressed
381    pub fn is_already_compressed(self) -> bool {
382        matches!(
383            self,
384            ObjectType::Jpeg
385                | ObjectType::Png
386                | ObjectType::Gif
387                | ObjectType::Webp
388                | ObjectType::Avif
389                | ObjectType::Heic
390                | ObjectType::GpuTexture
391                | ObjectType::Mp4
392                | ObjectType::Mov
393                | ObjectType::Avi
394                | ObjectType::Mkv
395                | ObjectType::Webm
396                | ObjectType::Flv
397                | ObjectType::Wmv
398                | ObjectType::Mpg
399                | ObjectType::Mp3
400                | ObjectType::Aac
401                | ObjectType::Ogg
402                | ObjectType::Opus
403                | ObjectType::Pdf
404                | ObjectType::Zip
405                | ObjectType::Gz
406                | ObjectType::SevenZ
407                | ObjectType::Rar
408                | ObjectType::Parquet
409                // PDF-based creative containers with embedded compressed streams
410                | ObjectType::AdobeIllustrator
411                | ObjectType::AdobeIndesign
412                // Office documents are ZIP containers with compressed XML
413                | ObjectType::WordDocument
414                | ObjectType::ExcelSpreadsheet
415                | ObjectType::PowerpointPresentation
416                | ObjectType::OpenDocument
417        )
418    }
419
420    /// Get the category of this object type
421    pub fn category(self) -> ObjectCategory {
422        match self {
423            ObjectType::Jpeg
424            | ObjectType::Png
425            | ObjectType::Gif
426            | ObjectType::Webp
427            | ObjectType::Avif
428            | ObjectType::Heic
429            | ObjectType::GpuTexture
430            | ObjectType::Tiff
431            | ObjectType::Bmp
432            | ObjectType::Raw
433            | ObjectType::Exr
434            | ObjectType::Hdr => ObjectCategory::Image,
435
436            ObjectType::Mp4
437            | ObjectType::Mov
438            | ObjectType::Avi
439            | ObjectType::Mkv
440            | ObjectType::Webm
441            | ObjectType::Flv
442            | ObjectType::Wmv
443            | ObjectType::Mpg => ObjectCategory::Video,
444
445            ObjectType::Mp3
446            | ObjectType::Aac
447            | ObjectType::Ogg
448            | ObjectType::Opus
449            | ObjectType::Flac
450            | ObjectType::Wav
451            | ObjectType::Aiff
452            | ObjectType::Alac => ObjectCategory::Audio,
453
454            ObjectType::Pdf | ObjectType::Svg | ObjectType::Eps => ObjectCategory::Document,
455
456            ObjectType::Text
457            | ObjectType::Json
458            | ObjectType::Xml
459            | ObjectType::Yaml
460            | ObjectType::Toml
461            | ObjectType::Csv => ObjectCategory::Text,
462
463            ObjectType::Zip
464            | ObjectType::Tar
465            | ObjectType::Gz
466            | ObjectType::SevenZ
467            | ObjectType::Rar
468            | ObjectType::CompressedLog => ObjectCategory::Archive,
469
470            ObjectType::Parquet
471            | ObjectType::MlData
472            | ObjectType::MlModel
473            | ObjectType::MlDeployment => ObjectCategory::Archive, // ML formats as data archives
474
475            // ML specialized (training vs inference)
476            ObjectType::MlCheckpoint | ObjectType::MlInference => ObjectCategory::MlSpecialized,
477
478            // 3D interchange/exchange formats
479            ObjectType::Model3D => ObjectCategory::CreativeProject,
480
481            // Creative project files
482            ObjectType::AdobePhotoshop
483            | ObjectType::AdobeIllustrator
484            | ObjectType::AdobeIndesign
485            | ObjectType::AdobeAfterEffects
486            | ObjectType::AdobePremiere
487            | ObjectType::DavinciResolve
488            | ObjectType::FinalCutPro
489            | ObjectType::AvidMediaComposer
490            | ObjectType::Blender
491            | ObjectType::Maya
492            | ObjectType::ThreeDsMax
493            | ObjectType::Cinema4D
494            | ObjectType::Houdini
495            | ObjectType::ProTools
496            | ObjectType::AbletonLive
497            | ObjectType::FLStudio
498            | ObjectType::LogicPro
499            | ObjectType::AutoCad
500            | ObjectType::SketchUp
501            | ObjectType::Revit
502            | ObjectType::UnityProject
503            | ObjectType::UnrealProject
504            | ObjectType::GodotProject => ObjectCategory::CreativeProject,
505
506            // Office documents
507            ObjectType::WordDocument
508            | ObjectType::ExcelSpreadsheet
509            | ObjectType::PowerpointPresentation
510            | ObjectType::OpenDocument => ObjectCategory::Office,
511
512            // Database
513            ObjectType::SqliteDatabase => ObjectCategory::Database,
514
515            ObjectType::GitBlob | ObjectType::GitTree | ObjectType::GitCommit => {
516                ObjectCategory::GitObject
517            }
518
519            ObjectType::Unknown => ObjectCategory::Unknown,
520        }
521    }
522}
523
524/// Object category for high-level classification
525#[allow(missing_docs)]
526#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
527pub enum ObjectCategory {
528    Image,
529    Video,
530    Audio,
531    Document,
532    Text,
533    Archive,
534    CreativeProject, // Adobe, video NLEs, DAWs, 3D/DCC, CAD, game engines
535    Office,          // Word, Excel, PowerPoint, OpenDocument
536    MlSpecialized,   // ML training checkpoints vs inference models
537    Database,        // SQLite, database files
538    GitObject,
539    Unknown,
540}
541
542/// Size threshold for switching from Brotli to Zstd for text files
543/// At 500MB+, Brotli level 9 becomes too slow; Zstd provides 10x faster compression
544/// with only ~20% compression ratio loss
545const LARGE_TEXT_THRESHOLD: usize = 500 * 1024 * 1024; // 500 MB
546
547/// Compression strategy selection
548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
549pub enum CompressionStrategy {
550    /// No compression (store as-is)
551    Store,
552
553    /// Zlib compression (Git-compatible)
554    Zlib(CompressionLevel),
555
556    /// Zstd compression (fast, good ratio)
557    Zstd(CompressionLevel),
558
559    /// Brotli compression (best ratio, slower)
560    Brotli(CompressionLevel),
561
562    /// Delta compression (for similar files)
563    Delta,
564}
565
566impl CompressionStrategy {
567    /// Select optimal strategy for object type
568    pub fn for_object_type(obj_type: ObjectType) -> Self {
569        match obj_type {
570            // Already compressed images: store without recompression
571            ObjectType::Jpeg
572            | ObjectType::Png
573            | ObjectType::Gif
574            | ObjectType::Webp
575            | ObjectType::Avif
576            | ObjectType::Heic
577            | ObjectType::GpuTexture => CompressionStrategy::Store,
578
579            // Uncompressed images: Zstd best compression
580            ObjectType::Tiff
581            | ObjectType::Bmp
582            | ObjectType::Raw
583            | ObjectType::Exr
584            | ObjectType::Hdr => CompressionStrategy::Zstd(CompressionLevel::Best),
585
586            // Already compressed video: store without recompression
587            ObjectType::Mp4
588            | ObjectType::Mov
589            | ObjectType::Avi
590            | ObjectType::Mkv
591            | ObjectType::Webm
592            | ObjectType::Flv
593            | ObjectType::Wmv
594            | ObjectType::Mpg => CompressionStrategy::Store,
595
596            // Already compressed audio: store without recompression
597            ObjectType::Mp3 | ObjectType::Aac | ObjectType::Ogg | ObjectType::Opus => {
598                CompressionStrategy::Store
599            }
600
601            // Uncompressed/lossless audio: Zstd best
602            ObjectType::Flac | ObjectType::Wav | ObjectType::Aiff | ObjectType::Alac => {
603                CompressionStrategy::Zstd(CompressionLevel::Best)
604            }
605
606            // Documents: Zstd default
607            ObjectType::Pdf | ObjectType::Svg | ObjectType::Eps => {
608                CompressionStrategy::Zstd(CompressionLevel::Default)
609            }
610
611            // Text/Code: Brotli for maximum compression on structured data
612            // CHANGED: Switched from Zstd to Brotli for 15-30% better compression ratios
613            // Brotli excels at text/structured data with dictionary-based compression
614            ObjectType::Text
615            | ObjectType::Json
616            | ObjectType::Xml
617            | ObjectType::Yaml
618            | ObjectType::Toml
619            | ObjectType::Csv => CompressionStrategy::Brotli(CompressionLevel::Default),
620
621            // Already compressed archives: store
622            ObjectType::Zip
623            | ObjectType::Gz
624            | ObjectType::SevenZ
625            | ObjectType::Rar
626            | ObjectType::Parquet
627            | ObjectType::CompressedLog => CompressionStrategy::Store,
628
629            // TAR is uncompressed container
630            ObjectType::Tar => CompressionStrategy::Zstd(CompressionLevel::Default),
631
632            // ML data formats: Zstd fast (good for large numeric arrays)
633            ObjectType::MlData | ObjectType::MlModel => {
634                CompressionStrategy::Zstd(CompressionLevel::Fast)
635            }
636
637            // ML training checkpoints: Zstd fast (huge files, created frequently)
638            ObjectType::MlCheckpoint => CompressionStrategy::Zstd(CompressionLevel::Fast),
639
640            // ML inference models: Zstd default (better compression for archival)
641            ObjectType::MlInference | ObjectType::MlDeployment => {
642                CompressionStrategy::Zstd(CompressionLevel::Default)
643            }
644
645            // 3D interchange formats: Zstd best (mesh/geometry data compresses well)
646            // STL/OBJ/PLY: raw float triangles → 60-70% compression typical
647            // GLB/FBX/DAE: binary mesh with metadata → 30-50% compression typical
648            ObjectType::Model3D => CompressionStrategy::Zstd(CompressionLevel::Best),
649
650            // PDF-based creative containers: store without recompression
651            // AI/InDesign files are PDF containers with embedded compressed streams
652            // Zstd compression expands the data on every chunk, wasting CPU
653            ObjectType::AdobeIllustrator | ObjectType::AdobeIndesign => CompressionStrategy::Store,
654
655            // Creative project files: Zstd default with heavy delta compression
656            // These files have internal structure and benefit from both compression + delta
657            ObjectType::AdobePhotoshop
658            | ObjectType::AdobeAfterEffects
659            | ObjectType::AdobePremiere
660            | ObjectType::DavinciResolve
661            | ObjectType::FinalCutPro
662            | ObjectType::AvidMediaComposer
663            | ObjectType::Blender
664            | ObjectType::Maya
665            | ObjectType::ThreeDsMax
666            | ObjectType::Cinema4D
667            | ObjectType::Houdini
668            | ObjectType::ProTools
669            | ObjectType::AbletonLive
670            | ObjectType::FLStudio
671            | ObjectType::LogicPro
672            | ObjectType::AutoCad
673            | ObjectType::SketchUp
674            | ObjectType::Revit
675            | ObjectType::UnityProject
676            | ObjectType::UnrealProject
677            | ObjectType::GodotProject => CompressionStrategy::Zstd(CompressionLevel::Default),
678
679            // Office documents: store without recompression (ZIP containers with compressed XML)
680            ObjectType::WordDocument
681            | ObjectType::ExcelSpreadsheet
682            | ObjectType::PowerpointPresentation
683            | ObjectType::OpenDocument => CompressionStrategy::Store,
684
685            // Database: Zstd default
686            ObjectType::SqliteDatabase => CompressionStrategy::Zstd(CompressionLevel::Default),
687
688            // Git objects: Zlib for compatibility
689            ObjectType::GitBlob | ObjectType::GitTree | ObjectType::GitCommit => {
690                CompressionStrategy::Zlib(CompressionLevel::Default)
691            }
692
693            // Unknown/binary: Zstd default (safe choice)
694            ObjectType::Unknown => CompressionStrategy::Zstd(CompressionLevel::Default),
695        }
696    }
697
698    /// Select optimal strategy for object type with size consideration
699    ///
700    /// For large text files (>500MB), switches from Brotli to Zstd for 10x faster compression
701    /// with only ~20% compression ratio loss.
702    pub fn for_object_type_with_size(obj_type: ObjectType, data_size: usize) -> Self {
703        // Check if this is a text type that would normally use Brotli
704        let base_strategy = Self::for_object_type(obj_type);
705
706        // For large text files, switch from Brotli to Zstd for faster compression
707        if data_size >= LARGE_TEXT_THRESHOLD {
708            if let CompressionStrategy::Brotli(_) = base_strategy {
709                // Use Zstd Default for large text files (10x faster, ~20% worse ratio)
710                return CompressionStrategy::Zstd(CompressionLevel::Default);
711            }
712        }
713
714        base_strategy
715    }
716}
717
718/// Type-aware compressor trait
719pub trait TypeAwareCompressor: Send + Sync {
720    /// Compress with automatic strategy selection
721    fn compress_typed(&self, data: &[u8], obj_type: ObjectType) -> CompressionResult<Vec<u8>>;
722
723    /// Compress with automatic strategy selection considering data size
724    /// For large text files (>500MB), uses Zstd instead of Brotli for faster compression
725    fn compress_typed_with_size(
726        &self,
727        data: &[u8],
728        obj_type: ObjectType,
729    ) -> CompressionResult<Vec<u8>>;
730
731    /// Decompress data (auto-detects algorithm)
732    fn decompress_typed(&self, data: &[u8]) -> CompressionResult<Vec<u8>>;
733
734    /// Get compression strategy for object type
735    fn strategy_for_type(&self, obj_type: ObjectType) -> CompressionStrategy;
736
737    /// Get compression strategy for object type with size consideration
738    fn strategy_for_type_with_size(
739        &self,
740        obj_type: ObjectType,
741        data_size: usize,
742    ) -> CompressionStrategy;
743}
744
745/// Smart compressor with automatic type-based strategy selection
746#[derive(Clone)]
747pub struct SmartCompressor {
748    zlib: ZlibCompressor,
749    zstd_fast: ZstdCompressor,
750    zstd_default: ZstdCompressor,
751    zstd_best: ZstdCompressor,
752    brotli_best: BrotliCompressor,
753}
754
755impl SmartCompressor {
756    /// Create new smart compressor with all algorithms ready
757    pub fn new() -> Self {
758        Self {
759            zlib: ZlibCompressor::new(CompressionLevel::Default),
760            zstd_fast: ZstdCompressor::new(CompressionLevel::Fast),
761            zstd_default: ZstdCompressor::new(CompressionLevel::Default),
762            zstd_best: ZstdCompressor::new(CompressionLevel::Best),
763            brotli_best: BrotliCompressor::new(CompressionLevel::Best),
764        }
765    }
766
767    /// Compress with explicit strategy
768    ///
769    /// If compression would EXPAND the data (common for already-compressed content
770    /// like embedded JPEGs in AI/PSD files), automatically falls back to Store mode.
771    fn compress_with_strategy(
772        &self,
773        data: &[u8],
774        strategy: CompressionStrategy,
775    ) -> CompressionResult<Vec<u8>> {
776        // Store mode: prefix with 0x00 magic byte
777        if matches!(strategy, CompressionStrategy::Store) {
778            let mut result = Vec::with_capacity(data.len() + 1);
779            result.push(0x00); // Store magic byte
780            result.extend_from_slice(data);
781            return Ok(result);
782        }
783
784        let compressed = match strategy {
785            CompressionStrategy::Store => unreachable!(), // Handled above
786
787            CompressionStrategy::Zlib(level) => {
788                let compressor = ZlibCompressor::new(level);
789                compressor.compress(data)?
790            }
791
792            CompressionStrategy::Zstd(level) => {
793                let compressor = match level {
794                    CompressionLevel::Fast => &self.zstd_fast,
795                    CompressionLevel::Default => &self.zstd_default,
796                    CompressionLevel::Best => &self.zstd_best,
797                };
798                compressor.compress(data)?
799            }
800
801            CompressionStrategy::Brotli(level) => {
802                let compressor = BrotliCompressor::new(level);
803                compressor.compress(data)?
804            }
805
806            CompressionStrategy::Delta => {
807                // Delta compression requires a base - not implemented in simple compress
808                // Fall back to Zstd
809                self.zstd_default.compress(data)?
810            }
811        };
812
813        // CRITICAL FIX: If compression expanded the data (happens with already-compressed
814        // content like embedded JPEGs in AI/PSD files), fall back to Store mode.
815        // This prevents significant size overhead on creative files.
816        if compressed.len() >= data.len() {
817            tracing::debug!(
818                original_size = data.len(),
819                compressed_size = compressed.len(),
820                "Compression expanded data, falling back to Store mode"
821            );
822            let mut result = Vec::with_capacity(data.len() + 1);
823            result.push(0x00); // Store magic byte
824            result.extend_from_slice(data);
825            return Ok(result);
826        }
827
828        Ok(compressed)
829    }
830}
831
832impl Default for SmartCompressor {
833    fn default() -> Self {
834        Self::new()
835    }
836}
837
838impl fmt::Debug for SmartCompressor {
839    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
840        f.debug_struct("SmartCompressor")
841            .field("strategies", &"Zlib|Zstd|Brotli|Delta")
842            .finish()
843    }
844}
845
846impl TypeAwareCompressor for SmartCompressor {
847    fn compress_typed(&self, data: &[u8], obj_type: ObjectType) -> CompressionResult<Vec<u8>> {
848        let strategy = self.strategy_for_type(obj_type);
849        self.compress_with_strategy(data, strategy)
850    }
851
852    fn compress_typed_with_size(
853        &self,
854        data: &[u8],
855        obj_type: ObjectType,
856    ) -> CompressionResult<Vec<u8>> {
857        let strategy = self.strategy_for_type_with_size(obj_type, data.len());
858        self.compress_with_strategy(data, strategy)
859    }
860
861    fn decompress_typed(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
862        // Auto-detect compression algorithm
863        use crate::CompressionAlgorithm;
864
865        // Check for Store mode magic byte (0x00 prefix added by compress_with_strategy fallback)
866        // This handles data that couldn't be compressed efficiently (already-compressed content).
867        if !data.is_empty() && data[0] == 0x00 {
868            // Check if this looks like Store mode (no compression magic after the prefix)
869            let remaining = &data[1..];
870            let algo = CompressionAlgorithm::detect(remaining);
871            if algo == CompressionAlgorithm::None {
872                // Strip the Store prefix and return raw data
873                return Ok(remaining.to_vec());
874            }
875        }
876
877        let algo = CompressionAlgorithm::detect(data);
878
879        match algo {
880            CompressionAlgorithm::None => Ok(data.to_vec()),
881            CompressionAlgorithm::Zlib => {
882                // False positive possible: raw data starting with 0x78 + valid checksum byte
883                // can be misdetected as zlib. Fall back to raw data if decompression fails.
884                Ok(self.zlib.decompress(data).unwrap_or_else(|_| data.to_vec()))
885            }
886            CompressionAlgorithm::Zstd => {
887                // False positive rare but possible: raw data starting with zstd magic bytes
888                // (0x28 0xB5 0x2F 0xFD). Fall back to raw data if decompression fails.
889                Ok(self
890                    .zstd_default
891                    .decompress(data)
892                    .unwrap_or_else(|_| data.to_vec()))
893            }
894            CompressionAlgorithm::Brotli => {
895                // False positive rare but possible: raw data starting with "BRT\x01".
896                // Fall back to raw data if decompression fails.
897                Ok(self
898                    .brotli_best
899                    .decompress(data)
900                    .unwrap_or_else(|_| data.to_vec()))
901            }
902        }
903    }
904
905    fn strategy_for_type(&self, obj_type: ObjectType) -> CompressionStrategy {
906        CompressionStrategy::for_object_type(obj_type)
907    }
908
909    fn strategy_for_type_with_size(
910        &self,
911        obj_type: ObjectType,
912        data_size: usize,
913    ) -> CompressionStrategy {
914        CompressionStrategy::for_object_type_with_size(obj_type, data_size)
915    }
916}
917
918impl Compressor for SmartCompressor {
919    fn compress(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
920        // Default to Zstd when no type information available
921        self.zstd_default.compress(data)
922    }
923
924    fn decompress(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
925        self.decompress_typed(data)
926    }
927}
928
929#[cfg(test)]
930#[allow(clippy::unwrap_used)]
931mod tests {
932    use super::*;
933
934    #[test]
935    fn test_object_type_from_extension() {
936        assert_eq!(ObjectType::from_extension("jpg"), ObjectType::Jpeg);
937        assert_eq!(ObjectType::from_extension("JPEG"), ObjectType::Jpeg);
938        assert_eq!(ObjectType::from_extension("png"), ObjectType::Png);
939        assert_eq!(ObjectType::from_extension("tiff"), ObjectType::Tiff);
940        assert_eq!(ObjectType::from_extension("mp4"), ObjectType::Mp4);
941        assert_eq!(ObjectType::from_extension("pdf"), ObjectType::Pdf);
942        assert_eq!(ObjectType::from_extension("txt"), ObjectType::Text);
943        assert_eq!(ObjectType::from_extension("rs"), ObjectType::Text);
944        assert_eq!(ObjectType::from_extension("json"), ObjectType::Json);
945        assert_eq!(ObjectType::from_extension("unknown"), ObjectType::Unknown);
946    }
947
948    #[test]
949    fn test_object_type_from_path() {
950        assert_eq!(ObjectType::from_path("image.jpg"), ObjectType::Jpeg);
951        assert_eq!(ObjectType::from_path("/path/to/file.png"), ObjectType::Png);
952        assert_eq!(ObjectType::from_path("document.PDF"), ObjectType::Pdf);
953        assert_eq!(ObjectType::from_path("code.rs"), ObjectType::Text);
954        assert_eq!(ObjectType::from_path("noextension"), ObjectType::Unknown);
955    }
956
957    #[test]
958    fn test_object_type_from_magic_bytes() {
959        // JPEG
960        let jpeg_data = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10];
961        assert_eq!(ObjectType::from_magic_bytes(&jpeg_data), ObjectType::Jpeg);
962
963        // PNG
964        let png_data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
965        assert_eq!(ObjectType::from_magic_bytes(&png_data), ObjectType::Png);
966
967        // GIF
968        let gif_data = b"GIF89a";
969        assert_eq!(ObjectType::from_magic_bytes(gif_data), ObjectType::Gif);
970
971        // PDF
972        let pdf_data = b"%PDF-1.4";
973        assert_eq!(ObjectType::from_magic_bytes(pdf_data), ObjectType::Pdf);
974
975        // Unknown
976        let unknown_data = b"random";
977        assert_eq!(
978            ObjectType::from_magic_bytes(unknown_data),
979            ObjectType::Unknown
980        );
981    }
982
983    #[test]
984    fn test_is_already_compressed() {
985        assert!(ObjectType::Jpeg.is_already_compressed());
986        assert!(ObjectType::Png.is_already_compressed());
987        assert!(ObjectType::Mp4.is_already_compressed());
988        assert!(ObjectType::Zip.is_already_compressed());
989        // PDF-based creative containers
990        assert!(ObjectType::AdobeIllustrator.is_already_compressed());
991        assert!(ObjectType::AdobeIndesign.is_already_compressed());
992        // Office ZIP containers
993        assert!(ObjectType::WordDocument.is_already_compressed());
994        assert!(ObjectType::ExcelSpreadsheet.is_already_compressed());
995
996        assert!(!ObjectType::Tiff.is_already_compressed());
997        assert!(!ObjectType::Bmp.is_already_compressed());
998        assert!(!ObjectType::Text.is_already_compressed());
999        assert!(!ObjectType::Raw.is_already_compressed());
1000        // PSD is NOT already compressed (uncompressed layer data)
1001        assert!(!ObjectType::AdobePhotoshop.is_already_compressed());
1002    }
1003
1004    #[test]
1005    fn test_compression_strategy_selection() {
1006        // Already compressed → Store
1007        assert_eq!(
1008            CompressionStrategy::for_object_type(ObjectType::Jpeg),
1009            CompressionStrategy::Store
1010        );
1011        assert_eq!(
1012            CompressionStrategy::for_object_type(ObjectType::Mp4),
1013            CompressionStrategy::Store
1014        );
1015
1016        // Uncompressed images → Zstd Best
1017        assert_eq!(
1018            CompressionStrategy::for_object_type(ObjectType::Tiff),
1019            CompressionStrategy::Zstd(CompressionLevel::Best)
1020        );
1021        assert_eq!(
1022            CompressionStrategy::for_object_type(ObjectType::Raw),
1023            CompressionStrategy::Zstd(CompressionLevel::Best)
1024        );
1025
1026        // Text → Brotli Default (15-30% better compression for structured data)
1027        assert_eq!(
1028            CompressionStrategy::for_object_type(ObjectType::Text),
1029            CompressionStrategy::Brotli(CompressionLevel::Default)
1030        );
1031        assert_eq!(
1032            CompressionStrategy::for_object_type(ObjectType::Json),
1033            CompressionStrategy::Brotli(CompressionLevel::Default)
1034        );
1035
1036        // Documents → Zstd Default
1037        assert_eq!(
1038            CompressionStrategy::for_object_type(ObjectType::Pdf),
1039            CompressionStrategy::Zstd(CompressionLevel::Default)
1040        );
1041
1042        // PDF-based creative containers → Store (already compressed internally)
1043        assert_eq!(
1044            CompressionStrategy::for_object_type(ObjectType::AdobeIllustrator),
1045            CompressionStrategy::Store
1046        );
1047        assert_eq!(
1048            CompressionStrategy::for_object_type(ObjectType::AdobeIndesign),
1049            CompressionStrategy::Store
1050        );
1051
1052        // Office ZIP containers → Store (already compressed internally)
1053        assert_eq!(
1054            CompressionStrategy::for_object_type(ObjectType::WordDocument),
1055            CompressionStrategy::Store
1056        );
1057
1058        // PSD still gets Zstd (uncompressed layer data benefits from compression)
1059        assert_eq!(
1060            CompressionStrategy::for_object_type(ObjectType::AdobePhotoshop),
1061            CompressionStrategy::Zstd(CompressionLevel::Default)
1062        );
1063    }
1064
1065    #[test]
1066    fn test_smart_compressor_jpeg_no_compression() {
1067        let compressor = SmartCompressor::new();
1068        let jpeg_data = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46];
1069
1070        let compressed = compressor
1071            .compress_typed(&jpeg_data, ObjectType::Jpeg)
1072            .unwrap();
1073
1074        // Should store with 0x00 prefix (Store mode magic byte)
1075        assert_eq!(compressed.len(), jpeg_data.len() + 1);
1076        assert_eq!(compressed[0], 0x00);
1077        assert_eq!(&compressed[1..], &jpeg_data[..]);
1078    }
1079
1080    #[test]
1081    fn test_smart_compressor_text_compression() {
1082        let compressor = SmartCompressor::new();
1083        let text_data = b"Hello, World! ".repeat(100);
1084
1085        let compressed = compressor
1086            .compress_typed(&text_data, ObjectType::Text)
1087            .unwrap();
1088
1089        // Text should compress well
1090        assert!(compressed.len() < text_data.len());
1091
1092        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1093        assert_eq!(decompressed, text_data);
1094    }
1095
1096    #[test]
1097    fn test_smart_compressor_unknown_type() {
1098        let compressor = SmartCompressor::new();
1099        let data = b"Some binary data...".repeat(50);
1100
1101        let compressed = compressor
1102            .compress_typed(&data, ObjectType::Unknown)
1103            .unwrap();
1104
1105        // Unknown should use Zstd default
1106        assert!(compressed.len() < data.len());
1107
1108        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1109        assert_eq!(decompressed, data);
1110    }
1111
1112    #[test]
1113    fn test_smart_compressor_fallback() {
1114        let compressor = SmartCompressor::new();
1115        let data = b"Test data";
1116
1117        // compress() without type should use default (Zstd)
1118        let compressed = compressor.compress(data).unwrap();
1119        let decompressed = compressor.decompress(&compressed).unwrap();
1120
1121        assert_eq!(decompressed, data);
1122    }
1123
1124    #[test]
1125    fn test_compression_strategy_for_all_types() {
1126        // Verify strategy exists for all types
1127        let all_types = [
1128            // Images
1129            ObjectType::Jpeg,
1130            ObjectType::Png,
1131            ObjectType::Gif,
1132            ObjectType::Webp,
1133            ObjectType::Avif,
1134            ObjectType::Heic,
1135            ObjectType::Tiff,
1136            ObjectType::Bmp,
1137            ObjectType::Raw,
1138            ObjectType::Exr,
1139            ObjectType::Hdr,
1140            // Video
1141            ObjectType::Mp4,
1142            ObjectType::Mov,
1143            ObjectType::Avi,
1144            ObjectType::Mkv,
1145            ObjectType::Webm,
1146            ObjectType::Flv,
1147            ObjectType::Wmv,
1148            ObjectType::Mpg,
1149            // Audio
1150            ObjectType::Mp3,
1151            ObjectType::Aac,
1152            ObjectType::Ogg,
1153            ObjectType::Opus,
1154            ObjectType::Flac,
1155            ObjectType::Wav,
1156            ObjectType::Aiff,
1157            ObjectType::Alac,
1158            // Documents
1159            ObjectType::Pdf,
1160            ObjectType::Svg,
1161            ObjectType::Eps,
1162            // Text
1163            ObjectType::Text,
1164            ObjectType::Json,
1165            ObjectType::Xml,
1166            ObjectType::Yaml,
1167            ObjectType::Toml,
1168            ObjectType::Csv,
1169            // Archives
1170            ObjectType::Zip,
1171            ObjectType::Tar,
1172            ObjectType::Gz,
1173            ObjectType::SevenZ,
1174            ObjectType::Rar,
1175            // Creative projects (sample)
1176            ObjectType::AdobePhotoshop,
1177            ObjectType::Blender,
1178            // Office (sample)
1179            ObjectType::WordDocument,
1180            // ML specialized (sample)
1181            ObjectType::MlCheckpoint,
1182            ObjectType::MlInference,
1183            // Git
1184            ObjectType::GitBlob,
1185            ObjectType::GitTree,
1186            ObjectType::GitCommit,
1187            // Unknown
1188            ObjectType::Unknown,
1189        ];
1190
1191        for obj_type in &all_types {
1192            let strategy = CompressionStrategy::for_object_type(*obj_type);
1193            // Just verify it doesn't panic
1194            assert!(matches!(
1195                strategy,
1196                CompressionStrategy::Store
1197                    | CompressionStrategy::Zlib(_)
1198                    | CompressionStrategy::Zstd(_)
1199                    | CompressionStrategy::Brotli(_)
1200                    | CompressionStrategy::Delta
1201            ));
1202        }
1203    }
1204
1205    #[test]
1206    fn test_object_category() {
1207        assert_eq!(ObjectType::Jpeg.category(), ObjectCategory::Image);
1208        assert_eq!(ObjectType::Tiff.category(), ObjectCategory::Image);
1209        assert_eq!(ObjectType::Mp4.category(), ObjectCategory::Video);
1210        assert_eq!(ObjectType::Mp3.category(), ObjectCategory::Audio);
1211        assert_eq!(ObjectType::Wav.category(), ObjectCategory::Audio);
1212        assert_eq!(ObjectType::Pdf.category(), ObjectCategory::Document);
1213        assert_eq!(ObjectType::Text.category(), ObjectCategory::Text);
1214        assert_eq!(ObjectType::Json.category(), ObjectCategory::Text);
1215        assert_eq!(ObjectType::Zip.category(), ObjectCategory::Archive);
1216        assert_eq!(ObjectType::GitBlob.category(), ObjectCategory::GitObject);
1217        assert_eq!(ObjectType::Unknown.category(), ObjectCategory::Unknown);
1218    }
1219
1220    #[test]
1221    fn test_new_file_extensions() {
1222        assert_eq!(ObjectType::from_extension("avif"), ObjectType::Avif);
1223        assert_eq!(ObjectType::from_extension("heic"), ObjectType::Heic);
1224        assert_eq!(ObjectType::from_extension("exr"), ObjectType::Exr);
1225        assert_eq!(ObjectType::from_extension("hdr"), ObjectType::Hdr);
1226        assert_eq!(ObjectType::from_extension("flv"), ObjectType::Flv);
1227        assert_eq!(ObjectType::from_extension("wmv"), ObjectType::Wmv);
1228        assert_eq!(ObjectType::from_extension("opus"), ObjectType::Opus);
1229        assert_eq!(ObjectType::from_extension("aiff"), ObjectType::Aiff);
1230        assert_eq!(ObjectType::from_extension("toml"), ObjectType::Toml);
1231        assert_eq!(ObjectType::from_extension("csv"), ObjectType::Csv);
1232        assert_eq!(ObjectType::from_extension("7z"), ObjectType::SevenZ);
1233    }
1234
1235    #[test]
1236    fn test_smart_compressor_multiple_types() {
1237        let compressor = SmartCompressor::new();
1238
1239        // Test different types with same content
1240        let content = b"Test content ".repeat(100);
1241
1242        let jpeg_result = compressor
1243            .compress_typed(&content, ObjectType::Jpeg)
1244            .unwrap();
1245        let text_result = compressor
1246            .compress_typed(&content, ObjectType::Text)
1247            .unwrap();
1248        let tiff_result = compressor
1249            .compress_typed(&content, ObjectType::Tiff)
1250            .unwrap();
1251
1252        // JPEG should not compress (store with 0x00 prefix)
1253        assert_eq!(jpeg_result.len(), content.len() + 1);
1254
1255        // Text and TIFF should compress (different algorithms)
1256        assert!(text_result.len() < content.len());
1257        assert!(tiff_result.len() < content.len());
1258
1259        // All should decompress correctly
1260        assert_eq!(compressor.decompress_typed(&jpeg_result).unwrap(), content);
1261        assert_eq!(compressor.decompress_typed(&text_result).unwrap(), content);
1262        assert_eq!(compressor.decompress_typed(&tiff_result).unwrap(), content);
1263    }
1264
1265    #[test]
1266    fn test_smart_compressor_empty_data() {
1267        let compressor = SmartCompressor::new();
1268        let empty: &[u8] = b"";
1269
1270        let compressed = compressor.compress_typed(empty, ObjectType::Text).unwrap();
1271        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1272
1273        assert_eq!(decompressed, empty);
1274    }
1275
1276    #[test]
1277    fn test_debug_format() {
1278        let compressor = SmartCompressor::new();
1279        let debug_str = format!("{:?}", compressor);
1280        assert!(debug_str.contains("SmartCompressor"));
1281    }
1282
1283    // ============================================================================
1284    // NEW TESTS FOR ENHANCED COMPRESSION STRATEGY
1285    // ============================================================================
1286
1287    #[test]
1288    fn test_creative_project_file_extensions() {
1289        // Adobe Creative Cloud
1290        assert_eq!(
1291            ObjectType::from_extension("psd"),
1292            ObjectType::AdobePhotoshop
1293        );
1294        assert_eq!(
1295            ObjectType::from_extension("psb"),
1296            ObjectType::AdobePhotoshop
1297        );
1298        assert_eq!(
1299            ObjectType::from_extension("ai"),
1300            ObjectType::AdobeIllustrator
1301        );
1302        assert_eq!(
1303            ObjectType::from_extension("indd"),
1304            ObjectType::AdobeIndesign
1305        );
1306        assert_eq!(
1307            ObjectType::from_extension("aep"),
1308            ObjectType::AdobeAfterEffects
1309        );
1310        assert_eq!(
1311            ObjectType::from_extension("prproj"),
1312            ObjectType::AdobePremiere
1313        );
1314
1315        // Video NLEs
1316        assert_eq!(
1317            ObjectType::from_extension("drp"),
1318            ObjectType::DavinciResolve
1319        );
1320        assert_eq!(
1321            ObjectType::from_extension("fcpbundle"),
1322            ObjectType::FinalCutPro
1323        );
1324        assert_eq!(
1325            ObjectType::from_extension("avb"),
1326            ObjectType::AvidMediaComposer
1327        );
1328
1329        // 3D/DCC
1330        assert_eq!(ObjectType::from_extension("blend"), ObjectType::Blender);
1331        assert_eq!(ObjectType::from_extension("ma"), ObjectType::Maya);
1332        assert_eq!(ObjectType::from_extension("max"), ObjectType::ThreeDsMax);
1333        assert_eq!(ObjectType::from_extension("c4d"), ObjectType::Cinema4D);
1334        assert_eq!(ObjectType::from_extension("hip"), ObjectType::Houdini);
1335
1336        // Audio DAWs
1337        assert_eq!(ObjectType::from_extension("ptx"), ObjectType::ProTools);
1338        assert_eq!(ObjectType::from_extension("als"), ObjectType::AbletonLive);
1339        assert_eq!(ObjectType::from_extension("flp"), ObjectType::FLStudio);
1340        assert_eq!(ObjectType::from_extension("logic"), ObjectType::LogicPro);
1341
1342        // CAD
1343        assert_eq!(ObjectType::from_extension("dwg"), ObjectType::AutoCad);
1344        assert_eq!(ObjectType::from_extension("skp"), ObjectType::SketchUp);
1345        assert_eq!(ObjectType::from_extension("rvt"), ObjectType::Revit);
1346
1347        // Game engines
1348        assert_eq!(
1349            ObjectType::from_extension("unity"),
1350            ObjectType::UnityProject
1351        );
1352        assert_eq!(
1353            ObjectType::from_extension("uasset"),
1354            ObjectType::UnrealProject
1355        );
1356        assert_eq!(ObjectType::from_extension("tscn"), ObjectType::GodotProject);
1357    }
1358
1359    #[test]
1360    fn test_office_document_extensions() {
1361        assert_eq!(ObjectType::from_extension("docx"), ObjectType::WordDocument);
1362        assert_eq!(ObjectType::from_extension("doc"), ObjectType::WordDocument);
1363        assert_eq!(
1364            ObjectType::from_extension("xlsx"),
1365            ObjectType::ExcelSpreadsheet
1366        );
1367        assert_eq!(
1368            ObjectType::from_extension("xls"),
1369            ObjectType::ExcelSpreadsheet
1370        );
1371        assert_eq!(
1372            ObjectType::from_extension("pptx"),
1373            ObjectType::PowerpointPresentation
1374        );
1375        assert_eq!(
1376            ObjectType::from_extension("ppt"),
1377            ObjectType::PowerpointPresentation
1378        );
1379        assert_eq!(ObjectType::from_extension("odt"), ObjectType::OpenDocument);
1380        assert_eq!(ObjectType::from_extension("ods"), ObjectType::OpenDocument);
1381    }
1382
1383    #[test]
1384    fn test_ml_specialized_extensions() {
1385        // Training checkpoints
1386        assert_eq!(ObjectType::from_extension("ckpt"), ObjectType::MlCheckpoint);
1387        assert_eq!(ObjectType::from_extension("pt"), ObjectType::MlCheckpoint);
1388        assert_eq!(ObjectType::from_extension("pth"), ObjectType::MlCheckpoint);
1389
1390        // Inference models
1391        assert_eq!(ObjectType::from_extension("onnx"), ObjectType::MlInference);
1392        assert_eq!(ObjectType::from_extension("gguf"), ObjectType::MlInference);
1393        assert_eq!(
1394            ObjectType::from_extension("tflite"),
1395            ObjectType::MlInference
1396        );
1397        assert_eq!(
1398            ObjectType::from_extension("llamafile"),
1399            ObjectType::MlInference
1400        );
1401    }
1402
1403    #[test]
1404    fn test_database_extensions() {
1405        assert_eq!(
1406            ObjectType::from_extension("sqlite"),
1407            ObjectType::SqliteDatabase
1408        );
1409        assert_eq!(ObjectType::from_extension("db"), ObjectType::SqliteDatabase);
1410        assert_eq!(
1411            ObjectType::from_extension("db3"),
1412            ObjectType::SqliteDatabase
1413        );
1414    }
1415
1416    #[test]
1417    fn test_creative_project_categories() {
1418        assert_eq!(
1419            ObjectType::AdobePhotoshop.category(),
1420            ObjectCategory::CreativeProject
1421        );
1422        assert_eq!(
1423            ObjectType::Blender.category(),
1424            ObjectCategory::CreativeProject
1425        );
1426        assert_eq!(
1427            ObjectType::DavinciResolve.category(),
1428            ObjectCategory::CreativeProject
1429        );
1430        assert_eq!(
1431            ObjectType::ProTools.category(),
1432            ObjectCategory::CreativeProject
1433        );
1434        assert_eq!(
1435            ObjectType::AutoCad.category(),
1436            ObjectCategory::CreativeProject
1437        );
1438        assert_eq!(
1439            ObjectType::UnityProject.category(),
1440            ObjectCategory::CreativeProject
1441        );
1442    }
1443
1444    #[test]
1445    fn test_office_category() {
1446        assert_eq!(ObjectType::WordDocument.category(), ObjectCategory::Office);
1447        assert_eq!(
1448            ObjectType::ExcelSpreadsheet.category(),
1449            ObjectCategory::Office
1450        );
1451        assert_eq!(
1452            ObjectType::PowerpointPresentation.category(),
1453            ObjectCategory::Office
1454        );
1455        assert_eq!(ObjectType::OpenDocument.category(), ObjectCategory::Office);
1456    }
1457
1458    #[test]
1459    fn test_ml_specialized_category() {
1460        assert_eq!(
1461            ObjectType::MlCheckpoint.category(),
1462            ObjectCategory::MlSpecialized
1463        );
1464        assert_eq!(
1465            ObjectType::MlInference.category(),
1466            ObjectCategory::MlSpecialized
1467        );
1468    }
1469
1470    #[test]
1471    fn test_database_category() {
1472        assert_eq!(
1473            ObjectType::SqliteDatabase.category(),
1474            ObjectCategory::Database
1475        );
1476    }
1477
1478    #[test]
1479    fn test_creative_project_compression_strategy() {
1480        // All creative projects should use Zstd Default
1481        assert_eq!(
1482            CompressionStrategy::for_object_type(ObjectType::AdobePhotoshop),
1483            CompressionStrategy::Zstd(CompressionLevel::Default)
1484        );
1485        assert_eq!(
1486            CompressionStrategy::for_object_type(ObjectType::Blender),
1487            CompressionStrategy::Zstd(CompressionLevel::Default)
1488        );
1489        assert_eq!(
1490            CompressionStrategy::for_object_type(ObjectType::DavinciResolve),
1491            CompressionStrategy::Zstd(CompressionLevel::Default)
1492        );
1493    }
1494
1495    #[test]
1496    fn test_ml_specialized_compression_strategy() {
1497        // Training checkpoints use Fast (for speed with large files)
1498        assert_eq!(
1499            CompressionStrategy::for_object_type(ObjectType::MlCheckpoint),
1500            CompressionStrategy::Zstd(CompressionLevel::Fast)
1501        );
1502        // Inference models use Default (better compression for archival)
1503        assert_eq!(
1504            CompressionStrategy::for_object_type(ObjectType::MlInference),
1505            CompressionStrategy::Zstd(CompressionLevel::Default)
1506        );
1507    }
1508
1509    #[test]
1510    fn test_office_compression_strategy() {
1511        assert_eq!(
1512            CompressionStrategy::for_object_type(ObjectType::WordDocument),
1513            CompressionStrategy::Store
1514        );
1515        assert_eq!(
1516            CompressionStrategy::for_object_type(ObjectType::ExcelSpreadsheet),
1517            CompressionStrategy::Store
1518        );
1519    }
1520
1521    #[test]
1522    fn test_text_uses_brotli() {
1523        // Verify text/structured data now uses Brotli instead of Zstd
1524        assert_eq!(
1525            CompressionStrategy::for_object_type(ObjectType::Text),
1526            CompressionStrategy::Brotli(CompressionLevel::Default)
1527        );
1528        assert_eq!(
1529            CompressionStrategy::for_object_type(ObjectType::Json),
1530            CompressionStrategy::Brotli(CompressionLevel::Default)
1531        );
1532        assert_eq!(
1533            CompressionStrategy::for_object_type(ObjectType::Csv),
1534            CompressionStrategy::Brotli(CompressionLevel::Default)
1535        );
1536        assert_eq!(
1537            CompressionStrategy::for_object_type(ObjectType::Xml),
1538            CompressionStrategy::Brotli(CompressionLevel::Default)
1539        );
1540    }
1541
1542    #[test]
1543    fn test_case_insensitive_extensions() {
1544        // Test uppercase extensions work correctly
1545        assert_eq!(
1546            ObjectType::from_extension("PSD"),
1547            ObjectType::AdobePhotoshop
1548        );
1549        assert_eq!(ObjectType::from_extension("BLEND"), ObjectType::Blender);
1550        assert_eq!(ObjectType::from_extension("ONNX"), ObjectType::MlInference);
1551        assert_eq!(ObjectType::from_extension("DOCX"), ObjectType::WordDocument);
1552    }
1553
1554    #[test]
1555    fn test_psd_no_longer_in_image_uncompressed() {
1556        // PSD is now AdobePhotoshop (creative project), not in uncompressed images
1557        // It should use Zstd Default, not Zstd Best
1558        assert_eq!(
1559            CompressionStrategy::for_object_type(ObjectType::AdobePhotoshop),
1560            CompressionStrategy::Zstd(CompressionLevel::Default)
1561        );
1562
1563        // Compare with actual uncompressed image (should use Best)
1564        assert_eq!(
1565            CompressionStrategy::for_object_type(ObjectType::Tiff),
1566            CompressionStrategy::Zstd(CompressionLevel::Best)
1567        );
1568    }
1569
1570    // ============================================================================
1571    // INTEGRATION TESTS - VERIFY ALL COMPRESSION/DECOMPRESSION FLOWS
1572    // ============================================================================
1573
1574    #[test]
1575    fn test_integration_brotli_text_roundtrip() {
1576        // Test that Brotli compression for text types works end-to-end
1577        let compressor = SmartCompressor::new();
1578
1579        // Test JSON
1580        let json_data =
1581            r#"{"name": "MediaGit", "version": "1.0", "features": ["compression", "delta"]}"#
1582                .repeat(50);
1583        let compressed = compressor
1584            .compress_typed(json_data.as_bytes(), ObjectType::Json)
1585            .unwrap();
1586        assert!(compressed.len() < json_data.len(), "JSON should compress");
1587        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1588        assert_eq!(
1589            json_data.as_bytes(),
1590            &decompressed[..],
1591            "JSON roundtrip failed"
1592        );
1593
1594        // Test CSV
1595        let csv_data = "id,name,value\n1,Alice,100\n2,Bob,200\n".repeat(100);
1596        let compressed = compressor
1597            .compress_typed(csv_data.as_bytes(), ObjectType::Csv)
1598            .unwrap();
1599        assert!(compressed.len() < csv_data.len(), "CSV should compress");
1600        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1601        assert_eq!(
1602            csv_data.as_bytes(),
1603            &decompressed[..],
1604            "CSV roundtrip failed"
1605        );
1606
1607        // Test XML
1608        let xml_data = "<root><item id=\"1\">Value</item></root>".repeat(50);
1609        let compressed = compressor
1610            .compress_typed(xml_data.as_bytes(), ObjectType::Xml)
1611            .unwrap();
1612        assert!(compressed.len() < xml_data.len(), "XML should compress");
1613        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1614        assert_eq!(
1615            xml_data.as_bytes(),
1616            &decompressed[..],
1617            "XML roundtrip failed"
1618        );
1619
1620        // Test plain text
1621        let text_data = "The quick brown fox jumps over the lazy dog. ".repeat(100);
1622        let compressed = compressor
1623            .compress_typed(text_data.as_bytes(), ObjectType::Text)
1624            .unwrap();
1625        assert!(compressed.len() < text_data.len(), "Text should compress");
1626        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1627        assert_eq!(
1628            text_data.as_bytes(),
1629            &decompressed[..],
1630            "Text roundtrip failed"
1631        );
1632    }
1633
1634    #[test]
1635    fn test_integration_creative_project_roundtrip() {
1636        // Test that creative project files use correct compression
1637        let compressor = SmartCompressor::new();
1638
1639        // Simulate PSD file data (binary with some structure)
1640        let psd_data = vec![0x38, 0x42, 0x50, 0x53]; // "8BPS" header
1641        let mut data = psd_data.clone();
1642        data.extend_from_slice(&vec![0xAB; 10000]); // Add some data
1643
1644        let compressed = compressor
1645            .compress_typed(&data, ObjectType::AdobePhotoshop)
1646            .unwrap();
1647        assert!(compressed.len() < data.len(), "PSD should compress");
1648
1649        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1650        assert_eq!(data, decompressed, "PSD roundtrip failed");
1651    }
1652
1653    #[test]
1654    fn test_integration_ml_specialized_roundtrip() {
1655        // Test ML checkpoint (Zstd Fast) vs inference model (Zstd Default)
1656        let compressor = SmartCompressor::new();
1657
1658        // Simulate model weights (numeric data)
1659        let model_data = (0..5000).map(|x| (x % 256) as u8).collect::<Vec<_>>();
1660
1661        // Test checkpoint compression
1662        let checkpoint_compressed = compressor
1663            .compress_typed(&model_data, ObjectType::MlCheckpoint)
1664            .unwrap();
1665        let checkpoint_decompressed = compressor.decompress_typed(&checkpoint_compressed).unwrap();
1666        assert_eq!(
1667            model_data, checkpoint_decompressed,
1668            "Checkpoint roundtrip failed"
1669        );
1670
1671        // Test inference model compression
1672        let inference_compressed = compressor
1673            .compress_typed(&model_data, ObjectType::MlInference)
1674            .unwrap();
1675        let inference_decompressed = compressor.decompress_typed(&inference_compressed).unwrap();
1676        assert_eq!(
1677            model_data, inference_decompressed,
1678            "Inference model roundtrip failed"
1679        );
1680
1681        // Both should work, but inference might compress better (Default vs Fast)
1682        // We just verify both decompress correctly
1683    }
1684
1685    #[test]
1686    fn test_integration_office_document_roundtrip() {
1687        // Test office documents (ZIP containers with XML)
1688        let compressor = SmartCompressor::new();
1689
1690        // Simulate docx structure (ZIP-like)
1691        let docx_data = b"PK\x03\x04...document content...".repeat(100);
1692
1693        let compressed = compressor
1694            .compress_typed(&docx_data, ObjectType::WordDocument)
1695            .unwrap();
1696        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1697        assert_eq!(docx_data, &decompressed[..], "DOCX roundtrip failed");
1698    }
1699
1700    #[test]
1701    fn test_integration_database_roundtrip() {
1702        // Test SQLite database compression
1703        let compressor = SmartCompressor::new();
1704
1705        // Simulate SQLite data
1706        let db_data = b"SQLite format 3\x00...table data...".repeat(100);
1707
1708        let compressed = compressor
1709            .compress_typed(&db_data, ObjectType::SqliteDatabase)
1710            .unwrap();
1711        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1712        assert_eq!(db_data, &decompressed[..], "SQLite roundtrip failed");
1713    }
1714
1715    #[test]
1716    fn test_integration_auto_detection_mixed_types() {
1717        // Test that auto-detection works across all compression types
1718        let compressor = SmartCompressor::new();
1719
1720        let test_data = b"Test data for compression ".repeat(50);
1721
1722        // Compress with different types and verify all decompress correctly
1723        let types = vec![
1724            ObjectType::Text,           // Brotli
1725            ObjectType::Json,           // Brotli
1726            ObjectType::AdobePhotoshop, // Zstd Default
1727            ObjectType::MlCheckpoint,   // Zstd Fast
1728            ObjectType::WordDocument,   // Zstd Default
1729            ObjectType::Tiff,           // Zstd Best
1730        ];
1731
1732        for obj_type in types {
1733            let compressed = compressor.compress_typed(&test_data, obj_type).unwrap();
1734            let decompressed = compressor.decompress_typed(&compressed).unwrap();
1735            assert_eq!(
1736                test_data,
1737                &decompressed[..],
1738                "Auto-detection failed for {:?}",
1739                obj_type
1740            );
1741        }
1742    }
1743
1744    #[test]
1745    fn test_integration_already_compressed_types() {
1746        // Verify that already-compressed types are stored without recompression
1747        let compressor = SmartCompressor::new();
1748
1749        // Simulate compressed formats
1750        let jpeg_data = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46];
1751        let mp4_data = b"....ftypisom....";
1752        let zip_data = vec![0x50, 0x4B, 0x03, 0x04];
1753
1754        // These should be stored with 0x00 Store prefix (not recompressed)
1755        let jpeg_compressed = compressor
1756            .compress_typed(&jpeg_data, ObjectType::Jpeg)
1757            .unwrap();
1758        assert_eq!(jpeg_compressed[0], 0x00, "JPEG should have Store prefix");
1759        assert_eq!(
1760            &jpeg_compressed[1..],
1761            &jpeg_data[..],
1762            "JPEG should not be recompressed"
1763        );
1764
1765        let mp4_compressed = compressor
1766            .compress_typed(mp4_data, ObjectType::Mp4)
1767            .unwrap();
1768        assert_eq!(mp4_compressed[0], 0x00, "MP4 should have Store prefix");
1769        assert_eq!(
1770            &mp4_compressed[1..],
1771            mp4_data,
1772            "MP4 should not be recompressed"
1773        );
1774
1775        let zip_compressed = compressor
1776            .compress_typed(&zip_data, ObjectType::Zip)
1777            .unwrap();
1778        assert_eq!(zip_compressed[0], 0x00, "ZIP should have Store prefix");
1779        assert_eq!(
1780            &zip_compressed[1..],
1781            &zip_data[..],
1782            "ZIP should not be recompressed"
1783        );
1784    }
1785
1786    #[test]
1787    fn test_integration_compression_ratio_expectations() {
1788        // Test that compression ratios meet expectations for different types
1789        let compressor = SmartCompressor::new();
1790
1791        // Highly repetitive text should compress very well with Brotli
1792        let repetitive_text = "AAAAAAAAAA".repeat(1000);
1793        let compressed = compressor
1794            .compress_typed(repetitive_text.as_bytes(), ObjectType::Text)
1795            .unwrap();
1796        let ratio = compressed.len() as f64 / repetitive_text.len() as f64;
1797        assert!(
1798            ratio < 0.1,
1799            "Repetitive text should compress to <10% with Brotli, got {:.2}%",
1800            ratio * 100.0
1801        );
1802
1803        // Verify decompression
1804        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1805        assert_eq!(repetitive_text.as_bytes(), &decompressed[..]);
1806    }
1807
1808    #[test]
1809    fn test_integration_empty_data_all_types() {
1810        // Verify empty data handling for all compression strategies
1811        let compressor = SmartCompressor::new();
1812        let empty: &[u8] = b"";
1813
1814        let types = vec![
1815            ObjectType::Text,           // Brotli
1816            ObjectType::Json,           // Brotli
1817            ObjectType::AdobePhotoshop, // Zstd Default
1818            ObjectType::MlCheckpoint,   // Zstd Fast
1819            ObjectType::Tiff,           // Zstd Best
1820            ObjectType::Jpeg,           // Store
1821        ];
1822
1823        for obj_type in types {
1824            let compressed = compressor.compress_typed(empty, obj_type).unwrap();
1825            let decompressed = compressor.decompress_typed(&compressed).unwrap();
1826            assert_eq!(
1827                empty,
1828                &decompressed[..],
1829                "Empty data failed for {:?}",
1830                obj_type
1831            );
1832        }
1833    }
1834
1835    #[test]
1836    fn test_integration_large_data_performance() {
1837        // Test that large files compress/decompress correctly
1838        let compressor = SmartCompressor::new();
1839
1840        // 1MB of structured data
1841        let large_json = format!(
1842            r#"{{"data": [{}]}}"#,
1843            (0..10000)
1844                .map(|i| format!("{}", i))
1845                .collect::<Vec<_>>()
1846                .join(",")
1847        );
1848
1849        let compressed = compressor
1850            .compress_typed(large_json.as_bytes(), ObjectType::Json)
1851            .unwrap();
1852        assert!(
1853            compressed.len() < large_json.len(),
1854            "Large JSON should compress"
1855        );
1856
1857        let decompressed = compressor.decompress_typed(&compressed).unwrap();
1858        assert_eq!(
1859            large_json.as_bytes(),
1860            &decompressed[..],
1861            "Large JSON roundtrip failed"
1862        );
1863
1864        // Verify significant compression for structured data
1865        let ratio = compressed.len() as f64 / large_json.len() as f64;
1866        assert!(
1867            ratio < 0.5,
1868            "Large JSON should compress to <50%, got {:.2}%",
1869            ratio * 100.0
1870        );
1871    }
1872
1873    #[test]
1874    fn test_integration_all_new_extensions_mapped() {
1875        // Verify all new extensions have valid mappings
1876        let new_extensions = vec![
1877            // Creative projects
1878            ("psd", ObjectType::AdobePhotoshop),
1879            ("ai", ObjectType::AdobeIllustrator),
1880            ("indd", ObjectType::AdobeIndesign),
1881            ("aep", ObjectType::AdobeAfterEffects),
1882            ("prproj", ObjectType::AdobePremiere),
1883            ("drp", ObjectType::DavinciResolve),
1884            ("blend", ObjectType::Blender),
1885            ("ma", ObjectType::Maya),
1886            ("als", ObjectType::AbletonLive),
1887            ("dwg", ObjectType::AutoCad),
1888            ("unity", ObjectType::UnityProject),
1889            // Office
1890            ("docx", ObjectType::WordDocument),
1891            ("xlsx", ObjectType::ExcelSpreadsheet),
1892            ("pptx", ObjectType::PowerpointPresentation),
1893            ("odt", ObjectType::OpenDocument),
1894            // ML
1895            ("ckpt", ObjectType::MlCheckpoint),
1896            ("onnx", ObjectType::MlInference),
1897            // Database
1898            ("sqlite", ObjectType::SqliteDatabase),
1899        ];
1900
1901        for (ext, expected_type) in new_extensions {
1902            let detected_type = ObjectType::from_extension(ext);
1903            assert_eq!(
1904                detected_type, expected_type,
1905                "Extension '{}' should map to {:?}, got {:?}",
1906                ext, expected_type, detected_type
1907            );
1908
1909            // Verify each type has a compression strategy
1910            let strategy = CompressionStrategy::for_object_type(detected_type);
1911            assert!(
1912                matches!(
1913                    strategy,
1914                    CompressionStrategy::Store
1915                        | CompressionStrategy::Zlib(_)
1916                        | CompressionStrategy::Zstd(_)
1917                        | CompressionStrategy::Brotli(_)
1918                        | CompressionStrategy::Delta
1919                ),
1920                "Type {:?} has invalid strategy: {:?}",
1921                detected_type,
1922                strategy
1923            );
1924        }
1925    }
1926
1927    #[test]
1928    fn test_integration_category_coverage() {
1929        // Verify all new categories are properly configured
1930        let category_samples = vec![
1931            (ObjectType::AdobePhotoshop, ObjectCategory::CreativeProject),
1932            (ObjectType::WordDocument, ObjectCategory::Office),
1933            (ObjectType::MlCheckpoint, ObjectCategory::MlSpecialized),
1934            (ObjectType::SqliteDatabase, ObjectCategory::Database),
1935        ];
1936
1937        for (obj_type, expected_category) in category_samples {
1938            let category = obj_type.category();
1939            assert_eq!(
1940                category, expected_category,
1941                "{:?} should be in {:?} category",
1942                obj_type, expected_category
1943            );
1944        }
1945    }
1946}