Skip to main content

ape_decoder/
format.rs

1use std::io::{Read, Seek, SeekFrom};
2
3use crate::error::{ApeError, ApeResult};
4
5// ---------------------------------------------------------------------------
6// Format flag constants
7// ---------------------------------------------------------------------------
8
9pub const APE_FORMAT_FLAG_8_BIT: u16 = 1 << 0; // OBSOLETE
10#[allow(dead_code)]
11pub const APE_FORMAT_FLAG_CRC: u16 = 1 << 1; // OBSOLETE
12pub const APE_FORMAT_FLAG_HAS_PEAK_LEVEL: u16 = 1 << 2; // OBSOLETE
13pub const APE_FORMAT_FLAG_24_BIT: u16 = 1 << 3; // OBSOLETE
14pub const APE_FORMAT_FLAG_HAS_SEEK_ELEMENTS: u16 = 1 << 4;
15pub const APE_FORMAT_FLAG_CREATE_WAV_HEADER: u16 = 1 << 5;
16pub const APE_FORMAT_FLAG_AIFF: u16 = 1 << 6;
17pub const APE_FORMAT_FLAG_W64: u16 = 1 << 7;
18pub const APE_FORMAT_FLAG_SND: u16 = 1 << 8;
19pub const APE_FORMAT_FLAG_BIG_ENDIAN: u16 = 1 << 9;
20pub const APE_FORMAT_FLAG_CAF: u16 = 1 << 10;
21pub const APE_FORMAT_FLAG_SIGNED_8_BIT: u16 = 1 << 11;
22pub const APE_FORMAT_FLAG_FLOATING_POINT: u16 = 1 << 12;
23
24// ---------------------------------------------------------------------------
25// Validation constants
26// ---------------------------------------------------------------------------
27
28pub const APE_MINIMUM_CHANNELS: u16 = 1;
29pub const APE_MAXIMUM_CHANNELS: u16 = 32;
30const APE_ONE_MILLION: u32 = 1_000_000;
31const APE_WAV_HEADER_OR_FOOTER_MAXIMUM_BYTES: u64 = 8 * 1024 * 1024;
32const FIND_DESCRIPTOR_MAX_SCAN: u64 = 1_048_576; // 1 MB
33
34// Magic bytes
35const MAGIC_MAC_SPACE: &[u8; 4] = b"MAC ";
36const MAGIC_MACF: &[u8; 4] = b"MACF";
37
38// Descriptor / header sizes on disk
39const APE_DESCRIPTOR_BYTES: u32 = 52;
40const APE_HEADER_BYTES: u32 = 24;
41
42// Old header size on disk
43const APE_HEADER_OLD_BYTES: u32 = 32;
44
45// ---------------------------------------------------------------------------
46// Structs
47// ---------------------------------------------------------------------------
48
49/// Parsed APE descriptor (52 bytes minimum, all fields little-endian on disk).
50#[derive(Debug, Clone)]
51#[allow(dead_code)]
52pub struct ApeDescriptor {
53    pub magic: [u8; 4],
54    pub version: u16,
55    pub padding: u16,
56    pub descriptor_bytes: u32,
57    pub header_bytes: u32,
58    pub seek_table_bytes: u32,
59    pub header_data_bytes: u32,
60    pub frame_data_bytes: u32,
61    pub frame_data_bytes_high: u32,
62    pub terminating_data_bytes: u32,
63    pub md5: [u8; 16],
64}
65
66/// Parsed APE header (24 bytes minimum, all fields little-endian on disk).
67#[derive(Debug, Clone)]
68pub struct ApeHeader {
69    pub compression_level: u16,
70    pub format_flags: u16,
71    pub blocks_per_frame: u32,
72    pub final_frame_blocks: u32,
73    pub total_frames: u32,
74    pub bits_per_sample: u16,
75    pub channels: u16,
76    pub sample_rate: u32,
77}
78
79/// All parsed and derived information about an APE file.
80#[derive(Debug, Clone)]
81#[allow(dead_code)]
82pub struct ApeFileInfo {
83    // Parsed structures
84    pub descriptor: ApeDescriptor,
85    pub header: ApeHeader,
86
87    // Seek table (64-bit corrected values, **not** including junk_header_bytes)
88    pub seek_table: Vec<u64>,
89
90    // Junk / header data
91    pub junk_header_bytes: u32,
92    pub wav_header_data: Vec<u8>,
93
94    // Derived values
95    pub total_blocks: i64,
96    pub block_align: u16,
97    pub bytes_per_sample: u16,
98    pub wav_data_bytes: i64,
99    pub length_ms: i64,
100    pub average_bitrate: i64,
101    pub decompressed_bitrate: i64,
102    pub seek_table_elements: i32,
103    pub ape_frame_data_bytes: u64,
104    pub terminating_data_bytes: u32,
105
106    // File-level
107    pub file_bytes: u64,
108}
109
110// ---------------------------------------------------------------------------
111// Parsing helpers
112// ---------------------------------------------------------------------------
113
114fn read_u16_le<R: Read>(r: &mut R) -> ApeResult<u16> {
115    let mut buf = [0u8; 2];
116    r.read_exact(&mut buf)?;
117    Ok(u16::from_le_bytes(buf))
118}
119
120fn read_u32_le<R: Read>(r: &mut R) -> ApeResult<u32> {
121    let mut buf = [0u8; 4];
122    r.read_exact(&mut buf)?;
123    Ok(u32::from_le_bytes(buf))
124}
125
126/// Convert a 32-bit seek table into 64-bit values, detecting 4 GB wraparound.
127fn convert_32bit_seek_table(raw: &[u32]) -> Vec<u64> {
128    let mut result = Vec::with_capacity(raw.len());
129    let mut add: u64 = 0;
130    let mut previous: u32 = 0;
131    for &val in raw {
132        if val < previous {
133            add += 0x1_0000_0000_u64;
134        }
135        result.push(add + val as u64);
136        previous = val;
137    }
138    result
139}
140
141// ---------------------------------------------------------------------------
142// FindDescriptor - scan for "MAC " or "MACF" magic, handling ID3v2 junk
143// ---------------------------------------------------------------------------
144
145fn find_descriptor<R: Read + Seek>(reader: &mut R) -> ApeResult<u32> {
146    reader.seek(SeekFrom::Start(0))?;
147
148    let mut junk_bytes: u32 = 0;
149
150    // Step 1: check for ID3v2 tag
151    let mut id3_header = [0u8; 10];
152    if reader.read_exact(&mut id3_header).is_ok() && &id3_header[0..3] == b"ID3" {
153        let flags = id3_header[5];
154        let sync_safe_len: u32 = ((id3_header[6] & 0x7F) as u32) << 21
155            | ((id3_header[7] & 0x7F) as u32) << 14
156            | ((id3_header[8] & 0x7F) as u32) << 7
157            | ((id3_header[9] & 0x7F) as u32);
158
159        let has_footer = flags & (1 << 4) != 0;
160        if has_footer {
161            junk_bytes = sync_safe_len + 20;
162        } else {
163            junk_bytes = sync_safe_len + 10;
164
165            // Scan past zero-byte padding
166            reader.seek(SeekFrom::Start(junk_bytes as u64))?;
167            let mut byte = [0u8; 1];
168            loop {
169                match reader.read_exact(&mut byte) {
170                    Ok(()) if byte[0] == 0x00 => junk_bytes += 1,
171                    _ => break,
172                }
173            }
174        }
175    }
176
177    // Step 2: seek to junk_bytes and read initial 4-byte window
178    reader.seek(SeekFrom::Start(junk_bytes as u64))?;
179    let mut window = [0u8; 4];
180    reader.read_exact(&mut window)?;
181
182    // Check initial window
183    if &window == MAGIC_MAC_SPACE || &window == MAGIC_MACF {
184        return Ok(junk_bytes);
185    }
186
187    // Step 3: scan byte-by-byte up to 1 MB
188    let mut scanned: u64 = 4;
189    let mut byte = [0u8; 1];
190    while scanned < FIND_DESCRIPTOR_MAX_SCAN {
191        if reader.read_exact(&mut byte).is_err() {
192            break;
193        }
194        // Shift window left by one byte
195        window[0] = window[1];
196        window[1] = window[2];
197        window[2] = window[3];
198        window[3] = byte[0];
199        scanned += 1;
200
201        if &window == MAGIC_MAC_SPACE || &window == MAGIC_MACF {
202            // The magic starts at (junk_bytes + scanned - 4)
203            let offset = junk_bytes as u64 + scanned - 4;
204            return Ok(offset as u32);
205        }
206    }
207
208    Err(ApeError::InvalidFormat(
209        "could not find APE descriptor magic",
210    ))
211}
212
213// ---------------------------------------------------------------------------
214// Read descriptor
215// ---------------------------------------------------------------------------
216
217fn read_descriptor<R: Read>(reader: &mut R) -> ApeResult<ApeDescriptor> {
218    let mut magic = [0u8; 4];
219    reader.read_exact(&mut magic)?;
220
221    let version = read_u16_le(reader)?;
222    let padding = read_u16_le(reader)?;
223    let descriptor_bytes = read_u32_le(reader)?;
224    let header_bytes = read_u32_le(reader)?;
225    let seek_table_bytes = read_u32_le(reader)?;
226    let header_data_bytes = read_u32_le(reader)?;
227    let frame_data_bytes = read_u32_le(reader)?;
228    let frame_data_bytes_high = read_u32_le(reader)?;
229    let terminating_data_bytes = read_u32_le(reader)?;
230
231    let mut md5 = [0u8; 16];
232    reader.read_exact(&mut md5)?;
233
234    Ok(ApeDescriptor {
235        magic,
236        version,
237        padding,
238        descriptor_bytes,
239        header_bytes,
240        seek_table_bytes,
241        header_data_bytes,
242        frame_data_bytes,
243        frame_data_bytes_high,
244        terminating_data_bytes,
245        md5,
246    })
247}
248
249// ---------------------------------------------------------------------------
250// Read header (current format, version >= 3980)
251// ---------------------------------------------------------------------------
252
253fn read_header<R: Read>(reader: &mut R) -> ApeResult<ApeHeader> {
254    let compression_level = read_u16_le(reader)?;
255    let format_flags = read_u16_le(reader)?;
256    let blocks_per_frame = read_u32_le(reader)?;
257    let final_frame_blocks = read_u32_le(reader)?;
258    let total_frames = read_u32_le(reader)?;
259    let bits_per_sample = read_u16_le(reader)?;
260    let channels = read_u16_le(reader)?;
261    let sample_rate = read_u32_le(reader)?;
262
263    Ok(ApeHeader {
264        compression_level,
265        format_flags,
266        blocks_per_frame,
267        final_frame_blocks,
268        total_frames,
269        bits_per_sample,
270        channels,
271        sample_rate,
272    })
273}
274
275// ---------------------------------------------------------------------------
276// Read old header (version < 3980)
277// ---------------------------------------------------------------------------
278
279fn read_old_header<R: Read + Seek>(
280    reader: &mut R,
281    magic: [u8; 4],
282    version: u16,
283) -> ApeResult<(ApeDescriptor, ApeHeader, Vec<u8>)> {
284    // We've already consumed magic (4) + version (2) = 6 bytes.
285    // Old header is 32 bytes total; read remaining 26 bytes worth of fields.
286    let compression_level = read_u16_le(reader)?;
287    let format_flags = read_u16_le(reader)?;
288    let channels = read_u16_le(reader)?;
289    let sample_rate = read_u32_le(reader)?;
290    let wav_header_bytes = read_u32_le(reader)?;
291    let terminating_bytes = read_u32_le(reader)?;
292    let total_frames = read_u32_le(reader)?;
293    let final_frame_blocks = read_u32_le(reader)?;
294
295    if total_frames == 0 {
296        return Err(ApeError::InvalidFormat(
297            "old format: total frames is 0 (non-finalized file)",
298        ));
299    }
300
301    // Derive bits_per_sample from format flags
302    let bits_per_sample = if format_flags & APE_FORMAT_FLAG_8_BIT != 0 {
303        8u16
304    } else if format_flags & APE_FORMAT_FLAG_24_BIT != 0 {
305        24u16
306    } else {
307        16u16
308    };
309
310    // Derive blocks_per_frame from version and compression level
311    let blocks_per_frame: u32 = if version >= 3950 {
312        73728 * 4
313    } else if version >= 3900 || (version >= 3800 && compression_level == 4000) {
314        73728
315    } else {
316        9216
317    };
318
319    // Read optional fields after header
320    let mut _peak_level: u32 = 0;
321    if format_flags & APE_FORMAT_FLAG_HAS_PEAK_LEVEL != 0 {
322        _peak_level = read_u32_le(reader)?;
323    }
324
325    let seek_table_elements: u32 = if format_flags & APE_FORMAT_FLAG_HAS_SEEK_ELEMENTS != 0 {
326        read_u32_le(reader)?
327    } else {
328        total_frames
329    };
330
331    // Cap at 1M entries (~4MB) to prevent OOM from malformed headers
332    if seek_table_elements > 1_000_000 {
333        return Err(ApeError::InvalidFormat("seek table too large"));
334    }
335
336    // Read WAV header data
337    let mut wav_header_data = Vec::new();
338    if format_flags & APE_FORMAT_FLAG_CREATE_WAV_HEADER == 0 && wav_header_bytes > 0 {
339        if (wav_header_bytes as u64) > APE_WAV_HEADER_OR_FOOTER_MAXIMUM_BYTES {
340            return Err(ApeError::InvalidFormat(
341                "WAV header data exceeds 8 MB limit",
342            ));
343        }
344        wav_header_data.resize(wav_header_bytes as usize, 0);
345        reader.read_exact(&mut wav_header_data)?;
346    }
347
348    // Read seek table (u32 entries)
349    let seek_table_bytes = seek_table_elements * 4;
350    let mut seek_raw = vec![0u32; seek_table_elements as usize];
351    for entry in seek_raw.iter_mut() {
352        *entry = read_u32_le(reader)?;
353    }
354
355    // Skip seek bit table for version <= 3800
356    if version <= 3800 {
357        // seek bit table: 1 byte per element
358        reader.seek(SeekFrom::Current(seek_table_elements as i64))?;
359    }
360
361    // Build a synthetic descriptor
362    let descriptor = ApeDescriptor {
363        magic,
364        version,
365        padding: 0,
366        descriptor_bytes: 0, // no descriptor in old format
367        header_bytes: APE_HEADER_OLD_BYTES,
368        seek_table_bytes,
369        header_data_bytes: wav_header_bytes,
370        frame_data_bytes: 0,
371        frame_data_bytes_high: 0,
372        terminating_data_bytes: terminating_bytes,
373        md5: [0u8; 16],
374    };
375
376    let header = ApeHeader {
377        compression_level,
378        format_flags,
379        blocks_per_frame,
380        final_frame_blocks,
381        total_frames,
382        bits_per_sample,
383        channels,
384        sample_rate,
385    };
386
387    Ok((descriptor, header, wav_header_data))
388}
389
390// ---------------------------------------------------------------------------
391// Validation
392// ---------------------------------------------------------------------------
393
394fn validate(descriptor: &ApeDescriptor, header: &ApeHeader, file_bytes: u64) -> ApeResult<()> {
395    // Channel count
396    if header.channels < APE_MINIMUM_CHANNELS || header.channels > APE_MAXIMUM_CHANNELS {
397        return Err(ApeError::InvalidFormat(
398            "channel count out of range (must be 1..=32)",
399        ));
400    }
401
402    // Blocks per frame
403    if header.blocks_per_frame == 0 {
404        return Err(ApeError::InvalidFormat("blocks per frame is 0"));
405    }
406
407    if header.compression_level >= 5000 {
408        if header.blocks_per_frame > 10 * APE_ONE_MILLION {
409            return Err(ApeError::InvalidFormat(
410                "blocks per frame exceeds 10,000,000 for insane compression",
411            ));
412        }
413    } else if header.blocks_per_frame > APE_ONE_MILLION {
414        return Err(ApeError::InvalidFormat(
415            "blocks per frame exceeds 1,000,000",
416        ));
417    }
418
419    // Final frame blocks
420    if header.final_frame_blocks > header.blocks_per_frame {
421        return Err(ApeError::InvalidFormat(
422            "final frame blocks exceeds blocks per frame",
423        ));
424    }
425
426    // Seek table elements sanity
427    let seek_table_elements = descriptor.seek_table_bytes / 4;
428    if file_bytes > 0 && (seek_table_elements as u64) > file_bytes / 4 {
429        return Err(ApeError::InvalidFormat(
430            "seek table elements exceed file size / 4",
431        ));
432    }
433
434    // WAV header data size
435    if (descriptor.header_data_bytes as u64) > APE_WAV_HEADER_OR_FOOTER_MAXIMUM_BYTES {
436        return Err(ApeError::InvalidFormat(
437            "WAV header data exceeds 8 MB limit",
438        ));
439    }
440
441    Ok(())
442}
443
444// ---------------------------------------------------------------------------
445// Main parse function
446// ---------------------------------------------------------------------------
447
448/// Parse an APE file, returning all metadata and derived values.
449///
450/// Supports both current format (version >= 3980) and old format (version < 3980).
451pub fn parse<R: Read + Seek>(reader: &mut R) -> ApeResult<ApeFileInfo> {
452    // Get file size
453    let file_bytes = reader.seek(SeekFrom::End(0))?;
454
455    // Find descriptor magic
456    let junk_header_bytes = find_descriptor(reader)?;
457
458    // Seek to descriptor start
459    reader.seek(SeekFrom::Start(junk_header_bytes as u64))?;
460
461    // Peek at magic + version to decide format
462    let mut peek_buf = [0u8; 6];
463    reader.read_exact(&mut peek_buf)?;
464    let magic: [u8; 4] = [peek_buf[0], peek_buf[1], peek_buf[2], peek_buf[3]];
465    let version = u16::from_le_bytes([peek_buf[4], peek_buf[5]]);
466
467    if version < 3980 {
468        // Old format
469        let (descriptor, header, wav_header_data) = read_old_header(reader, magic, version)?;
470
471        // Compute derived values
472        let total_blocks: i64 = if header.total_frames == 0 {
473            0
474        } else {
475            (header.total_frames as i64 - 1) * header.blocks_per_frame as i64
476                + header.final_frame_blocks as i64
477        };
478
479        let bytes_per_sample = header.bits_per_sample / 8;
480        let block_align = (bytes_per_sample as u32 * header.channels as u32) as u16;
481        let wav_data_bytes = total_blocks.saturating_mul(block_align as i64);
482        let length_ms = if header.sample_rate > 0 {
483            total_blocks.saturating_mul(1000) / header.sample_rate as i64
484        } else {
485            0
486        };
487
488        let ape_frame_data_bytes: u64 =
489            (descriptor.frame_data_bytes_high as u64) << 32 | descriptor.frame_data_bytes as u64;
490
491        let ape_total_bytes = file_bytes as i64;
492        let average_bitrate = if length_ms > 0 {
493            ape_total_bytes.saturating_mul(8) / length_ms
494        } else {
495            0
496        };
497        let decompressed_bitrate = if header.sample_rate > 0 {
498            (block_align as i64)
499                .saturating_mul(header.sample_rate as i64)
500                .saturating_mul(8)
501                / 1000
502        } else {
503            0
504        };
505
506        let seek_table_elements = (descriptor.seek_table_bytes / 4) as i32;
507
508        // The seek table was already read by read_old_header; we need to reconstruct it.
509        // Re-read it: seek back to the right position.
510        // Actually, we need a different approach - let's re-parse more carefully.
511        // For old format, the seek table was already read in read_old_header.
512        // Let's refactor to pass it through.
513
514        // For now, return empty seek table and note this limitation.
515        // Actually, let me refactor read_old_header to also return the seek table.
516        // ... We'll reconstruct from the raw read above.
517
518        // Since read_old_header consumed the seek table, we need to re-approach.
519        // Let me re-read from the file.
520        // Actually, let's just re-do this properly.
521
522        // We already read past everything in read_old_header. Let's re-seek and re-read.
523        // The seek table starts after: junk + 32 (old header) + optional peak (4) + optional seek_elements (4) + wav_header
524        let mut seek_offset = junk_header_bytes as u64 + APE_HEADER_OLD_BYTES as u64;
525        if header.format_flags & APE_FORMAT_FLAG_HAS_PEAK_LEVEL != 0 {
526            seek_offset += 4;
527        }
528        if header.format_flags & APE_FORMAT_FLAG_HAS_SEEK_ELEMENTS != 0 {
529            seek_offset += 4;
530        }
531        if header.format_flags & APE_FORMAT_FLAG_CREATE_WAV_HEADER == 0 {
532            seek_offset += descriptor.header_data_bytes as u64;
533        }
534
535        reader.seek(SeekFrom::Start(seek_offset))?;
536        let n_seek = seek_table_elements as usize;
537        let mut seek_raw = vec![0u32; n_seek];
538        for entry in seek_raw.iter_mut() {
539            *entry = read_u32_le(reader)?;
540        }
541        let seek_table = convert_32bit_seek_table(&seek_raw);
542
543        validate(&descriptor, &header, file_bytes)?;
544
545        return Ok(ApeFileInfo {
546            descriptor,
547            header,
548            seek_table,
549            junk_header_bytes,
550            wav_header_data,
551            total_blocks,
552            block_align,
553            bytes_per_sample,
554            wav_data_bytes,
555            length_ms,
556            average_bitrate,
557            decompressed_bitrate,
558            seek_table_elements,
559            ape_frame_data_bytes,
560            terminating_data_bytes: 0,
561            file_bytes,
562        });
563    }
564
565    // Current format (version >= 3980)
566    // Seek back to descriptor start (we already consumed 6 bytes for the peek)
567    reader.seek(SeekFrom::Start(junk_header_bytes as u64))?;
568
569    // Read descriptor (52 bytes)
570    let descriptor = read_descriptor(reader)?;
571
572    // Skip extra descriptor bytes
573    if descriptor.descriptor_bytes > APE_DESCRIPTOR_BYTES {
574        reader.seek(SeekFrom::Current(
575            (descriptor.descriptor_bytes - APE_DESCRIPTOR_BYTES) as i64,
576        ))?;
577    }
578
579    // Read header (24 bytes)
580    let header = read_header(reader)?;
581
582    // Skip extra header bytes
583    if descriptor.header_bytes > APE_HEADER_BYTES {
584        reader.seek(SeekFrom::Current(
585            (descriptor.header_bytes - APE_HEADER_BYTES) as i64,
586        ))?;
587    }
588
589    // Read seek table (u32 entries, then convert to u64)
590    let seek_table_elements = (descriptor.seek_table_bytes / 4) as i32;
591    if seek_table_elements < 0 || seek_table_elements > 1_000_000 {
592        return Err(ApeError::InvalidFormat("seek table too large"));
593    }
594    if file_bytes > 0 && (seek_table_elements as u64) > file_bytes / 4 {
595        return Err(ApeError::InvalidFormat(
596            "seek table elements exceed file size",
597        ));
598    }
599    let mut seek_raw = vec![0u32; seek_table_elements as usize];
600    for entry in seek_raw.iter_mut() {
601        *entry = read_u32_le(reader)?;
602    }
603    let seek_table = convert_32bit_seek_table(&seek_raw);
604
605    // Read WAV header data
606    let mut wav_header_data = Vec::new();
607    if descriptor.header_data_bytes > 0 {
608        if (descriptor.header_data_bytes as u64) > APE_WAV_HEADER_OR_FOOTER_MAXIMUM_BYTES {
609            return Err(ApeError::InvalidFormat(
610                "WAV header data exceeds 8 MB limit",
611            ));
612        }
613        wav_header_data.resize(descriptor.header_data_bytes as usize, 0);
614        reader.read_exact(&mut wav_header_data)?;
615    }
616
617    // Validate
618    validate(&descriptor, &header, file_bytes)?;
619
620    // Compute derived values
621    let total_blocks: i64 = if header.total_frames == 0 {
622        0
623    } else {
624        (header.total_frames as i64 - 1) * header.blocks_per_frame as i64
625            + header.final_frame_blocks as i64
626    };
627
628    let bytes_per_sample = header.bits_per_sample / 8;
629    let block_align = (bytes_per_sample as u32 * header.channels as u32) as u16;
630    let wav_data_bytes = total_blocks.saturating_mul(block_align as i64);
631
632    let length_ms = if header.sample_rate > 0 {
633        total_blocks.saturating_mul(1000) / header.sample_rate as i64
634    } else {
635        0
636    };
637
638    let ape_frame_data_bytes: u64 =
639        (descriptor.frame_data_bytes_high as u64) << 32 | descriptor.frame_data_bytes as u64;
640
641    let ape_total_bytes = file_bytes as i64;
642    let average_bitrate = if length_ms > 0 {
643        ape_total_bytes.saturating_mul(8) / length_ms
644    } else {
645        0
646    };
647
648    let decompressed_bitrate = if header.sample_rate > 0 {
649        (block_align as i64)
650            .saturating_mul(header.sample_rate as i64)
651            .saturating_mul(8)
652            / 1000
653    } else {
654        0
655    };
656
657    Ok(ApeFileInfo {
658        descriptor,
659        header,
660        seek_table,
661        junk_header_bytes,
662        wav_header_data,
663        total_blocks,
664        block_align,
665        bytes_per_sample,
666        wav_data_bytes,
667        length_ms,
668        average_bitrate,
669        decompressed_bitrate,
670        seek_table_elements,
671        ape_frame_data_bytes,
672        terminating_data_bytes: 0,
673        file_bytes,
674    })
675}
676
677// ---------------------------------------------------------------------------
678// Helper methods on ApeFileInfo
679// ---------------------------------------------------------------------------
680
681impl ApeFileInfo {
682    /// Returns the number of audio blocks in the given frame.
683    ///
684    /// All frames except the last have `blocks_per_frame` blocks; the last
685    /// frame has `final_frame_blocks`.
686    pub fn frame_block_count(&self, frame_idx: u32) -> u32 {
687        if self.header.total_frames == 0 {
688            return 0;
689        }
690        if frame_idx == self.header.total_frames - 1 {
691            self.header.final_frame_blocks
692        } else {
693            self.header.blocks_per_frame
694        }
695    }
696
697    /// Returns the compressed byte count for the given frame.
698    ///
699    /// For non-final frames this is the difference between consecutive seek
700    /// table entries. For the final frame it is the distance from its seek
701    /// position to the end of the compressed data region.
702    pub fn frame_byte_count(&self, frame_idx: u32) -> u64 {
703        if self.header.total_frames == 0 {
704            return 0;
705        }
706        if frame_idx < self.header.total_frames - 1 {
707            self.seek_byte(frame_idx + 1) - self.seek_byte(frame_idx)
708        } else {
709            // Final frame: from seek position to end of compressed data
710            // End of compressed data = file_size - terminating_data - tag_bytes
711            // For simplicity we exclude terminating data; tag detection would
712            // require more work. This matches the SDK pattern.
713            let end = self
714                .file_bytes
715                .saturating_sub(self.descriptor.terminating_data_bytes as u64);
716            let start = self.seek_byte(frame_idx);
717            if end > start {
718                end - start
719            } else {
720                0
721            }
722        }
723    }
724
725    /// Returns the absolute byte offset in the file for the given frame,
726    /// including the junk header offset.
727    pub fn seek_byte(&self, frame_idx: u32) -> u64 {
728        let idx = frame_idx as usize;
729        if idx < self.seek_table.len() {
730            self.seek_table[idx] + self.junk_header_bytes as u64
731        } else {
732            0
733        }
734    }
735}
736
737// ---------------------------------------------------------------------------
738// Tests
739// ---------------------------------------------------------------------------
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744    use std::fs::File;
745    use std::path::PathBuf;
746
747    fn parse_test_file(name: &str) -> ApeFileInfo {
748        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
749            .join("tests/fixtures/ape")
750            .join(name);
751        let mut file = File::open(&path).unwrap_or_else(|e| {
752            panic!("failed to open {}: {}", path.display(), e);
753        });
754        parse(&mut file).unwrap_or_else(|e| {
755            panic!("failed to parse {}: {}", path.display(), e);
756        })
757    }
758
759    #[test]
760    fn test_sine_16s_c2000_descriptor() {
761        let info = parse_test_file("sine_16s_c2000.ape");
762        assert_eq!(&info.descriptor.magic, b"MAC ");
763        assert!(info.descriptor.version >= 3980, "expected current format");
764        assert_eq!(info.descriptor.descriptor_bytes, APE_DESCRIPTOR_BYTES);
765        assert_eq!(info.descriptor.header_bytes, APE_HEADER_BYTES);
766    }
767
768    #[test]
769    fn test_sine_16s_c2000_header() {
770        let info = parse_test_file("sine_16s_c2000.ape");
771        assert_eq!(info.header.compression_level, 2000);
772        assert_eq!(info.header.bits_per_sample, 16);
773        assert_eq!(info.header.channels, 2);
774        assert_eq!(info.header.sample_rate, 44100);
775    }
776
777    #[test]
778    fn test_sine_16m_c2000_mono() {
779        let info = parse_test_file("sine_16m_c2000.ape");
780        assert_eq!(info.header.channels, 1);
781        assert_eq!(info.header.bits_per_sample, 16);
782        assert_eq!(info.header.sample_rate, 44100);
783        assert_eq!(info.header.compression_level, 2000);
784        assert_eq!(info.block_align, 2); // 1 channel * 2 bytes
785    }
786
787    #[test]
788    fn test_sine_8s_c2000() {
789        let info = parse_test_file("sine_8s_c2000.ape");
790        assert_eq!(info.header.bits_per_sample, 8);
791        assert_eq!(info.header.channels, 2);
792        assert_eq!(info.bytes_per_sample, 1);
793        assert_eq!(info.block_align, 2); // 2 channels * 1 byte
794    }
795
796    #[test]
797    fn test_sine_24s_c2000() {
798        let info = parse_test_file("sine_24s_c2000.ape");
799        assert_eq!(info.header.bits_per_sample, 24);
800        assert_eq!(info.header.channels, 2);
801        assert_eq!(info.bytes_per_sample, 3);
802        assert_eq!(info.block_align, 6); // 2 channels * 3 bytes
803    }
804
805    #[test]
806    fn test_sine_32s_c2000() {
807        let info = parse_test_file("sine_32s_c2000.ape");
808        assert_eq!(info.header.bits_per_sample, 32);
809        assert_eq!(info.header.channels, 2);
810        assert_eq!(info.bytes_per_sample, 4);
811        assert_eq!(info.block_align, 8); // 2 channels * 4 bytes
812    }
813
814    #[test]
815    fn test_compression_levels() {
816        let c1000 = parse_test_file("sine_16s_c1000.ape");
817        assert_eq!(c1000.header.compression_level, 1000);
818
819        let c2000 = parse_test_file("sine_16s_c2000.ape");
820        assert_eq!(c2000.header.compression_level, 2000);
821
822        let c3000 = parse_test_file("sine_16s_c3000.ape");
823        assert_eq!(c3000.header.compression_level, 3000);
824
825        let c4000 = parse_test_file("sine_16s_c4000.ape");
826        assert_eq!(c4000.header.compression_level, 4000);
827
828        let c5000 = parse_test_file("sine_16s_c5000.ape");
829        assert_eq!(c5000.header.compression_level, 5000);
830    }
831
832    #[test]
833    fn test_derived_values() {
834        let info = parse_test_file("sine_16s_c2000.ape");
835
836        // block_align = (bits_per_sample / 8) * channels = 2 * 2 = 4
837        assert_eq!(info.block_align, 4);
838        assert_eq!(info.bytes_per_sample, 2);
839
840        // total_blocks should be consistent
841        if info.header.total_frames > 0 {
842            let expected = (info.header.total_frames as i64 - 1)
843                * info.header.blocks_per_frame as i64
844                + info.header.final_frame_blocks as i64;
845            assert_eq!(info.total_blocks, expected);
846        }
847
848        // wav_data_bytes = total_blocks * block_align
849        assert_eq!(
850            info.wav_data_bytes,
851            info.total_blocks * info.block_align as i64
852        );
853
854        // length_ms should be positive for non-empty files
855        assert!(info.length_ms > 0);
856
857        // bitrates should be positive
858        assert!(info.average_bitrate > 0);
859        assert!(info.decompressed_bitrate > 0);
860    }
861
862    #[test]
863    fn test_seek_table_populated() {
864        let info = parse_test_file("sine_16s_c2000.ape");
865        assert_eq!(info.seek_table.len(), info.header.total_frames as usize);
866        // First seek entry should be non-zero (points past header)
867        if !info.seek_table.is_empty() {
868            assert!(info.seek_table[0] > 0);
869        }
870        // Entries should be monotonically non-decreasing
871        for w in info.seek_table.windows(2) {
872            assert!(
873                w[1] >= w[0],
874                "seek table not monotonic: {} < {}",
875                w[1],
876                w[0]
877            );
878        }
879    }
880
881    #[test]
882    fn test_frame_block_count() {
883        let info = parse_test_file("sine_16s_c2000.ape");
884        if info.header.total_frames > 1 {
885            assert_eq!(info.frame_block_count(0), info.header.blocks_per_frame);
886            assert_eq!(
887                info.frame_block_count(info.header.total_frames - 1),
888                info.header.final_frame_blocks
889            );
890        }
891    }
892
893    #[test]
894    fn test_seek_byte_includes_junk() {
895        let info = parse_test_file("sine_16s_c2000.ape");
896        if !info.seek_table.is_empty() {
897            let raw_first = info.seek_table[0];
898            let seek_first = info.seek_byte(0);
899            assert_eq!(seek_first, raw_first + info.junk_header_bytes as u64);
900        }
901    }
902
903    #[test]
904    fn test_frame_byte_count_positive() {
905        let info = parse_test_file("sine_16s_c2000.ape");
906        for i in 0..info.header.total_frames {
907            let bc = info.frame_byte_count(i);
908            assert!(bc > 0, "frame {} byte count is 0", i);
909        }
910    }
911
912    #[test]
913    fn test_multiframe_file() {
914        let info = parse_test_file("multiframe_16s_c2000.ape");
915        // multiframe file should have multiple frames
916        assert!(
917            info.header.total_frames > 1,
918            "expected multiple frames, got {}",
919            info.header.total_frames
920        );
921        assert_eq!(info.header.channels, 2);
922        assert_eq!(info.header.bits_per_sample, 16);
923    }
924
925    #[test]
926    fn test_silence_file() {
927        let info = parse_test_file("silence_16s_c2000.ape");
928        assert_eq!(info.header.channels, 2);
929        assert_eq!(info.header.bits_per_sample, 16);
930        assert!(info.total_blocks > 0);
931    }
932
933    #[test]
934    fn test_short_file() {
935        let info = parse_test_file("short_16s_c2000.ape");
936        assert_eq!(info.header.channels, 2);
937        assert_eq!(info.header.bits_per_sample, 16);
938        assert!(info.total_blocks > 0);
939    }
940
941    #[test]
942    fn test_all_files_parseable() {
943        let test_files = [
944            "dc_offset_16s_c2000.ape",
945            "identical_16s_c2000.ape",
946            "impulse_16s_c2000.ape",
947            "left_only_16s_c2000.ape",
948            "multiframe_16s_c2000.ape",
949            "noise_16s_c2000.ape",
950            "short_16s_c2000.ape",
951            "silence_16s_c2000.ape",
952            "sine_16m_c2000.ape",
953            "sine_16s_c1000.ape",
954            "sine_16s_c2000.ape",
955            "sine_16s_c3000.ape",
956            "sine_16s_c4000.ape",
957            "sine_16s_c5000.ape",
958            "sine_24s_c2000.ape",
959            "sine_32s_c2000.ape",
960            "sine_8s_c2000.ape",
961        ];
962        for name in &test_files {
963            let info = parse_test_file(name);
964            assert!(info.header.total_frames > 0, "{}: no frames", name);
965            assert!(info.total_blocks > 0, "{}: no blocks", name);
966            assert_eq!(
967                info.seek_table.len(),
968                info.header.total_frames as usize,
969                "{}: seek table length mismatch",
970                name
971            );
972        }
973    }
974
975    #[test]
976    fn test_junk_header_bytes_zero_for_clean_files() {
977        // Standard test files should not have ID3v2 junk
978        let info = parse_test_file("sine_16s_c2000.ape");
979        assert_eq!(info.junk_header_bytes, 0);
980    }
981}