Skip to main content

ot_tools_io/arrangements/
file.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2026 Mike Robeson [dijksterhuis]
4*/
5
6use super::{
7    ArrangementError, ArrangementState, ARRANGEMENT_FILE_HEADER, ARRANGEMENT_FILE_VERSION,
8};
9use crate::identifiers::SavedState;
10use crate::{
11    Defaults, HasChecksumField, HasFileVersionField, HasHeaderField, IsDefault, OctatrackFileIO,
12    OtToolsIoError,
13};
14use ot_tools_io_derive::{
15    ArrayDefaults, AsMutDerive, AsRefDerive, BoxedArrayDefaults, IntegrityChecks,
16};
17use serde::{Deserialize, Serialize};
18use std::array::from_fn;
19
20// max length: 11336 bytes
21// note: fields are ordered according to the order they appear in the binary data
22// might change that at some point but for now leaving as is.
23/// Arrangement files (`arr??.*` file names)
24#[derive(
25    Clone,
26    Debug,
27    Eq,
28    Hash,
29    Ord,
30    PartialEq,
31    PartialOrd,
32    Deserialize,
33    Serialize,
34    IntegrityChecks,
35    ArrayDefaults,
36    BoxedArrayDefaults,
37    AsRefDerive,
38    AsMutDerive,
39)]
40pub struct ArrangementFile {
41    /// Header data
42    ///
43    /// # Validation
44    ///
45    /// Checked by the [`Self::validate`] method via the
46    /// [`HasHeaderField::check_header`] trait method.
47    pub header: [u8; 21],
48
49    /// Patch version of this data type. Is inspected on project load to determine if
50    /// the octatrack is able to load a project from current OS, or whether the project
51    /// files need to be patched and updated.
52    ///
53    /// # Validation
54    ///
55    /// Checked by the [`Self::validate`] method via the
56    /// [`HasFileVersionField::check_file_version`] trait method.
57    pub datatype_version: u8,
58
59    /// Dunno. Example data:
60    /// ```text
61    /// [0, 0]
62    /// ```
63    ///
64    /// # Validation
65    ///
66    /// Not checked by the [`Self::validate`] method.
67    pub unk1: [u8; 2],
68
69    /// Current arrangement data in active use.
70    /// This block is written when saving via Project Menu -> SYNC TO CARD.
71    ///
72    /// The second block is written when saving the arrangement via Arranger Menu -> SAVE.
73    ///
74    /// # Validation
75    ///
76    /// Checked by the [`Self::validate`] method.
77    pub working_state: ArrangementState,
78
79    /// Dunno.
80    ///
81    /// # Validation
82    ///
83    /// Not checked by the [`Self::validate`] method.
84    pub unk2: u8,
85
86    /// Whether the arrangement has been saved within the arrangement menu.
87    ///
88    /// # Validation
89    ///
90    /// Checked by the [`Self::validate`] method.
91    pub saved_state: u8,
92
93    /// Arrangement data from the previous saved state.
94    /// This block is written when saving the arrangement via Arranger Menu -> SAVE.
95    ///
96    /// # Validation
97    ///
98    /// Checked by the [`Self::validate`] method.
99    pub stored_state: ArrangementState,
100
101    /// The current save/unsaved state of **all** loaded arrangements.
102    /// 'Saved' means there's been at least one ARRANGER MENU save operation performed,
103    /// and [`ArrangementState`] data has been written to the `arrangement_state_previous` field.
104    ///
105    /// Example data:
106    /// ```text
107    /// Arrangement 1 has been saved: [1, 0, 0, 0, 0, 0, 0, 0]
108    /// Arrangement 2 has been saved: [0, 1, 0, 0, 0, 0, 0, 0]
109    /// Arrangement 2, 7 & 8 have been saved: [0, 1, 0, 0, 0, 0, 1, 1]
110    /// ```
111    ///
112    /// # Validation
113    ///
114    /// Checked by the [`Self::validate`] method.
115    pub other_saved_states: [u8; 8],
116
117    /// Checksum for the file.
118    ///
119    /// # Rant
120    ///
121    /// On the Octatrack, changing an arrangement row variant in an Arrangement
122    /// **does not** clear the bytes for that row.
123    /// All it does is change the byte indicating the type of row.
124    ///
125    /// This would be fine if the checksum calculation didn't sum these irrelevant
126    /// bytes.
127    /// But it does.
128    ///
129    /// See [`ArrangeRow`][super::ArrangeRow] for more information.
130    ///
131    /// # Validation
132    ///
133    /// Not checked by the [`Self::validate`] method.
134    /// Use the [`HasChecksumField::check_checksum`] to check this field.
135    pub checksum: u16,
136}
137
138impl Default for ArrangementFile {
139    fn default() -> Self {
140        let mut new = Self {
141            header: ARRANGEMENT_FILE_HEADER,
142            datatype_version: ARRANGEMENT_FILE_VERSION,
143            unk1: from_fn(|_| 0),
144            working_state: ArrangementState::default(),
145            unk2: 0,
146            saved_state: 0,
147            stored_state: ArrangementState::default(),
148            // TODO: by default this actually always 0, but generating test data where these things
149            //       are 'inactive' is basically impossible!
150            // for now, I'm setting this to default a 1-values array, but in reality it depends...
151            // cba to type this out fully. gonna bite me later i know it. basically i need to check
152            // if arrangement chaining affects this flag or if the arrangement has been saved.
153            other_saved_states: from_fn(|_| 0),
154            checksum: 0,
155        };
156        new.checksum = new.calculate_checksum().unwrap();
157        new
158    }
159}
160
161impl IsDefault for ArrangementFile {
162    fn is_default(&self) -> bool {
163        let default = &ArrangementFile::default();
164        // check everything except the arrangement name fields (see
165        // ArrangementBlock's IsDefault implementation for more details)
166        self.working_state.is_default()
167            && self.stored_state.is_default()
168            && default.unk1 == self.unk1
169            && default.unk2 == self.unk2
170    }
171}
172
173#[cfg(test)]
174mod is_default {
175    use crate::arrangements::ArrangementFile;
176    use crate::test_utils::get_arrange_dirpath;
177    use crate::{IsDefault, OctatrackFileIO};
178
179    #[test]
180    fn true_not_modified_default() {
181        assert!(ArrangementFile::default().is_default())
182    }
183    #[test]
184    fn true_not_modified_file() {
185        let arr =
186            ArrangementFile::from_data_file(&get_arrange_dirpath().join("blank.work")).unwrap();
187        assert!(arr.is_default())
188    }
189    #[test]
190    fn false_modified_file() {
191        let arr = ArrangementFile::from_data_file(&get_arrange_dirpath().join("full-options.work"))
192            .unwrap();
193        assert!(!arr.is_default())
194    }
195}
196
197impl ArrangementFile {
198    /// converts the `other_saved_states` field to an array of `bool` values.
199    /// ```
200    /// # use ot_tools_io::{ArrangementFile, OtToolsIoError};
201    /// # fn main() -> Result<(), OtToolsIoError> {
202    /// assert_eq!(
203    ///     ArrangementFile::default().other_saved_states(),
204    ///     [false, false, false, false, false, false, false, false]
205    /// );
206    /// # Ok(()) }
207    /// ```
208    pub fn other_saved_states(&self) -> [bool; 8] {
209        from_fn(|i| self.other_saved_states[i] == 1)
210    }
211
212    /// converts the `saved_state` field to a `bool` value.
213    /// ```
214    /// # use ot_tools_io::{ArrangementFile, OtToolsIoError};
215    /// # fn main() -> Result<(), OtToolsIoError> {
216    /// assert_eq!(
217    ///     ArrangementFile::default().saved_state(),
218    ///     false,
219    /// );
220    /// # Ok(()) }
221    /// ```
222    pub fn saved_state(&self) -> bool {
223        self.saved_state == 1
224    }
225
226    /// Owned access to the [`ArrangementState`] specified by the [`SavedState`]
227    /// ```
228    /// # use ot_tools_io::{ArrangementFile, OtToolsIoError};
229    /// # use ot_tools_io::identifiers::SavedState;
230    /// # use ot_tools_io::types::ArrangementState;
231    /// # fn main() -> Result<(), OtToolsIoError> {
232    /// assert_eq!(
233    ///     ArrangementFile::default().arrangement_state(&SavedState::Working),
234    ///     ArrangementState::default(),
235    /// );
236    /// # Ok(()) }
237    /// ```
238    pub fn arrangement_state(self, saved_state: &SavedState) -> ArrangementState {
239        match saved_state {
240            SavedState::Working => self.working_state,
241            SavedState::Stored => self.stored_state,
242        }
243    }
244
245    /// Reference access to the [`ArrangementState`] specified by the [`SavedState`]
246    /// ```
247    /// # use ot_tools_io::{ArrangementFile, OtToolsIoError};
248    /// # use ot_tools_io::identifiers::SavedState;
249    /// # use ot_tools_io::types::ArrangementState;
250    /// # fn main() -> Result<(), OtToolsIoError> {
251    /// assert_eq!(
252    ///     ArrangementFile::default().arrangement_state_ref(&SavedState::Working),
253    ///     &ArrangementState::default(),
254    /// );
255    /// # Ok(()) }
256    /// ```
257    pub fn arrangement_state_ref(&self, saved_state: &SavedState) -> &ArrangementState {
258        match saved_state {
259            SavedState::Working => &self.working_state,
260            SavedState::Stored => &self.stored_state,
261        }
262    }
263
264    /// Mutable reference access to the [`ArrangementState`] specified by the [`SavedState`]
265    /// ```
266    /// # use ot_tools_io::{ArrangementFile, OtToolsIoError};
267    /// # use ot_tools_io::identifiers::SavedState;
268    /// # use ot_tools_io::types::ArrangementState;
269    /// # fn main() -> Result<(), OtToolsIoError> {
270    /// assert_eq!(
271    ///     ArrangementFile::default().arrangement_state_mut(&SavedState::Working),
272    ///     &mut ArrangementState::default(),
273    /// );
274    /// # Ok(()) }
275    /// ```
276    pub fn arrangement_state_mut(&mut self, saved_state: &SavedState) -> &mut ArrangementState {
277        match saved_state {
278            SavedState::Working => &mut self.working_state,
279            SavedState::Stored => &mut self.stored_state,
280        }
281    }
282
283    /// Run validation on relevant fields, see the documentation on fields in
284    /// [`Self`] to find out what is actually validated.
285    /// ```
286    /// # use ot_tools_io::{ArrangementFile, OtToolsIoError};
287    /// # use ot_tools_io::types::ArrangementState;
288    /// # fn main() -> Result<(), OtToolsIoError> {
289    /// ArrangementFile::default().validate()?;
290    ///
291    /// assert!(ArrangementFile { saved_state: 5, ..Default::default() }.validate().is_err());
292    /// assert!(
293    ///     ArrangementFile {
294    ///         working_state: ArrangementState { n_rows: 10, ..Default::default() },
295    ///         ..Default::default()
296    ///     }.validate().is_err()
297    /// );
298    /// # Ok(()) }
299    /// ```
300    pub fn validate(&self) -> Result<(), OtToolsIoError> {
301        // WARNING: DO NOT CALL ANYTHING RELATED TO CHECKSUMS HERE!
302        // YOU WILL GET RECURSION!
303        self.check_header()?;
304        self.check_file_version()?;
305
306        if !self.other_saved_states.iter().all(|x| (0..=1).contains(x)) {
307            return Err(ArrangementError::SavedStates(self.other_saved_states).into());
308        }
309        if !(0..=1).contains(&self.saved_state) {
310            return Err(ArrangementError::SaveState(self.saved_state).into());
311        }
312        self.working_state.validate()?;
313        self.stored_state.validate()?;
314        Ok(())
315    }
316
317    /// We can't just use [`bincode::deserialize`] with arrangements as we need
318    /// to do some custom parsing within [`ArrangementState`]
319    pub(super) fn parse_from_bytes(bytes: &[u8; 11336]) -> Result<Self, OtToolsIoError> {
320        let mut shift = 0;
321
322        let header: [u8; 21] = from_fn(|i| bytes[i + shift]);
323        shift += 21;
324
325        let datatype_version: u8 = bytes[shift];
326        shift += 1;
327
328        let unk1: [u8; 2] = from_fn(|i| bytes[i + shift]);
329        shift += 2;
330
331        let curr_arr_bytes: [u8; 5650] = from_fn(|i| bytes[i + shift]);
332        shift += 5650;
333
334        let arrangement_state_current = ArrangementState::parse_from_bytes(&curr_arr_bytes)?;
335
336        let unk2: u8 = bytes[shift];
337        shift += 1;
338
339        let saved_flag: u8 = bytes[shift];
340        shift += 1;
341
342        let prev_arr_bytes: [u8; 5650] = from_fn(|i| bytes[i + shift]);
343        shift += 5650;
344
345        let arrangement_state_previous = ArrangementState::parse_from_bytes(&prev_arr_bytes)?;
346
347        let arrangements_saved_state: [u8; 8] = from_fn(|i| bytes[i + shift]);
348        shift += 8;
349
350        let check_sum_arr: [u8; 2] = from_fn(|i| bytes[i + shift]);
351        let checksum = u16::from_be_bytes(check_sum_arr);
352
353        let arr = Self {
354            header,
355            datatype_version,
356            unk1,
357            working_state: arrangement_state_current,
358            unk2,
359            saved_state: saved_flag,
360            stored_state: arrangement_state_previous,
361            other_saved_states: arrangements_saved_state,
362            checksum,
363        };
364        arr.validate()?;
365        Ok(arr)
366    }
367
368    /// We can't just use [`bincode::serialize`] with arrangements as we need
369    /// to do some custom parsing within [`ArrangementState`]
370    pub(super) fn parse_to_bytes(&self) -> Result<Vec<u8>, OtToolsIoError> {
371        self.validate()?;
372        let mut v = vec![];
373
374        v.append(&mut self.header.to_vec());
375        v.append(&mut vec![self.datatype_version]);
376        v.append(&mut self.unk1.to_vec());
377        v.append(&mut self.working_state.parse_to_bytes()?);
378        v.append(&mut vec![self.unk2]);
379        v.append(&mut vec![self.saved_state]);
380        v.append(&mut self.stored_state.parse_to_bytes()?);
381        v.append(&mut self.other_saved_states.to_vec());
382        v.append(&mut self.checksum.to_be_bytes().to_vec());
383        Ok(v)
384    }
385}
386
387#[cfg(test)]
388mod validate {
389    use crate::arrangements::ArrangementError;
390    use crate::{ArrangementFile, OtToolsIoError};
391
392    #[test]
393    fn err_saved_states() -> Result<(), OtToolsIoError> {
394        let arr = ArrangementFile {
395            other_saved_states: [2, 2, 2, 2, 2, 2, 2, 2],
396            ..Default::default()
397        };
398        assert_eq!(
399            arr.validate().err().unwrap().to_string(),
400            OtToolsIoError::Arrangement(ArrangementError::SavedStates([2, 2, 2, 2, 2, 2, 2, 2]))
401                .to_string()
402        );
403        Ok(())
404    }
405
406    #[test]
407    fn err_saved_state() -> Result<(), OtToolsIoError> {
408        let arr = ArrangementFile {
409            saved_state: 2,
410            ..Default::default()
411        };
412        assert_eq!(
413            arr.validate().err().unwrap().to_string(),
414            OtToolsIoError::Arrangement(ArrangementError::SaveState(2)).to_string()
415        );
416        Ok(())
417    }
418}
419
420#[cfg(test)]
421mod parse_from_bytes {
422    use crate::arrangements::mocks::mock_arrangement_file_empty_rows;
423    use crate::ArrangementFile;
424    #[test]
425    fn valid_empty_rows() {
426        let (expected, test_bytes) = mock_arrangement_file_empty_rows();
427        let r = ArrangementFile::parse_from_bytes(&test_bytes);
428        assert_eq!(expected, r.unwrap());
429    }
430}
431
432#[cfg(test)]
433mod parse_to_bytes {
434    #[test]
435    #[ignore]
436    fn nope() {
437        unreachable!(
438            "testing this would require manipulating a 11336 length array and i'm not doing that"
439        );
440    }
441}
442
443impl OctatrackFileIO for ArrangementFile {
444    fn from_bytes(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
445        ArrangementFile::parse_from_bytes(
446            bytes
447                .as_array()
448                .ok_or(ArrangementError::ByteLength(bytes.len()))?,
449        )
450    }
451
452    fn to_bytes(&self) -> Result<Vec<u8>, OtToolsIoError> {
453        self.parse_to_bytes()
454    }
455}
456
457#[cfg(test)]
458mod from_bytes {
459    use crate::{
460        read_bin_file, test_utils::get_arrange_dirpath, ArrangementFile, HasChecksumField,
461        OctatrackFileIO, OtToolsIoError,
462    };
463
464    const HACKED_ARR_NAME: [u8; 15] = [68, 73, 70, 70, 78, 65, 77, 69, 66, 76, 65, 78, 75, 49, 0];
465
466    #[test]
467    fn valid() -> Result<(), OtToolsIoError> {
468        let path = get_arrange_dirpath().join("blank.work");
469        let bytes = read_bin_file(&path)?;
470        let s = ArrangementFile::from_bytes(&bytes)?;
471
472        // TODO: having to hack the default names
473        let mut x = ArrangementFile::default();
474        x.working_state.name = HACKED_ARR_NAME;
475        x.stored_state.name = HACKED_ARR_NAME;
476        // TODO: Another hack
477        x.saved_state = 1;
478        x.other_saved_states = [1, 1, 1, 1, 1, 1, 1, 1];
479        x.checksum = x.calculate_checksum()?;
480
481        assert_eq!(s, x);
482        Ok(())
483    }
484}
485
486#[cfg(test)]
487mod to_bytes {
488    const HACKED_ARR_NAME: [u8; 15] = [68, 73, 70, 70, 78, 65, 77, 69, 66, 76, 65, 78, 75, 49, 0];
489    use crate::{
490        read_bin_file, test_utils::get_arrange_dirpath, ArrangementFile, HasChecksumField,
491        OctatrackFileIO, OtToolsIoError,
492    };
493    #[test]
494    fn valid() -> Result<(), OtToolsIoError> {
495        let path = get_arrange_dirpath().join("blank.work");
496        let bytes = read_bin_file(&path)?;
497
498        // TODO: having to hack the default names
499        let mut x = ArrangementFile::default();
500        x.working_state.name = HACKED_ARR_NAME;
501        x.stored_state.name = HACKED_ARR_NAME;
502        // TODO: Another hack
503        x.saved_state = 1;
504        x.other_saved_states = [1, 1, 1, 1, 1, 1, 1, 1];
505        x.checksum = x.calculate_checksum()?;
506
507        let b = x.to_bytes()?;
508        assert_eq!(b, bytes);
509        Ok(())
510    }
511}
512
513impl HasChecksumField for ArrangementFile {
514    fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
515        let bytes = self.to_bytes()?;
516        let payload = &bytes[16..bytes.len() - 2];
517        let mut checksum: u16 = 0;
518        for b in payload {
519            checksum = checksum.wrapping_add(*b as u16);
520        }
521        Ok(checksum)
522    }
523
524    fn update_checksum(&mut self) -> Result<(), OtToolsIoError> {
525        self.checksum = self.calculate_checksum()?;
526        Ok(())
527    }
528
529    fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
530        Ok(self.checksum == self.calculate_checksum()?)
531    }
532}
533
534// NOTE: This only tests whether the trait methods work or not, not whether they are correct!
535#[cfg(test)]
536mod checksum_field {
537    use crate::{ArrangementFile, HasChecksumField, OtToolsIoError};
538    #[test]
539    fn valid() -> Result<(), OtToolsIoError> {
540        let mut x = ArrangementFile::default();
541        x.checksum = x.calculate_checksum()?;
542        assert!(x.check_checksum()?);
543        Ok(())
544    }
545
546    #[test]
547    fn invalid() -> Result<(), OtToolsIoError> {
548        let x = ArrangementFile {
549            checksum: u16::MAX,
550            ..Default::default()
551        };
552        assert!(!x.check_checksum()?);
553        Ok(())
554    }
555
556    mod files {
557        use crate::arrangements::ArrangementFile;
558        use crate::test_utils::{get_arrange_dirpath, get_blank_proj_dirpath};
559        use crate::{HasChecksumField, OctatrackFileIO, OtToolsIoError};
560        use std::path::Path;
561
562        fn helper_test_chksum(fp: &Path) -> Result<(u16, u16), OtToolsIoError> {
563            let valid = ArrangementFile::from_data_file(fp)?;
564            let mut test = valid.clone();
565            test.checksum = 0;
566            let chk = test.calculate_checksum()?;
567            Ok((chk, valid.checksum))
568        }
569
570        #[test]
571        fn blank_project_file() -> Result<(), OtToolsIoError> {
572            let (test, valid) = helper_test_chksum(&get_blank_proj_dirpath().join("arr01.work"))?;
573            assert_eq!(test, valid);
574            Ok(())
575        }
576
577        // works with original sample attrs implementation
578        #[test]
579        fn blank() -> Result<(), OtToolsIoError> {
580            let (test, valid) = helper_test_chksum(&get_arrange_dirpath().join("blank.work"))?;
581            assert_eq!(test, valid);
582            Ok(())
583        }
584
585        // works with original sample attrs implementation
586        #[test]
587        fn blank_diffname1() -> Result<(), OtToolsIoError> {
588            let (test, valid) =
589                helper_test_chksum(&get_arrange_dirpath().join("blank-diffname1.work"))?;
590            assert_eq!(test, valid);
591            Ok(())
592        }
593
594        // works with original sample attrs implementation
595        #[test]
596        fn blank_diffname2() -> Result<(), OtToolsIoError> {
597            let (test, valid) =
598                helper_test_chksum(&get_arrange_dirpath().join("blank-diffname2.work"))?;
599            assert_eq!(test, valid);
600            Ok(())
601        }
602
603        #[test]
604        fn one_rem_row_notext() -> Result<(), OtToolsIoError> {
605            let (test, valid) =
606                helper_test_chksum(&get_arrange_dirpath().join("1-rem-blank-txt.work"))?;
607            assert_eq!(test, valid);
608            Ok(())
609        }
610
611        #[test]
612        fn one_rem_row_wtext() -> Result<(), OtToolsIoError> {
613            let (test, valid) =
614                helper_test_chksum(&get_arrange_dirpath().join("1-rem-CHAIN-txt.work"))?;
615            assert_eq!(test, valid);
616            Ok(())
617        }
618
619        #[test]
620        fn two_rem_row_wtext() -> Result<(), OtToolsIoError> {
621            let (test, valid) =
622                helper_test_chksum(&get_arrange_dirpath().join("2-rem-CHAIN-txt.work"))?;
623            assert_eq!(test, valid);
624            Ok(())
625        }
626
627        #[test]
628        fn four_patterns() -> Result<(), OtToolsIoError> {
629            let (test, valid) = helper_test_chksum(&get_arrange_dirpath().join("4-patterns.work"))?;
630            assert_eq!(test, valid);
631            Ok(())
632        }
633
634        #[test]
635        fn one_pattern() -> Result<(), OtToolsIoError> {
636            let (test, valid) = helper_test_chksum(&get_arrange_dirpath().join("1-pattern.work"))?;
637            assert_eq!(test, valid);
638            Ok(())
639        }
640
641        #[test]
642        fn one_halt() -> Result<(), OtToolsIoError> {
643            let (test, valid) = helper_test_chksum(&get_arrange_dirpath().join("1-halt.work"))?;
644            assert_eq!(test, valid);
645            Ok(())
646        }
647
648        #[test]
649        fn one_pattern_1_loop() -> Result<(), OtToolsIoError> {
650            let (test, valid) =
651                helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-loop.work"))?;
652            assert_eq!(test, valid);
653            Ok(())
654        }
655
656        #[test]
657        fn one_pattern_1_jump_1_loop() -> Result<(), OtToolsIoError> {
658            let (test, valid) =
659                helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-jump-1-loop.work"))?;
660            assert_eq!(test, valid);
661            Ok(())
662        }
663
664        #[test]
665        fn one_pattern_1_jump_1_loop_1_halt() -> Result<(), OtToolsIoError> {
666            let (test, valid) = helper_test_chksum(
667                &get_arrange_dirpath().join("1-patt-1-jump-1-loop-1-halt.work"),
668            )?;
669            assert_eq!(test, valid);
670            Ok(())
671        }
672
673        #[test]
674        fn full_options() -> Result<(), OtToolsIoError> {
675            let (test, valid) =
676                helper_test_chksum(&get_arrange_dirpath().join("full-options.work"))?;
677            assert_eq!(test, valid);
678            Ok(())
679        }
680
681        // FIXME: Did i not save/copy this properly? this only has empty rows
682        #[test]
683        fn full_options_no_rems() -> Result<(), OtToolsIoError> {
684            let (test, valid) =
685                helper_test_chksum(&get_arrange_dirpath().join("full-options-no-rems.work"))?;
686            assert_eq!(test, valid);
687            Ok(())
688        }
689
690        #[test]
691        fn no_saved_flag() -> Result<(), OtToolsIoError> {
692            let (test, valid) =
693                helper_test_chksum(&get_arrange_dirpath().join("no-saved-flag.work"))?;
694            assert_eq!(test, valid);
695            Ok(())
696        }
697
698        #[test]
699        fn with_saved_flag() -> Result<(), OtToolsIoError> {
700            let (test, valid) =
701                helper_test_chksum(&get_arrange_dirpath().join("with-saved-flag.work"))?;
702            assert_eq!(test, valid);
703            Ok(())
704        }
705
706        #[test]
707        fn blank_samename_saved() -> Result<(), OtToolsIoError> {
708            let (test, valid) = helper_test_chksum(
709                &get_arrange_dirpath().join("blank-samename-saved-chktest.work"),
710            )?;
711            assert_eq!(test, valid);
712            Ok(())
713        }
714
715        #[test]
716        fn blank_samename_unsaved() -> Result<(), OtToolsIoError> {
717            let (test, valid) = helper_test_chksum(
718                &get_arrange_dirpath().join("blank-samename-unsaved-chktest.work"),
719            )?;
720            assert_eq!(test, valid);
721            Ok(())
722        }
723    }
724}
725
726impl HasHeaderField for ArrangementFile {
727    fn check_header(&self) -> Result<bool, OtToolsIoError> {
728        Ok(self.header == ARRANGEMENT_FILE_HEADER)
729    }
730}
731
732#[cfg(test)]
733mod header_field {
734    use crate::{ArrangementFile, HasHeaderField, OtToolsIoError};
735    #[test]
736    fn valid() -> Result<(), OtToolsIoError> {
737        assert!(ArrangementFile::default().check_header()?);
738        Ok(())
739    }
740
741    #[test]
742    fn invalid() -> Result<(), OtToolsIoError> {
743        let mut mutated = ArrangementFile::default();
744        mutated.header[0] = 0x00;
745        mutated.header[1] = 200;
746        mutated.header[2] = 126;
747        assert!(!mutated.check_header()?);
748        Ok(())
749    }
750}
751
752impl HasFileVersionField for ArrangementFile {
753    fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
754        Ok(self.datatype_version == ARRANGEMENT_FILE_VERSION)
755    }
756}
757
758#[cfg(test)]
759mod file_version_field {
760    use crate::{ArrangementFile, HasFileVersionField, OtToolsIoError};
761    #[test]
762    fn valid() -> Result<(), OtToolsIoError> {
763        assert!(ArrangementFile::default().check_file_version()?);
764        Ok(())
765    }
766
767    #[test]
768    fn invalid() -> Result<(), OtToolsIoError> {
769        let x = ArrangementFile {
770            datatype_version: 0,
771            ..Default::default()
772        };
773        assert!(!x.check_file_version()?);
774        Ok(())
775    }
776}