dff_meta/
lib.rs

1//! DFF file utilities.
2//!
3//! A DFF (DFF File Format) is a high-resolution audio file which
4//! contains uncompressed DSD audio data along with information about
5//! how the audio data is encoded. It can also optionally include an
6//! [`ID3v2`](http://id3.org/) tag which contains metadata about the
7//! music e.g. artist, album, etc.
8//! 
9//! This library allows you to read DFF file metadata, and provides a 
10//! reference to the underlying file itself.It is up to the user to decide 
11//! how to read the sound data, using metadata including data offset and 
12//! audio length from the DffFile object to seek to and read the audio bytes 
13//! from the underlying file.
14//!
15//! Only supports ID3 tags that appear at the end of the file, not
16//! those found in the property chunk. DST is not supported. Mostly
17//! geared toward stereo and mono audio.
18//!
19//! # Examples
20//!
21//! This example displays the metadata for the DFF file
22//! `my/music.dff`.
23//!
24//!```no_run
25//! use dff_meta::DffFile;
26//! use std::path::Path;
27//!
28//! let path = Path::new("my/music.dff");
29//!
30//! match DffFile::open(path) {
31//!     Ok(dff_file) => {
32//!         println!("DFF file metadata:\n\n{}", dff_file);
33//!     }
34//!     Err(error) => {
35//!         println!("Error: {}", error);
36//!     }
37//! }
38//! ```
39
40mod id3_display;
41pub mod model;
42
43use crate::model::*;
44use id3::Tag;
45use std::collections::HashMap;
46use std::convert::TryFrom;
47use std::fmt;
48use std::fs::File;
49use std::io;
50use std::io::Read;
51use std::io::SeekFrom;
52use std::io::prelude::*;
53use std::path::Path;
54use std::u64;
55
56#[derive(Debug)]
57pub struct DffFile {
58    file: File,
59    frm_chunk: FormDsdChunk,
60    dsd_data_offset: u64,
61    dsd_audio_size: u64,
62}
63
64impl DffFile {
65    /// Attempt to open and parse the metadata of DFF file in
66    /// read-only mode. Sample data is not read into memory to keep
67    /// the memory footprint small.
68    ///
69    /// # Errors
70    ///
71    /// This function will return an error if `path` does not exist or
72    /// is not a readable and valid DFF file.
73    ///
74    pub fn open(path: &Path) -> Result<DffFile, Error> {
75        let mut file = File::open(path)?;
76        let mut chunk_buf16 = [0u8; 16];
77        let mut prop_buf4 = [0u8; 4];
78
79        // FORM (FRM8)
80        file.read_exact(&mut chunk_buf16)?;
81        let mut frm_chunk = FormDsdChunk::try_from(chunk_buf16)?;
82
83        // FVER
84        file.read_exact(&mut chunk_buf16)?;
85        let fver_chunk = FormatVersionChunk::try_from(chunk_buf16)?;
86        frm_chunk
87            .chunk
88            .local_chunks
89            .insert(FVER_LABEL, LocalChunk::FormatVersion(fver_chunk));
90
91        // Locate PROP (abort if DSD encountered first)
92        let mut hdr_buf = scan_until(&mut file, PROP_LABEL, Some(DSD_LABEL))?;
93        chunk_buf16[0..12].copy_from_slice(&hdr_buf);
94        // Read property_type (4 bytes) then build 16-byte buffer
95        file.read_exact(&mut prop_buf4)?;
96        chunk_buf16[12..16].copy_from_slice(&prop_buf4);
97
98        let pc = PropertyChunk::try_from(chunk_buf16)?;
99        frm_chunk
100            .chunk
101            .local_chunks
102            .insert(PROP_LABEL, LocalChunk::Property(pc));
103
104        let prop_data_size = match frm_chunk.chunk.local_chunks.get(&PROP_LABEL) {
105            Some(LocalChunk::Property(prop)) => prop.chunk.header.ck_data_size,
106            _ => return Err(Error::PropChunkHeader),
107        };
108        let prop_data_offset = file.stream_position()? - 4;
109
110        if let Some(LocalChunk::Property(prop_chunk_inner)) =
111            frm_chunk.chunk.local_chunks.get_mut(&PROP_LABEL)
112        {
113            while file.stream_position()? < prop_data_offset + prop_data_size as u64
114                && file.read_exact(&mut hdr_buf).is_ok()
115            {
116                let ck_id = u32_from_byte_buffer(&hdr_buf, 0);
117                let ck_data_size = u64_from_byte_buffer(&hdr_buf, 4);
118
119                match ck_id {
120                    FS_LABEL => {
121                        let mut chunk_data_buffer: [u8; 4] = [0; 4];
122                        file.read_exact(&mut chunk_data_buffer)?;
123                        let fs_chunk = SampleRateChunk::try_from({
124                            let mut buf = [0u8; 16];
125                            buf[0..12].copy_from_slice(&hdr_buf);
126                            buf[12..16].copy_from_slice(&chunk_data_buffer);
127                            buf
128                        })?;
129                        prop_chunk_inner
130                            .chunk
131                            .local_chunks
132                            .insert(FS_LABEL, LocalChunk::SampleRate(fs_chunk));
133                    }
134                    CHNL_LABEL => {
135                        let mut data_buf = vec![0u8; ck_data_size as usize];
136                        file.read_exact(&mut data_buf)?;
137                        let mut full_buf = Vec::with_capacity(12 + data_buf.len());
138                        full_buf.extend_from_slice(&hdr_buf);
139                        full_buf.extend_from_slice(&data_buf);
140                        let chnl_chunk = ChannelsChunk::try_from(full_buf.as_slice())?;
141                        prop_chunk_inner
142                            .chunk
143                            .local_chunks
144                            .insert(CHNL_LABEL, LocalChunk::Channels(chnl_chunk));
145                    }
146                    COMP_LABEL => {
147                        let mut data_buf = vec![0u8; ck_data_size as usize];
148                        file.read_exact(&mut data_buf)?;
149                        let mut full_buf = Vec::with_capacity(12 + data_buf.len());
150                        full_buf.extend_from_slice(&hdr_buf);
151                        full_buf.extend_from_slice(&data_buf);
152                        let cmpr_chunk = CompressionTypeChunk::try_from(full_buf.as_slice())?;
153                        prop_chunk_inner
154                            .chunk
155                            .local_chunks
156                            .insert(COMP_LABEL, LocalChunk::CompressionType(cmpr_chunk));
157                    }
158                    ABS_TIME_LABEL => {
159                        let mut data_buf = vec![0u8; ck_data_size as usize];
160                        file.read_exact(&mut data_buf)?;
161                        let mut full_buf = Vec::with_capacity(12 + data_buf.len());
162                        full_buf.extend_from_slice(&hdr_buf);
163                        full_buf.extend_from_slice(&data_buf);
164                        if let Ok(abs_chunk) = AbsoluteStartTimeChunk::try_from(full_buf.as_slice())
165                        {
166                            prop_chunk_inner
167                                .chunk
168                                .local_chunks
169                                .insert(ABS_TIME_LABEL, LocalChunk::AbsoluteStartTime(abs_chunk));
170                        }
171                    }
172                    LS_CONF_LABEL => {
173                        let mut data_buf = vec![0u8; ck_data_size as usize];
174                        file.read_exact(&mut data_buf)?;
175                        let mut full_buf = Vec::with_capacity(12 + data_buf.len());
176                        full_buf.extend_from_slice(&hdr_buf);
177                        full_buf.extend_from_slice(&data_buf);
178                        if let Ok(lsco_chunk) =
179                            LoudspeakerConfigChunk::try_from(full_buf.as_slice())
180                        {
181                            prop_chunk_inner
182                                .chunk
183                                .local_chunks
184                                .insert(LS_CONF_LABEL, LocalChunk::LoudspeakerConfig(lsco_chunk));
185                        }
186                    }
187                    _ => {
188                        file.seek(SeekFrom::Current(if ck_data_size & 1 == 1 {
189                            ck_data_size + 1
190                        } else {
191                            ck_data_size
192                        } as i64))?;
193                    }
194                }
195            }
196        }
197
198        hdr_buf = scan_until(&mut file, DSD_LABEL, None)?;
199        let dsd_data_offset = file.stream_position()?;
200        let dsd_chunk = DsdChunk::try_from(hdr_buf)?;
201        let dsd_audio_size = dsd_chunk.chunk.header.ck_data_size;
202        frm_chunk
203            .chunk
204            .local_chunks
205            .insert(DSD_LABEL, LocalChunk::Dsd(dsd_chunk));
206
207        // Seek past raw DSD audio data + pad byte if odd
208        file.seek(SeekFrom::Current(if dsd_audio_size & 1 == 1 {
209            dsd_audio_size + 1
210        } else {
211            dsd_audio_size
212        } as i64))?;
213
214        // If we got this far, we have enough for at least a basic dff file
215        let mut dff_file = DffFile {
216            file,
217            frm_chunk,
218            dsd_data_offset,
219            dsd_audio_size,
220        };
221
222        // Now look for an ID3 tag
223        hdr_buf = match scan_until(&mut dff_file.file, ID3_LABEL, None) {
224            Ok(buf) => buf,
225            Err(_e) => {
226                return Ok(dff_file);
227            }
228        };
229
230        match dff_file.add_id3_chunk(hdr_buf) {
231            Ok(()) => return Ok(dff_file),
232            Err(e) => return Err(Error::Id3Error(e, dff_file)),
233        };
234    }
235
236    /// Return a reference to the underlying [File](std::fs::File).
237    #[must_use]
238    pub fn file(&self) -> &File {
239        &self.file
240    }
241
242    /// Return the byte offset in the file where the DSD audio data starts.
243    pub fn get_dsd_data_offset(&self) -> u64 {
244        self.dsd_data_offset
245    }
246
247    /// Return the length of the DSD audio data in bytes.
248    pub fn get_audio_length(&self) -> u64 {
249        self.dsd_audio_size
250    }
251
252    /// Return the number of audio channels.
253    pub fn get_num_channels(&self) -> Result<usize, Error> {
254        let prop_chunk = match self.frm_chunk.chunk.local_chunks.get(&PROP_LABEL) {
255            Some(LocalChunk::Property(prop)) => prop,
256            _ => return Err(Error::PropChunkHeader),
257        };
258        match prop_chunk.chunk.local_chunks.get(&CHNL_LABEL) {
259            Some(LocalChunk::Channels(chnl)) => Ok(chnl.num_channels as usize),
260            _ => return Err(Error::ChnlNumber),
261        }
262    }
263
264    /// Return the sample rate in Hz.
265    pub fn get_sample_rate(&self) -> Result<u32, Error> {
266        let prop_chunk = match self.frm_chunk.chunk.local_chunks.get(&PROP_LABEL) {
267            Some(LocalChunk::Property(prop)) => prop,
268            _ => return Err(Error::PropChunkHeader),
269        };
270        match prop_chunk.chunk.local_chunks.get(&FS_LABEL) {
271            Some(LocalChunk::SampleRate(fs)) => Ok(fs.sample_rate),
272            _ => return Err(Error::FsChunkHeader),
273        }
274    }
275
276    /// Return the size of the FORM chunk in bytes
277    pub fn get_form_chunk_size(&self) -> u64 {
278        self.frm_chunk.chunk.header.ck_data_size + CHUNK_HEADER_SIZE
279    }
280
281    /// Return the total size of the DFF file in bytes
282    pub fn get_file_size(&self) -> Result<u64, io::Error> {
283        let metadata = self.file.metadata()?;
284        Ok(metadata.len())
285    }
286
287    /// Return a reference to the optional `ID3v2` [Tag](id3::Tag).
288    pub fn id3_tag(&self) -> &Option<Tag> {
289        match self.frm_chunk.chunk.local_chunks.get(&ID3_LABEL) {
290            Some(LocalChunk::Id3(id3_chunk)) => &id3_chunk.tag,
291            _ => &None,
292        }
293    }
294
295    /// Return the DFF format version number.
296    pub fn get_dff_version(&self) -> Result<u32, Error> {
297        let fver_chunk = match self.frm_chunk.chunk.local_chunks.get(&FVER_LABEL) {
298            Some(LocalChunk::FormatVersion(fver)) => fver,
299            _ => return Err(Error::FverChunkHeader),
300        };
301        Ok(fver_chunk.format_version)
302    }
303
304    /// Add the read ID3 chunk to the DFF file's FORM chunk.
305    fn add_id3_chunk(
306        &mut self,
307        hdr_buf: [u8; CHUNK_HEADER_SIZE as usize],
308    ) -> Result<(), id3::Error> {
309        let ck_id = u32_from_byte_buffer(&hdr_buf, 0);
310        let ck_data_size = u64_from_byte_buffer(&hdr_buf, std::mem::size_of::<ID>());
311        let mut data = vec![0u8; ck_data_size as usize];
312        let mut tag_read_err: Option<id3::Error> = None;
313
314        if let Err(_e) = self.file.read_exact(&mut data) {
315            tag_read_err = Some(id3::Error::new(
316                id3::ErrorKind::Io(std::io::Error::new(
317                    std::io::ErrorKind::Other,
318                    "Failed to read complete ID3 chunk data",
319                )),
320                "Couldn't fill buffer",
321            ));
322        }
323
324        let mut cursor = std::io::Cursor::new(&data);
325        let tag = match id3::Tag::read_from2(&mut cursor) {
326            Ok(t) => Some(t),
327            Err(e) => {
328                let partial_tag = e.partial_tag.clone();
329                tag_read_err = Some(e);
330                partial_tag
331            }
332        };
333
334        if tag.is_some() {
335            let id3_chunk = Id3Chunk {
336                chunk: Chunk::new(ChunkHeader {
337                    ck_id,
338                    ck_data_size,
339                }),
340                tag,
341            };
342
343            self.frm_chunk
344                .chunk
345                .local_chunks
346                .insert(ID3_LABEL, LocalChunk::Id3(id3_chunk));
347        }
348
349        if tag_read_err.is_some() {
350            return Err(tag_read_err.unwrap());
351        }
352        Ok(())
353    }
354}
355
356impl fmt::Display for DffFile {
357    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
358        write!(
359            f,
360            "File size: {} bytes\nForm Chunk Size: {} bytes\nDSD Audio Offset: {} bytes\nAudio Length: {} bytes\nChannels: {}\nSample Rate: {} Hz\nDFF Version: {}\nID3 Tag:\n{}",
361            self.get_file_size().unwrap_or(0),
362            self.get_form_chunk_size(),
363            self.get_dsd_data_offset(),
364            self.get_audio_length(),
365            self.get_num_channels().unwrap_or(0),
366            self.get_sample_rate().unwrap_or(0),
367            self.get_dff_version().unwrap_or(0),
368            if let Some(tag) = &self.id3_tag() {
369                id3_display::id3_tag_to_string(tag)
370            } else {
371                String::from("No ID3 tag present.")
372            }
373        )
374    }
375}
376
377/// Helper: scan forward for a chunk header matching `want_label`, error if `abort_label` appears first.
378/// Returns the 12-byte header (ID + size). Skips payload (and pad byte if size is odd) of non-matching chunks.
379fn scan_until(
380    file: &mut File,
381    want_label: u32,
382    abort_label: Option<u32>,
383) -> Result<[u8; CHUNK_HEADER_SIZE as usize], Error> {
384    loop {
385        let mut hdr = [0u8; CHUNK_HEADER_SIZE as usize];
386        match file.read_exact(&mut hdr) {
387            Ok(_) => {}
388            Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
389                return Err(Error::Eof);
390            }
391            Err(e) => return Err(Error::IoError(e)),
392        }
393        let ck_id = u32_from_byte_buffer(&hdr, 0);
394        let ck_data_size = u64_from_byte_buffer(&hdr, std::mem::size_of::<ID>());
395        if ck_id == want_label {
396            return Ok(hdr);
397        } else if Some(ck_id) == abort_label {
398            return Err(Error::PrematureTagFound(
399                String::from_utf8_lossy(&ck_id.to_be_bytes()).to_string(),
400            ));
401        } else {
402            // Skip payload + pad if odd
403            file.seek(SeekFrom::Current(ck_data_size as i64))?;
404            if ck_data_size & 1 == 1 {
405                file.seek(SeekFrom::Current(1))?;
406            }
407        }
408    }
409}
410
411/// Return a `u64` which starts from `index` in the specified byte
412/// buffer, interpretting the bytes as little-endian.
413fn u64_from_byte_buffer(buffer: &[u8], index: usize) -> u64 {
414    let mut byte_array: [u8; 8] = [0; 8];
415    byte_array.copy_from_slice(&buffer[index..index + 8]);
416
417    u64::from_be_bytes(byte_array)
418}
419
420/// Return a `u32` which starts from `index` in the specified byte
421/// buffer, interpretting the bytes as little-endian.
422fn u32_from_byte_buffer(buffer: &[u8], index: usize) -> u32 {
423    let mut byte_array: [u8; 4] = [0; 4];
424    byte_array.copy_from_slice(&buffer[index..index + 4]);
425
426    u32::from_be_bytes(byte_array)
427}
428
429impl Chunk {
430    pub fn new(header: ChunkHeader) -> Chunk {
431        Chunk {
432            header,
433            local_chunks: HashMap::new(),
434        }
435    }
436}
437
438impl FormDsdChunk {
439    #[inline]
440    pub fn is_valid(&self) -> bool {
441        self.chunk.header.ck_id == u32::from_be_bytes(*b"FRM8") && self.form_type == DSD_LABEL
442    }
443}
444
445/// IMPLEMENTATION: Convert 16‑byte header into FormDsdChunk
446impl TryFrom<[u8; 16]> for FormDsdChunk {
447    type Error = Error;
448
449    fn try_from(buf: [u8; 16]) -> Result<Self, Self::Error> {
450        // Big‑endian helpers
451        let be_u32 = |i: usize| {
452            let mut a = [0u8; 4];
453            a.copy_from_slice(&buf[i..i + 4]);
454            u32::from_be_bytes(a)
455        };
456        let be_u64 = |i: usize| {
457            let mut a = [0u8; 8];
458            a.copy_from_slice(&buf[i..i + 8]);
459            u64::from_be_bytes(a)
460        };
461
462        let ck_id = be_u32(0);
463        let ck_data_size = be_u64(4);
464        let form_type = be_u32(12);
465        let header = ChunkHeader {
466            ck_id,
467            ck_data_size,
468        };
469        let chunk = FormDsdChunk {
470            chunk: Chunk::new(header),
471            form_type,
472        };
473
474        if !chunk.is_valid() {
475            return Err(Error::FormChunkHeader);
476        }
477        if chunk.form_type != DSD_LABEL {
478            return Err(Error::FormTypeMismatch);
479        }
480
481        Ok(chunk)
482    }
483}
484
485impl TryFrom<[u8; 16]> for FormatVersionChunk {
486    type Error = Error;
487
488    fn try_from(buf: [u8; 16]) -> Result<Self, Self::Error> {
489        let be_u32 = |i: usize| {
490            let mut a = [0u8; 4];
491            a.copy_from_slice(&buf[i..i + 4]);
492            u32::from_be_bytes(a)
493        };
494        let be_u64 = |i: usize| {
495            let mut a = [0u8; 8];
496            a.copy_from_slice(&buf[i..i + 8]);
497            u64::from_be_bytes(a)
498        };
499
500        let ck_id = be_u32(0);
501        let ck_data_size = be_u64(4);
502        let version = be_u32(12);
503        let header = ChunkHeader {
504            ck_id,
505            ck_data_size,
506        };
507        let chunk = FormatVersionChunk {
508            chunk: Chunk::new(header),
509            format_version: version,
510        };
511
512        if chunk.chunk.header.ck_id != FVER_LABEL {
513            return Err(Error::FverChunkHeader);
514        }
515        if chunk.chunk.header.ck_data_size != 4 {
516            return Err(Error::FverChunkSize);
517        }
518
519        Ok(chunk)
520    }
521}
522
523impl TryFrom<[u8; 16]> for PropertyChunk {
524    type Error = Error;
525    fn try_from(buf: [u8; 16]) -> Result<Self, Self::Error> {
526        let be_u32 = |i: usize| {
527            let mut a = [0u8; 4];
528            a.copy_from_slice(&buf[i..i + 4]);
529            u32::from_be_bytes(a)
530        };
531        let be_u64 = |i: usize| {
532            let mut a = [0u8; 8];
533            a.copy_from_slice(&buf[i..i + 8]);
534            u64::from_be_bytes(a)
535        };
536
537        let ck_id = be_u32(0);
538        let ck_data_size = be_u64(4);
539        // Must at least contain 4 bytes for property_type.
540        if ck_data_size < 4 {
541            return Err(Error::ChnlChunkSize); // reuse generic size error not ideal; kept minimal
542        }
543        let property_type = be_u32(12);
544
545        let header = ChunkHeader {
546            ck_id,
547            ck_data_size,
548        };
549        let chunk = PropertyChunk {
550            chunk: Chunk::new(header),
551            property_type,
552        };
553
554        if chunk.chunk.header.ck_id != PROP_LABEL {
555            return Err(Error::PropChunkHeader);
556        }
557        if chunk.property_type != SND_LABEL {
558            return Err(Error::PropChunkType);
559        }
560        Ok(chunk)
561    }
562}
563
564impl TryFrom<[u8; 16]> for SampleRateChunk {
565    type Error = Error;
566    fn try_from(buf: [u8; 16]) -> Result<Self, Self::Error> {
567        let be_u32 = |i: usize| {
568            let mut a = [0u8; 4];
569            a.copy_from_slice(&buf[i..i + 4]);
570            u32::from_be_bytes(a)
571        };
572        let be_u64 = |i: usize| {
573            let mut a = [0u8; 8];
574            a.copy_from_slice(&buf[i..i + 8]);
575            u64::from_be_bytes(a)
576        };
577
578        let ck_id = be_u32(0);
579        let ck_data_size = be_u64(4);
580        let sample_rate = be_u32(12);
581
582        let header = ChunkHeader {
583            ck_id,
584            ck_data_size,
585        };
586        let chunk = SampleRateChunk {
587            chunk: Chunk::new(header),
588            sample_rate,
589        };
590
591        if chunk.chunk.header.ck_id != FS_LABEL {
592            return Err(Error::FsChunkHeader);
593        }
594        if chunk.chunk.header.ck_data_size != 4 {
595            return Err(Error::FsChunkSize);
596        }
597        Ok(chunk)
598    }
599}
600
601impl TryFrom<&[u8]> for ChannelsChunk {
602    type Error = Error;
603    fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
604        if buf.len() < 14 {
605            return Err(Error::ChnlChunkSize);
606        }
607
608        let ck_id = {
609            let mut a = [0u8; 4];
610            a.copy_from_slice(&buf[0..4]);
611            u32::from_be_bytes(a)
612        };
613
614        let ck_data_size = {
615            let mut a = [0u8; 8];
616            a.copy_from_slice(&buf[4..12]);
617            u64::from_be_bytes(a)
618        };
619
620        // Total expected length = 12(header) + ck_data_size
621        if buf.len() as u64 != 12 + ck_data_size {
622            return Err(Error::ChnlChunkSize);
623        }
624
625        let num_channels = {
626            let mut a = [0u8; 2];
627            a.copy_from_slice(&buf[12..14]);
628            u16::from_be_bytes(a)
629        };
630
631        if num_channels != 1 && num_channels != 2 {
632            return Err(Error::ChnlNumber);
633        }
634
635        // Each channel id is 4 bytes
636        let expected_ids_bytes = (num_channels as usize) * 4;
637        if 14 + expected_ids_bytes != buf.len() {
638            return Err(Error::ChnlChunkSize);
639        }
640
641        let mut channel_ids = Vec::with_capacity(num_channels as usize);
642        let mut idx = 14;
643        for _ in 0..num_channels {
644            let mut a = [0u8; 4];
645            a.copy_from_slice(&buf[idx..idx + 4]);
646            channel_ids.push(u32::from_be_bytes(a));
647            idx += 4;
648        }
649
650        let header = ChunkHeader {
651            ck_id,
652            ck_data_size,
653        };
654
655        let chunk = ChannelsChunk {
656            chunk: Chunk::new(header),
657            num_channels,
658            ch_id: channel_ids,
659        };
660
661        if chunk.chunk.header.ck_id != CHNL_LABEL {
662            return Err(Error::ChnlChunkHeader);
663        }
664        if expected_ids_bytes != chunk.ch_id.len() * 4 {
665            return Err(Error::ChnlChunkSize);
666        }
667        Ok(chunk)
668    }
669}
670
671impl TryFrom<&[u8]> for CompressionTypeChunk {
672    type Error = Error;
673    fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
674        // Need at least 12 (header) + 4 (compression type)
675        if buf.len() < 16 {
676            return Err(Error::CmprChunkSize);
677        }
678
679        // Header
680        let ck_id = {
681            let mut a = [0u8; 4];
682            a.copy_from_slice(&buf[0..4]);
683            u32::from_be_bytes(a)
684        };
685
686        let ck_data_size = {
687            let mut a = [0u8; 8];
688            a.copy_from_slice(&buf[4..12]);
689            u64::from_be_bytes(a)
690        };
691
692        if buf.len() as u64 != 12 + ck_data_size || ck_data_size < 4 {
693            return Err(Error::CmprChunkSize);
694        }
695
696        let compression_type = {
697            let mut a = [0u8; 4];
698            a.copy_from_slice(&buf[12..16]);
699            u32::from_be_bytes(a)
700        };
701
702        // Remaining bytes (if any) are a UTF-8 / ASCII name, often null terminated
703        let name_bytes = if ck_data_size > 4 {
704            &buf[16..(12 + ck_data_size as usize)]
705        } else {
706            &[]
707        };
708
709        let compression_name = match std::str::from_utf8(name_bytes) {
710            Ok(inner_str) => inner_str.to_string(),
711            Err(_) => String::new(),
712        };
713
714        let chunk = CompressionTypeChunk {
715            chunk: Chunk::new(ChunkHeader {
716                ck_id,
717                ck_data_size,
718            }),
719            compression_type,
720            compression_name,
721        };
722
723        if chunk.chunk.header.ck_id != COMP_LABEL {
724            return Err(Error::CmprChunkHeader);
725        }
726        // New check: must be 'DSD '
727        // DST not yet implemented
728        if chunk.compression_type != DSD_LABEL || chunk.compression_name == "DST Encoded" {
729            return Err(Error::CmprTypeMismatch);
730        }
731        Ok(chunk)
732    }
733}
734
735impl TryFrom<&[u8]> for AbsoluteStartTimeChunk {
736    type Error = Error;
737    fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
738        // Need full header (12) + payload (8)
739        if buf.len() < 20 {
740            return Err(Error::AbssChunkSize);
741        }
742        // Chunk ID
743        let ck_id = {
744            let mut a = [0u8; 4];
745            a.copy_from_slice(&buf[0..4]);
746            u32::from_be_bytes(a)
747        };
748        // Data size (must be 8 bytes for: hours(2) + minutes(1) + seconds(1) + samples(4))
749        let ck_data_size = {
750            let mut a = [0u8; 8];
751            a.copy_from_slice(&buf[4..12]);
752            u64::from_be_bytes(a)
753        };
754        if ck_data_size != 8 || buf.len() as u64 != 12 + ck_data_size {
755            return Err(Error::AbssChunkSize);
756        }
757
758        // Payload layout (big-endian):
759        // bytes 12..14 : U16 hours
760        // byte  14     : U8 minutes
761        // byte  15     : U8 seconds
762        // bytes 16..20 : U32 samples (sample offset within that second)
763        let hours = {
764            let mut a = [0u8; 2];
765            a.copy_from_slice(&buf[12..14]);
766            u16::from_be_bytes(a)
767        };
768        let minutes = buf[14];
769        let seconds = buf[15];
770        let samples = {
771            let mut a = [0u8; 4];
772            a.copy_from_slice(&buf[16..20]);
773            u32::from_be_bytes(a)
774        };
775
776        let chunk = AbsoluteStartTimeChunk {
777            chunk: Chunk::new(ChunkHeader {
778                ck_id,
779                ck_data_size,
780            }),
781            hours,
782            minutes,
783            seconds,
784            samples,
785        };
786
787        if chunk.chunk.header.ck_id != ABS_TIME_LABEL {
788            return Err(Error::AbssChunkHeader);
789        }
790        Ok(chunk)
791    }
792}
793
794impl TryFrom<&[u8]> for LoudspeakerConfigChunk {
795    type Error = Error;
796    fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
797        // Need header (12) + 2 bytes payload
798        if buf.len() < 14 {
799            return Err(Error::LscoChunkSize);
800        }
801        let ck_id = {
802            let mut a = [0u8; 4];
803            a.copy_from_slice(&buf[0..4]);
804            u32::from_be_bytes(a)
805        };
806        if ck_id != LS_CONF_LABEL {
807            return Err(Error::LscoChunkHeader);
808        }
809        let ck_data_size = {
810            let mut a = [0u8; 8];
811            a.copy_from_slice(&buf[4..12]);
812            u64::from_be_bytes(a)
813        };
814
815        if buf.len() as u64 != 12 + ck_data_size {
816            return Err(Error::LscoChunkSize);
817        }
818
819        let ls_config = {
820            let mut a = [0u8; 2];
821            a.copy_from_slice(&buf[12..14]);
822            u16::from_be_bytes(a)
823        };
824
825        let chunk = LoudspeakerConfigChunk {
826            chunk: Chunk::new(ChunkHeader {
827                ck_id,
828                ck_data_size,
829            }),
830            ls_config,
831        };
832
833        // LSCO payload is exactly 2 bytes (U16 loudspeaker configuration code)
834        if chunk.chunk.header.ck_data_size != 2 {
835            return Err(Error::LscoChunkSize);
836        }
837        Ok(chunk)
838    }
839}
840
841impl fmt::Display for DsdChunk {
842    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
843        // Copy packed field to a local to avoid unaligned reference.
844        let size = self.chunk.header.ck_data_size;
845        write!(f, "Audio Length = {} bytes", size)
846    }
847}
848
849impl TryFrom<[u8; CHUNK_HEADER_SIZE as usize]> for DsdChunk {
850    type Error = Error;
851
852    fn try_from(buf: [u8; CHUNK_HEADER_SIZE as usize]) -> Result<Self, Self::Error> {
853        let ck_id = {
854            let mut a = [0u8; 4];
855            a.copy_from_slice(&buf[0..4]);
856            u32::from_be_bytes(a)
857        };
858        if ck_id != DSD_LABEL {
859            return Err(Error::DsdChunkHeader);
860        }
861
862        let ck_data_size = {
863            let mut a = [0u8; 8];
864            a.copy_from_slice(&buf[4..12]);
865            u64::from_be_bytes(a)
866        };
867        if ck_data_size == 0 {
868            return Err(Error::DsdChunkSize);
869        }
870        Ok(DsdChunk {
871            chunk: Chunk::new(ChunkHeader {
872                ck_id,
873                ck_data_size,
874            }),
875        })
876    }
877}
878
879impl fmt::Display for Error {
880    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
881        match self {
882            Error::DsdChunkHeader => f.write_str("A DSD chunk must start with the bytes 'DSD '."),
883            Error::DsdChunkSize => f.write_str("A DSD chunk must not have size 0."),
884            Error::Id3Error(id3_error, dff_file) => {
885                write!(f, "Id3 error: {} in file: {}", id3_error, dff_file)
886            }
887            Error::IoError(io_error) => {
888                write!(f, "IO error: {}", io_error)
889            }
890            Error::PrematureTagFound(e) => {
891                write!(f, "Chunk {} was found before it should have been.", e)
892            }
893            Error::FormChunkHeader => f.write_str("FORM chunk must start with 'FRM8'."),
894            Error::FormTypeMismatch => f.write_str("FORM chunk form type must be 'DSD '."),
895            Error::FverChunkHeader => f.write_str("Format Version chunk must start with 'FVER'."),
896            Error::FverChunkSize => f.write_str("FVER chunk data size must be 4."),
897            Error::FverUnsupportedVersion => {
898                f.write_str("Unsupported format version in FVER chunk.")
899            }
900            Error::PropChunkHeader => f.write_str("Property chunk must start with 'PROP'."),
901            Error::PropChunkType => f.write_str("Property chunk type must be 'SND '."),
902            Error::FsChunkHeader => f.write_str("Sample rate chunk must start with 'FS  '."),
903            Error::FsChunkSize => f.write_str("FS chunk size must be 4."),
904            Error::ChnlChunkHeader => f.write_str("Channels chunk must start with 'CHNL'."),
905            Error::ChnlChunkSize => f.write_str("CHNL chunk size does not match channel data."),
906            Error::ChnlNumber => f.write_str("CHNL number not found or is unsupported."),
907            Error::CmprChunkHeader => f.write_str("Compression type chunk must start with 'CMPR'."),
908            Error::CmprChunkSize => f.write_str("CMPR chunk size invalid or inconsistent."),
909            Error::AbssChunkHeader => {
910                f.write_str("Absolute start time chunk must start with 'ABSS'.")
911            }
912            Error::AbssChunkSize => f.write_str("ABSS chunk size invalid."),
913            Error::LscoChunkHeader => {
914                f.write_str("Loudspeaker config chunk must start with 'LSCO'.")
915            }
916            Error::LscoChunkSize => f.write_str("LSCO chunk size invalid or inconsistent."),
917            Error::CmprTypeMismatch => {
918                f.write_str("Compression type must be 'DSD '. DST not supported.")
919            }
920            Error::Eof => f.write_str("Unexpected end of file."),
921        }
922    }
923}
924
925impl std::error::Error for Error {
926    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
927        match self {
928            Error::IoError(io_error) => Some(io_error),
929            _ => None,
930        }
931    }
932}
933
934impl From<io::Error> for Error {
935    fn from(error: io::Error) -> Self {
936        Error::IoError(error)
937    }
938}
939
940#[cfg(test)]
941mod tests {
942    use id3::TagLike;
943
944    use super::*;
945
946    #[test]
947    fn read_file() {
948        let filename = "1kHz.dff";
949        let path = Path::new(filename);
950        let dff_file = DffFile::open(path).unwrap();
951
952        assert_eq!(dff_file.get_file_size().unwrap(), 36592);
953        assert_eq!(dff_file.get_form_chunk_size(), 71872);
954        assert_eq!(dff_file.get_dsd_data_offset(), 130);
955        assert_eq!(dff_file.get_audio_length(), 35280);
956        assert_eq!(dff_file.get_num_channels().unwrap(), 2);
957        assert_eq!(dff_file.get_sample_rate().unwrap(), 2822400);
958        assert_eq!(dff_file.get_dff_version().unwrap(), 17104896);
959
960        let tag = dff_file.id3_tag().clone().unwrap();
961
962        assert_eq!(tag.title().unwrap(), ".05 sec 1kHz");
963        assert_eq!(tag.artist().unwrap(), "dff");
964        assert_eq!(tag.album().unwrap(), "Test Tones");
965        assert_eq!(tag.year().unwrap(), 2025);
966        assert_eq!(tag.genre().unwrap(), "Test");
967        assert_eq!(tag.track().unwrap(), 1);
968    }
969}