1use 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#[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 pub header: [u8; 21],
48
49 pub datatype_version: u8,
58
59 pub unk1: [u8; 2],
68
69 pub working_state: ArrangementState,
78
79 pub unk2: u8,
85
86 pub saved_state: u8,
92
93 pub stored_state: ArrangementState,
100
101 pub other_saved_states: [u8; 8],
116
117 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 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 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 pub fn other_saved_states(&self) -> [bool; 8] {
209 from_fn(|i| self.other_saved_states[i] == 1)
210 }
211
212 pub fn saved_state(&self) -> bool {
223 self.saved_state == 1
224 }
225
226 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 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 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 pub fn validate(&self) -> Result<(), OtToolsIoError> {
301 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 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 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 let mut x = ArrangementFile::default();
474 x.working_state.name = HACKED_ARR_NAME;
475 x.stored_state.name = HACKED_ARR_NAME;
476 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 let mut x = ArrangementFile::default();
500 x.working_state.name = HACKED_ARR_NAME;
501 x.stored_state.name = HACKED_ARR_NAME;
502 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#[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 #[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 #[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 #[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 #[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}