Skip to main content

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