1use crate::slices::{Slice, SliceError};
22use crate::traits::SwapBytes;
23use crate::{
24 HasChecksumField, HasFileVersionField, HasHeaderField, IsDefault, OctatrackFileIO,
25 OtToolsIoError,
26};
27use ot_tools_io_derive::{IntegrityChecks, IsDefaultCheck};
28use serde::{Deserialize, Serialize};
29use serde_big_array::{Array, BigArray};
30use std::array::from_fn;
31use thiserror::Error;
32#[derive(Debug, Error)]
43pub enum MarkersError {
44 #[error("invalid loop point: {value}")]
45 Loop { value: u32 },
46 #[error("invalid trim: start={start} end={end}")]
47 Trim { start: u32, end: u32 },
48 #[error("invalid slice count: {value}")]
49 SliceCount { value: u32 },
50 #[error("invalid slice")]
51 Slice(#[from] SliceError),
52}
53
54pub const MARKERS_HEADER: [u8; 21] = [
56 0x46, 0x4f, 0x52, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x41, 0x4d, 0x50,
57 0x00, 0x00, 0x00, 0x00, 0x00,
58];
59
60pub const MARKERS_FILE_VERSION: u8 = 4;
62
63#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, IntegrityChecks, IsDefaultCheck)]
65pub struct MarkersFile {
66 #[serde(with = "BigArray")]
67 pub header: [u8; 21],
68
69 pub datatype_version: u8,
88
89 pub flex_slots: Box<Array<SlotMarkers, 136>>,
91
92 pub static_slots: Box<Array<SlotMarkers, 128>>,
94
95 pub checksum: u16,
97}
98
99impl MarkersFile {
100 fn new(
101 flex_slots: [SlotMarkers; 136],
102 static_slots: [SlotMarkers; 128],
103 ) -> Result<Self, OtToolsIoError> {
104 let mut init = Self {
105 header: MARKERS_HEADER,
106 datatype_version: MARKERS_FILE_VERSION,
107 flex_slots: Array(flex_slots).into(),
108 static_slots: Array(static_slots).into(),
109 checksum: 0,
110 };
111
112 init.checksum = init.calculate_checksum()?;
113 init.validate()?;
114 Ok(init)
115 }
116
117 fn validate(&self) -> Result<bool, MarkersError> {
118 for slot in self.flex_slots.iter() {
119 slot.validate()?;
120 }
121 for slot in self.static_slots.iter() {
122 slot.validate()?;
123 }
124
125 Ok(true)
126 }
127}
128
129#[cfg(test)]
130mod markers_file_validate {
131 use crate::{test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO, OtToolsIoError};
132 #[test]
133 fn valid() -> Result<(), OtToolsIoError> {
134 let path = get_blank_proj_dirpath().join("markers.work");
135 assert!(MarkersFile::from_data_file(&path)?.validate()?);
136 Ok(())
137 }
138
139 #[test]
140 fn invalid_trim_offset() -> Result<(), OtToolsIoError> {
141 let path = get_blank_proj_dirpath().join("markers.work");
142 let mut x = MarkersFile::from_data_file(&path)?;
143 x.flex_slots[0].trim_offset = 100;
144 assert_eq!(
145 x.validate().unwrap_err().to_string(),
146 "invalid trim: start=100 end=0".to_string()
147 );
148 Ok(())
149 }
150
151 #[test]
152 fn invalid_slice_count() -> Result<(), OtToolsIoError> {
153 let path = get_blank_proj_dirpath().join("markers.work");
154 let mut x = MarkersFile::from_data_file(&path)?;
155 x.flex_slots[0].slice_count = 100;
156 assert_eq!(
157 x.validate().unwrap_err().to_string(),
158 "invalid slice count: 100".to_string()
159 );
160 Ok(())
161 }
162
163 #[test]
164 fn invalid_loop_point() -> Result<(), OtToolsIoError> {
165 let path = get_blank_proj_dirpath().join("markers.work");
166 let mut x = MarkersFile::from_data_file(&path)?;
167 x.flex_slots[0].loop_point = 100;
168 assert_eq!(
169 x.validate().unwrap_err().to_string(),
170 "invalid loop point: 100".to_string()
171 );
172 Ok(())
173 }
174}
175
176impl Default for MarkersFile {
177 fn default() -> Self {
178 Self::new(
179 from_fn(|_| SlotMarkers::default()),
180 from_fn(|_| SlotMarkers::default()),
181 )
182 .unwrap()
183 }
184}
185
186impl SwapBytes for MarkersFile {
187 fn swap_bytes(self) -> Self {
188 let mut flex_slots = self.flex_slots.clone();
189 for (i, slot) in self.flex_slots.iter().enumerate() {
190 flex_slots[i] = slot.clone().swap_bytes();
191 }
192
193 let mut static_slots = self.static_slots.clone();
194 for (i, slot) in self.static_slots.iter().enumerate() {
195 static_slots[i] = slot.clone().swap_bytes();
196 }
197
198 Self {
199 header: self.header,
200 datatype_version: self.datatype_version,
201 flex_slots,
202 static_slots,
203 checksum: self.checksum.swap_bytes(),
204 }
205 }
206}
207
208impl OctatrackFileIO for MarkersFile {
209 fn encode(&self) -> Result<Vec<u8>, OtToolsIoError>
210 where
211 Self: Serialize,
212 {
213 let mut chkd = self.clone();
214 chkd.checksum = self.calculate_checksum()?;
215 let encoded = if cfg!(target_endian = "little") {
216 bincode::serialize(&chkd.swap_bytes())?
217 } else {
218 bincode::serialize(&chkd)?
219 };
220 Ok(encoded)
221 }
222
223 fn decode(bytes: &[u8]) -> Result<Self, OtToolsIoError>
224 where
225 Self: Sized,
226 Self: for<'a> Deserialize<'a>,
227 {
228 let mut x: Self = bincode::deserialize(bytes)?;
229 #[cfg(target_endian = "little")]
230 {
231 x = x.swap_bytes();
232 }
233
234 Ok(x)
235 }
236}
237
238#[cfg(test)]
239mod decode {
240 use crate::{
241 read_bin_file, test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO,
242 OtToolsIoError,
243 };
244 #[test]
245 fn valid() -> Result<(), OtToolsIoError> {
246 let path = get_blank_proj_dirpath().join("markers.work");
247 let bytes = read_bin_file(&path)?;
248 let s = MarkersFile::decode(&bytes)?;
249 assert_eq!(s, MarkersFile::default());
250 Ok(())
251 }
252}
253
254#[cfg(test)]
255mod encode {
256 use crate::{
257 read_bin_file, test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO,
258 OtToolsIoError,
259 };
260 #[test]
261 fn valid() -> Result<(), OtToolsIoError> {
262 let path = get_blank_proj_dirpath().join("markers.work");
263 let bytes = read_bin_file(&path)?;
264 let b = MarkersFile::default().encode()?;
265 assert_eq!(b, bytes);
266 Ok(())
267 }
268}
269
270impl HasChecksumField for MarkersFile {
271 fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
272 let bytes = bincode::serialize(self)?;
273 let mut chk: u16 = 0;
274 for byte in &bytes[16..bytes.len() - 2] {
275 chk = chk.wrapping_add(*byte as u16);
276 }
277 Ok(chk)
278 }
279 fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
280 Ok(self.checksum == self.calculate_checksum()?)
281 }
282}
283
284#[cfg(test)]
285mod checksum_field {
286 use crate::{HasChecksumField, MarkersFile, OtToolsIoError};
287 #[test]
288 fn valid() -> Result<(), OtToolsIoError> {
289 let mut x = MarkersFile::default();
290 x.checksum = x.calculate_checksum()?;
291 assert!(x.check_checksum()?);
292 Ok(())
293 }
294
295 #[test]
296 fn invalid() -> Result<(), OtToolsIoError> {
297 let x = MarkersFile {
298 checksum: u16::MAX,
299 ..Default::default()
300 };
301 assert!(!x.check_checksum()?);
302 Ok(())
303 }
304
305 mod files {
306 use crate::test_utils::{get_blank_proj_dirpath, get_markers_dirpath};
307 use crate::{HasChecksumField, MarkersFile, OctatrackFileIO, OtToolsIoError};
308 use std::path::Path;
309
310 fn helper_test_chksum(fp: &Path) -> Result<(u16, u16), OtToolsIoError> {
311 let valid = MarkersFile::from_data_file(fp)?;
312 let mut test = valid.clone();
313 test.checksum = 0;
314 let chk = test.calculate_checksum()?;
315 Ok((chk, valid.checksum))
316 }
317
318 #[allow(clippy::field_reassign_with_default)]
319 #[test]
320 fn default_method() -> Result<(), OtToolsIoError> {
321 let (_, valid) = helper_test_chksum(&get_blank_proj_dirpath().join("markers.work"))?;
322 let mut x = MarkersFile::default();
323 x.checksum = 0;
324 let test = x.calculate_checksum()?;
325 assert_eq!(test, valid);
326 Ok(())
327 }
328
329 #[test]
330 fn base_proj_default_file() -> Result<(), OtToolsIoError> {
331 let (test, valid) = helper_test_chksum(&get_blank_proj_dirpath().join("markers.work"))?;
332 assert_eq!(test, valid);
333 Ok(())
334 }
335
336 #[test]
337 fn flex_slot_noedit() -> Result<(), OtToolsIoError> {
338 let (test, valid) =
339 helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-noedit.work"))?;
340 assert_eq!(test, valid);
341 Ok(())
342 }
343
344 #[test]
345 fn flex_slot_1_loop() -> Result<(), OtToolsIoError> {
346 let (test, valid) =
347 helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-loop-edit.work"))?;
348 assert_eq!(test, valid);
349 Ok(())
350 }
351
352 #[test]
353 fn flex_slot_1_slice_1_loop() -> Result<(), OtToolsIoError> {
354 let (test, valid) =
355 helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-slice-1-looped.work"))?;
356 assert_eq!(test, valid);
357 Ok(())
358 }
359
360 #[test]
361 fn flex_slot_1_slice_1_noloop() -> Result<(), OtToolsIoError> {
362 let (test, valid) =
363 helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-slice-1-noloop.work"))?;
364 assert_eq!(test, valid);
365 Ok(())
366 }
367
368 #[test]
369 fn flex_slot_1_slice_4_noloop() -> Result<(), OtToolsIoError> {
370 let (test, valid) =
371 helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-slice-4-noloop.work"))?;
372 assert_eq!(test, valid);
373 Ok(())
374 }
375
376 #[test]
377 fn flex_slot_1_start_edit() -> Result<(), OtToolsIoError> {
378 let (test, valid) =
379 helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-start-edit.work"))?;
380 assert_eq!(test, valid);
381 Ok(())
382 }
383
384 #[test]
385 fn flex_slot_128_noedit() -> Result<(), OtToolsIoError> {
386 let (test, valid) =
387 helper_test_chksum(&get_markers_dirpath().join("flex-slot-128-noedit.work"))?;
388 assert_eq!(test, valid);
389 Ok(())
390 }
391
392 #[test]
393 fn recorder_slot_1_noedit() -> Result<(), OtToolsIoError> {
394 let (test, valid) =
395 helper_test_chksum(&get_markers_dirpath().join("recorder-slot-1-noedit.work"))?;
396 assert_eq!(test, valid);
397 Ok(())
398 }
399
400 #[test]
401 fn static_slot_1_noedit() -> Result<(), OtToolsIoError> {
402 let (test, valid) =
403 helper_test_chksum(&get_markers_dirpath().join("static-slot-1-noedit.work"))?;
404 assert_eq!(test, valid);
405 Ok(())
406 }
407
408 #[test]
409 fn static_slot_128_noedit() -> Result<(), OtToolsIoError> {
410 let (test, valid) =
411 helper_test_chksum(&get_markers_dirpath().join("static-slot-128-noedit.work"))?;
412 assert_eq!(test, valid);
413 Ok(())
414 }
415 }
416}
417
418impl HasHeaderField for MarkersFile {
419 fn check_header(&self) -> Result<bool, OtToolsIoError> {
420 Ok(self.header == MARKERS_HEADER)
421 }
422}
423
424#[cfg(test)]
425mod header_field {
426 use crate::{HasHeaderField, MarkersFile, OtToolsIoError};
427 #[test]
428 fn valid() -> Result<(), OtToolsIoError> {
429 assert!(MarkersFile::default().check_header()?);
430 Ok(())
431 }
432
433 #[test]
434 fn invalid() -> Result<(), OtToolsIoError> {
435 let mut mutated = MarkersFile::default();
436 mutated.header[0] = 0x00;
437 mutated.header[20] = 111;
438 assert!(!mutated.check_header()?);
439 Ok(())
440 }
441}
442
443impl HasFileVersionField for MarkersFile {
444 fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
445 Ok(self.datatype_version == MARKERS_FILE_VERSION)
446 }
447}
448
449#[cfg(test)]
450mod file_version_field {
451 use crate::{HasFileVersionField, MarkersFile, OtToolsIoError};
452 #[test]
453 fn valid() -> Result<(), OtToolsIoError> {
454 assert!(MarkersFile::default().check_file_version()?);
455 Ok(())
456 }
457
458 #[test]
459 fn invalid() -> Result<(), OtToolsIoError> {
460 let mut mutated = MarkersFile {
461 datatype_version: 0,
462 ..Default::default()
463 };
464 mutated.datatype_version = 0;
465 assert!(!mutated.check_file_version()?);
466 Ok(())
467 }
468}
469
470#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Hash)]
481pub struct SlotMarkers {
482 pub trim_offset: u32,
484
485 pub trim_end: u32,
487
488 pub loop_point: u32,
490
491 #[serde(with = "BigArray")]
493 pub slices: [Slice; 64],
494
495 pub slice_count: u32,
497}
498
499impl Default for SlotMarkers {
500 fn default() -> Self {
501 Self {
502 trim_offset: 0,
503 trim_end: 0,
504 loop_point: 0,
505 slices: from_fn(|_| Slice::default()),
506 slice_count: 0,
507 }
508 }
509}
510
511impl SwapBytes for SlotMarkers {
512 fn swap_bytes(self) -> Self {
513 let mut slices: [Slice; 64] = self.slices;
514
515 for (i, slice) in self.slices.iter().enumerate() {
516 slices[i] = slice.swap_bytes();
517 }
518
519 Self {
520 trim_offset: self.trim_offset.swap_bytes(),
521 trim_end: self.trim_end.swap_bytes(),
522 loop_point: self.loop_point.swap_bytes(),
523 slices,
524 slice_count: self.slice_count.swap_bytes(),
525 }
526 }
527}
528
529impl SlotMarkers {
530 fn validate(&self) -> Result<bool, MarkersError> {
531 for slice in self.slices.iter() {
532 slice.validate()?;
533 }
534 if self.trim_offset > self.trim_end {
535 return Err(MarkersError::Trim {
536 start: self.trim_offset,
537 end: self.trim_end,
538 });
539 }
540
541 let slice_count = self.slices.iter().filter(|x| !x.is_default()).count();
542
543 if self.slice_count != slice_count as u32 {
544 return Err(MarkersError::SliceCount {
545 value: self.slice_count,
546 });
547 }
548
549 if !crate::check_loop_point(self.loop_point, self.trim_offset, self.trim_end) {
550 return Err(MarkersError::Loop {
551 value: self.loop_point,
552 });
553 }
554
555 Ok(true)
556 }
557}
558
559#[cfg(test)]
560mod slot_markers_validate {
561 use super::SlotMarkers;
562 use crate::OtToolsIoError;
563 #[test]
564 fn valid() -> Result<(), OtToolsIoError> {
565 assert!(SlotMarkers::default().validate()?);
566 Ok(())
567 }
568
569 #[test]
570 fn invalid_trim_offset() -> Result<(), OtToolsIoError> {
571 let x = SlotMarkers {
572 trim_offset: 100,
573 ..SlotMarkers::default()
574 };
575 assert_eq!(
576 x.validate().unwrap_err().to_string(),
577 "invalid trim: start=100 end=0".to_string()
578 );
579 Ok(())
580 }
581
582 #[test]
583 fn invalid_slice_count() -> Result<(), OtToolsIoError> {
584 let x = SlotMarkers {
585 slice_count: 100,
586 ..SlotMarkers::default()
587 };
588 assert_eq!(
589 x.validate().unwrap_err().to_string(),
590 "invalid slice count: 100".to_string()
591 );
592 Ok(())
593 }
594
595 #[test]
596 fn invalid_loop_point() -> Result<(), OtToolsIoError> {
597 let x = SlotMarkers {
598 loop_point: 100,
599 ..SlotMarkers::default()
600 };
601 assert_eq!(
602 x.validate().unwrap_err().to_string(),
603 "invalid loop point: 100".to_string()
604 );
605 Ok(())
606 }
607}