Skip to main content

exif_oxide/raw/
offset.rs

1//! RAW offset processing infrastructure
2//!
3//! This module provides offset calculation systems for different RAW format types.
4//! Some manufacturers like Panasonic use entry-based offsets where the offset
5//! to data is stored in IFD entry values rather than fixed positions.
6//!
7//! ExifTool Reference: Various RAW modules use different offset strategies
8
9use crate::types::{ExifError, Result};
10use std::collections::HashMap;
11
12/// Entry-based offset processor for manufacturers like Panasonic
13/// ExifTool: PanasonicRaw.pm uses IFD entry values as offsets to actual data
14/// This is different from fixed-offset processors used by simpler formats
15pub struct EntryBasedOffsetProcessor {
16    /// Map of tag ID to offset extraction rules
17    /// ExifTool: Different tags have different offset calculation methods
18    offset_rules: HashMap<u16, OffsetExtractionRule>,
19}
20
21/// Rule for extracting offset from an IFD entry
22/// ExifTool: Different tags store offsets in different ways
23#[derive(Debug, Clone)]
24pub struct OffsetExtractionRule {
25    /// Tag ID this rule applies to
26    /// ExifTool: Tag number in IFD
27    pub tag_id: u16,
28
29    /// How to extract the offset from the IFD entry
30    /// ExifTool: Can be from value offset field or actual value
31    pub offset_field: OffsetField,
32
33    /// Base offset for calculations
34    /// ExifTool: Different manufacturers use different base offsets
35    pub base: OffsetBase,
36
37    /// Optional additional offset to add
38    /// ExifTool: Some formats require additional offset adjustments
39    pub additional_offset: i32,
40}
41
42/// How to extract offset from IFD entry
43/// ExifTool: Different extraction methods for different manufacturers
44#[derive(Debug, Clone, Copy)]
45pub enum OffsetField {
46    /// Use the value offset field from IFD entry
47    /// ExifTool: Standard TIFF offset where value_offset points to data
48    ValueOffset,
49
50    /// Use the actual value as offset
51    /// ExifTool: The value itself contains the offset (Panasonic pattern)
52    ActualValue,
53}
54
55/// Base offset for offset calculations
56/// ExifTool: Different manufacturers use different reference points
57#[derive(Debug, Clone, Copy)]
58pub enum OffsetBase {
59    /// Offset from start of file
60    /// ExifTool: Most common base
61    FileStart,
62
63    /// Offset from start of IFD
64    /// ExifTool: Some formats use IFD-relative offsets
65    IfdStart,
66
67    /// Offset from start of maker note data
68    /// ExifTool: Common for maker note subdirectories
69    MakerNoteStart,
70
71    /// Offset from current data position
72    /// ExifTool: Relative to current read position
73    DataPosition,
74}
75
76/// IFD entry structure (simplified)
77/// ExifTool: Standard TIFF IFD entry format
78#[derive(Debug, Clone)]
79pub struct IfdEntry {
80    /// Tag ID
81    /// ExifTool: 2-byte tag identifier
82    pub tag: u16,
83
84    /// Data type
85    /// ExifTool: 2-byte type identifier (BYTE, LONG, etc.)
86    pub data_type: u16,
87
88    /// Number of values
89    /// ExifTool: 4-byte count of values
90    pub count: u32,
91
92    /// Value or offset to value
93    /// ExifTool: 4-byte value or offset depending on size
94    pub value_offset: u32,
95
96    /// Actual file position of this entry
97    /// Used for relative calculations
98    pub entry_position: u64,
99}
100
101impl EntryBasedOffsetProcessor {
102    /// Create new entry-based offset processor
103    /// ExifTool: Initialize with manufacturer-specific rules
104    pub fn new(offset_rules: HashMap<u16, OffsetExtractionRule>) -> Self {
105        Self { offset_rules }
106    }
107
108    /// Get offset extraction rule for a tag
109    /// ExifTool: Look up how to process this tag's offset
110    pub fn get_rule(&self, tag_id: u16) -> Option<&OffsetExtractionRule> {
111        self.offset_rules.get(&tag_id)
112    }
113
114    /// Calculate actual offset from IFD entry and rule
115    /// ExifTool: Apply manufacturer-specific offset calculation
116    pub fn calculate_offset(
117        &self,
118        entry: &IfdEntry,
119        rule: &OffsetExtractionRule,
120        context: &OffsetContext,
121    ) -> Result<u64> {
122        // Extract raw offset based on field type
123        // ExifTool: Different extraction methods for different tags
124        let raw_offset = match rule.offset_field {
125            OffsetField::ValueOffset => entry.value_offset as i64,
126            OffsetField::ActualValue => {
127                // For actual value, we need to read the value if it's stored elsewhere
128                // For now, assume it's directly in value_offset field
129                entry.value_offset as i64
130            }
131        };
132
133        // Apply base offset calculation
134        // ExifTool: Different base calculations for different manufacturers
135        let base_offset = match rule.base {
136            OffsetBase::FileStart => 0i64,
137            OffsetBase::IfdStart => context.ifd_start as i64,
138            OffsetBase::MakerNoteStart => context.maker_note_start as i64,
139            OffsetBase::DataPosition => context.current_position as i64,
140        };
141
142        // Calculate final offset
143        // ExifTool: Base + raw offset + additional adjustment
144        let final_offset = base_offset + raw_offset + rule.additional_offset as i64;
145
146        if final_offset < 0 {
147            return Err(ExifError::ParseError(format!(
148                "Calculated negative offset for tag 0x{:04x}: {}",
149                entry.tag, final_offset
150            )));
151        }
152
153        Ok(final_offset as u64)
154    }
155
156    /// Add new offset extraction rule
157    /// ExifTool: Extend processor with additional rules
158    pub fn add_rule(&mut self, rule: OffsetExtractionRule) {
159        self.offset_rules.insert(rule.tag_id, rule);
160    }
161
162    /// Remove offset extraction rule
163    /// ExifTool: Remove rule for specific tag
164    pub fn remove_rule(&mut self, tag_id: u16) -> Option<OffsetExtractionRule> {
165        self.offset_rules.remove(&tag_id)
166    }
167
168    /// Get all configured tag IDs
169    /// ExifTool: List all tags that have offset rules
170    pub fn get_configured_tags(&self) -> Vec<u16> {
171        self.offset_rules.keys().copied().collect()
172    }
173}
174
175/// Context for offset calculations
176/// ExifTool: Provides reference points for relative offset calculations
177#[derive(Debug, Clone)]
178pub struct OffsetContext {
179    /// Start of current IFD in file
180    /// ExifTool: IFD base position
181    pub ifd_start: u64,
182
183    /// Start of maker note data in file
184    /// ExifTool: Maker note base position
185    pub maker_note_start: u64,
186
187    /// Current read position in file
188    /// ExifTool: Current file position
189    pub current_position: u64,
190
191    /// Total file size (for bounds checking)
192    /// ExifTool: File size for validation
193    pub file_size: u64,
194}
195
196impl OffsetContext {
197    /// Create new offset context
198    pub fn new(
199        ifd_start: u64,
200        maker_note_start: u64,
201        current_position: u64,
202        file_size: u64,
203    ) -> Self {
204        Self {
205            ifd_start,
206            maker_note_start,
207            current_position,
208            file_size,
209        }
210    }
211
212    /// Validate that offset is within file bounds
213    /// ExifTool: Bounds checking for calculated offsets
214    pub fn validate_offset(&self, offset: u64, data_size: u64) -> Result<()> {
215        if offset >= self.file_size {
216            return Err(ExifError::ParseError(format!(
217                "Offset {} exceeds file size {}",
218                offset, self.file_size
219            )));
220        }
221
222        if offset + data_size > self.file_size {
223            return Err(ExifError::ParseError(format!(
224                "Data at offset {offset} with size {data_size} exceeds file bounds"
225            )));
226        }
227
228        Ok(())
229    }
230}
231
232/// Simple offset processor for fixed-offset formats like Kyocera/Minolta
233/// ExifTool: Simple formats use fixed offsets from known base positions
234pub struct SimpleOffsetProcessor {
235    /// Base offset for all calculations
236    /// ExifTool: Fixed base position (usually start of data block)
237    base_offset: u64,
238}
239
240impl SimpleOffsetProcessor {
241    /// Create new simple offset processor
242    /// ExifTool: Initialize with base offset
243    pub fn new(base_offset: u64) -> Self {
244        Self { base_offset }
245    }
246
247    /// Calculate offset by adding relative offset to base
248    /// ExifTool: Simple addition for fixed-offset formats
249    pub fn calculate_offset(&self, relative_offset: u64) -> u64 {
250        self.base_offset + relative_offset
251    }
252
253    /// Update base offset
254    /// ExifTool: Change base position if needed
255    pub fn set_base_offset(&mut self, base_offset: u64) {
256        self.base_offset = base_offset;
257    }
258
259    /// Get current base offset
260    /// ExifTool: Get current base position
261    pub fn get_base_offset(&self) -> u64 {
262        self.base_offset
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn test_entry_based_offset_processor() {
272        // Create test rule for Panasonic tag 0x002e
273        let rule = OffsetExtractionRule {
274            tag_id: 0x002e,
275            offset_field: OffsetField::ActualValue,
276            base: OffsetBase::MakerNoteStart,
277            additional_offset: 0,
278        };
279
280        let mut rules = HashMap::new();
281        rules.insert(0x002e, rule);
282
283        let processor = EntryBasedOffsetProcessor::new(rules);
284
285        // Test rule retrieval
286        assert!(processor.get_rule(0x002e).is_some());
287        assert!(processor.get_rule(0x0000).is_none());
288
289        // Test configured tags
290        let tags = processor.get_configured_tags();
291        assert_eq!(tags.len(), 1);
292        assert!(tags.contains(&0x002e));
293    }
294
295    #[test]
296    fn test_offset_calculation() {
297        let rule = OffsetExtractionRule {
298            tag_id: 0x002e,
299            offset_field: OffsetField::ActualValue,
300            base: OffsetBase::MakerNoteStart,
301            additional_offset: 10,
302        };
303
304        let mut rules = HashMap::new();
305        rules.insert(0x002e, rule);
306
307        let processor = EntryBasedOffsetProcessor::new(rules);
308
309        let entry = IfdEntry {
310            tag: 0x002e,
311            data_type: 4, // LONG
312            count: 1,
313            value_offset: 0x1000,
314            entry_position: 0x500,
315        };
316
317        let context = OffsetContext::new(0x400, 0x800, 0x500, 0x10000);
318
319        let offset = processor
320            .calculate_offset(&entry, processor.get_rule(0x002e).unwrap(), &context)
321            .unwrap();
322
323        // Expected: maker_note_start (0x800) + value_offset (0x1000) + additional (10) = 0x180A
324        assert_eq!(offset, 0x180A);
325    }
326
327    #[test]
328    fn test_offset_context_validation() {
329        let context = OffsetContext::new(0x400, 0x800, 0x500, 0x10000);
330
331        // Valid offset and size
332        assert!(context.validate_offset(0x1000, 0x100).is_ok());
333
334        // Offset exceeds file size
335        assert!(context.validate_offset(0x20000, 0x100).is_err());
336
337        // Data extends beyond file
338        assert!(context.validate_offset(0xFF00, 0x200).is_err());
339    }
340
341    #[test]
342    fn test_simple_offset_processor() {
343        let mut processor = SimpleOffsetProcessor::new(0x1000);
344
345        assert_eq!(processor.calculate_offset(0x100), 0x1100);
346        assert_eq!(processor.get_base_offset(), 0x1000);
347
348        processor.set_base_offset(0x2000);
349        assert_eq!(processor.calculate_offset(0x100), 0x2100);
350        assert_eq!(processor.get_base_offset(), 0x2000);
351    }
352
353    #[test]
354    fn test_offset_field_types() {
355        let rule_value_offset = OffsetExtractionRule {
356            tag_id: 0x0001,
357            offset_field: OffsetField::ValueOffset,
358            base: OffsetBase::FileStart,
359            additional_offset: 0,
360        };
361
362        let rule_actual_value = OffsetExtractionRule {
363            tag_id: 0x0002,
364            offset_field: OffsetField::ActualValue,
365            base: OffsetBase::FileStart,
366            additional_offset: 0,
367        };
368
369        // Both should use value_offset field for now (simplified implementation)
370        let entry = IfdEntry {
371            tag: 0x0001,
372            data_type: 4,
373            count: 1,
374            value_offset: 0x1234,
375            entry_position: 0x500,
376        };
377
378        let context = OffsetContext::new(0, 0, 0, 0x10000);
379
380        let mut rules = HashMap::new();
381        rules.insert(0x0001, rule_value_offset);
382        rules.insert(0x0002, rule_actual_value);
383
384        let processor = EntryBasedOffsetProcessor::new(rules);
385
386        let offset1 = processor
387            .calculate_offset(&entry, processor.get_rule(0x0001).unwrap(), &context)
388            .unwrap();
389        let offset2 = processor
390            .calculate_offset(&entry, processor.get_rule(0x0002).unwrap(), &context)
391            .unwrap();
392
393        // Both should return the same result in current implementation
394        assert_eq!(offset1, 0x1234);
395        assert_eq!(offset2, 0x1234);
396    }
397}