use crate::slices::{Slice, SliceError};
use crate::traits::SwapBytes;
use crate::{
HasChecksumField, HasFileVersionField, HasHeaderField, IsDefault, OctatrackFileIO,
OtToolsIoError,
};
use ot_tools_io_derive::{IntegrityChecks, IsDefaultCheck};
use serde::{Deserialize, Serialize};
use serde_big_array::{Array, BigArray};
use std::array::from_fn;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MarkersError {
#[error("invalid loop point: {value}")]
Loop { value: u32 },
#[error("invalid trim: start={start} end={end}")]
Trim { start: u32, end: u32 },
#[error("invalid slice count: {value}")]
SliceCount { value: u32 },
#[error("invalid slice")]
Slice(#[from] SliceError),
}
pub const MARKERS_HEADER: [u8; 21] = [
0x46, 0x4f, 0x52, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x41, 0x4d, 0x50,
0x00, 0x00, 0x00, 0x00, 0x00,
];
pub const MARKERS_FILE_VERSION: u8 = 4;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, IntegrityChecks, IsDefaultCheck)]
pub struct MarkersFile {
#[serde(with = "BigArray")]
pub header: [u8; 21],
pub datatype_version: u8,
pub flex_slots: Box<Array<SlotMarkers, 136>>,
pub static_slots: Box<Array<SlotMarkers, 128>>,
pub checksum: u16,
}
impl MarkersFile {
fn new(
flex_slots: [SlotMarkers; 136],
static_slots: [SlotMarkers; 128],
) -> Result<Self, OtToolsIoError> {
let mut init = Self {
header: MARKERS_HEADER,
datatype_version: MARKERS_FILE_VERSION,
flex_slots: Array(flex_slots).into(),
static_slots: Array(static_slots).into(),
checksum: 0,
};
init.checksum = init.calculate_checksum()?;
init.validate()?;
Ok(init)
}
fn validate(&self) -> Result<bool, MarkersError> {
for slot in self.flex_slots.iter() {
slot.validate()?;
}
for slot in self.static_slots.iter() {
slot.validate()?;
}
Ok(true)
}
}
#[cfg(test)]
mod markers_file_validate {
use crate::{test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let path = get_blank_proj_dirpath().join("markers.work");
assert!(MarkersFile::from_data_file(&path)?.validate()?);
Ok(())
}
#[test]
fn invalid_trim_offset() -> Result<(), OtToolsIoError> {
let path = get_blank_proj_dirpath().join("markers.work");
let mut x = MarkersFile::from_data_file(&path)?;
x.flex_slots[0].trim_offset = 100;
assert_eq!(
x.validate().unwrap_err().to_string(),
"invalid trim: start=100 end=0".to_string()
);
Ok(())
}
#[test]
fn invalid_slice_count() -> Result<(), OtToolsIoError> {
let path = get_blank_proj_dirpath().join("markers.work");
let mut x = MarkersFile::from_data_file(&path)?;
x.flex_slots[0].slice_count = 100;
assert_eq!(
x.validate().unwrap_err().to_string(),
"invalid slice count: 100".to_string()
);
Ok(())
}
#[test]
fn invalid_loop_point() -> Result<(), OtToolsIoError> {
let path = get_blank_proj_dirpath().join("markers.work");
let mut x = MarkersFile::from_data_file(&path)?;
x.flex_slots[0].loop_point = 100;
assert_eq!(
x.validate().unwrap_err().to_string(),
"invalid loop point: 100".to_string()
);
Ok(())
}
}
impl Default for MarkersFile {
fn default() -> Self {
Self::new(
from_fn(|_| SlotMarkers::default()),
from_fn(|_| SlotMarkers::default()),
)
.unwrap()
}
}
impl SwapBytes for MarkersFile {
fn swap_bytes(self) -> Self {
let mut flex_slots = self.flex_slots.clone();
for (i, slot) in self.flex_slots.iter().enumerate() {
flex_slots[i] = slot.clone().swap_bytes();
}
let mut static_slots = self.static_slots.clone();
for (i, slot) in self.static_slots.iter().enumerate() {
static_slots[i] = slot.clone().swap_bytes();
}
Self {
header: self.header,
datatype_version: self.datatype_version,
flex_slots,
static_slots,
checksum: self.checksum.swap_bytes(),
}
}
}
impl OctatrackFileIO for MarkersFile {
fn encode(&self) -> Result<Vec<u8>, OtToolsIoError>
where
Self: Serialize,
{
let mut chkd = self.clone();
chkd.checksum = self.calculate_checksum()?;
let encoded = if cfg!(target_endian = "little") {
bincode::serialize(&chkd.swap_bytes())?
} else {
bincode::serialize(&chkd)?
};
Ok(encoded)
}
fn decode(bytes: &[u8]) -> Result<Self, OtToolsIoError>
where
Self: Sized,
Self: for<'a> Deserialize<'a>,
{
let mut x: Self = bincode::deserialize(bytes)?;
#[cfg(target_endian = "little")]
{
x = x.swap_bytes();
}
Ok(x)
}
}
#[cfg(test)]
mod decode {
use crate::{
read_bin_file, test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO,
OtToolsIoError,
};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let path = get_blank_proj_dirpath().join("markers.work");
let bytes = read_bin_file(&path)?;
let s = MarkersFile::decode(&bytes)?;
assert_eq!(s, MarkersFile::default());
Ok(())
}
}
#[cfg(test)]
mod encode {
use crate::{
read_bin_file, test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO,
OtToolsIoError,
};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let path = get_blank_proj_dirpath().join("markers.work");
let bytes = read_bin_file(&path)?;
let b = MarkersFile::default().encode()?;
assert_eq!(b, bytes);
Ok(())
}
}
impl HasChecksumField for MarkersFile {
fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
let bytes = bincode::serialize(self)?;
let mut chk: u16 = 0;
for byte in &bytes[16..bytes.len() - 2] {
chk = chk.wrapping_add(*byte as u16);
}
Ok(chk)
}
fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
Ok(self.checksum == self.calculate_checksum()?)
}
}
#[cfg(test)]
mod checksum_field {
use crate::{HasChecksumField, MarkersFile, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let mut x = MarkersFile::default();
x.checksum = x.calculate_checksum()?;
assert!(x.check_checksum()?);
Ok(())
}
#[test]
fn invalid() -> Result<(), OtToolsIoError> {
let x = MarkersFile {
checksum: u16::MAX,
..Default::default()
};
assert!(!x.check_checksum()?);
Ok(())
}
mod files {
use crate::test_utils::{get_blank_proj_dirpath, get_markers_dirpath};
use crate::{HasChecksumField, MarkersFile, OctatrackFileIO, OtToolsIoError};
use std::path::Path;
fn helper_test_chksum(fp: &Path) -> Result<(u16, u16), OtToolsIoError> {
let valid = MarkersFile::from_data_file(fp)?;
let mut test = valid.clone();
test.checksum = 0;
let chk = test.calculate_checksum()?;
Ok((chk, valid.checksum))
}
#[allow(clippy::field_reassign_with_default)]
#[test]
fn default_method() -> Result<(), OtToolsIoError> {
let (_, valid) = helper_test_chksum(&get_blank_proj_dirpath().join("markers.work"))?;
let mut x = MarkersFile::default();
x.checksum = 0;
let test = x.calculate_checksum()?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn base_proj_default_file() -> Result<(), OtToolsIoError> {
let (test, valid) = helper_test_chksum(&get_blank_proj_dirpath().join("markers.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn flex_slot_noedit() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-noedit.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn flex_slot_1_loop() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-loop-edit.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn flex_slot_1_slice_1_loop() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-slice-1-looped.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn flex_slot_1_slice_1_noloop() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-slice-1-noloop.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn flex_slot_1_slice_4_noloop() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-slice-4-noloop.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn flex_slot_1_start_edit() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-start-edit.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn flex_slot_128_noedit() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_markers_dirpath().join("flex-slot-128-noedit.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn recorder_slot_1_noedit() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_markers_dirpath().join("recorder-slot-1-noedit.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn static_slot_1_noedit() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_markers_dirpath().join("static-slot-1-noedit.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn static_slot_128_noedit() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_markers_dirpath().join("static-slot-128-noedit.work"))?;
assert_eq!(test, valid);
Ok(())
}
}
}
impl HasHeaderField for MarkersFile {
fn check_header(&self) -> Result<bool, OtToolsIoError> {
Ok(self.header == MARKERS_HEADER)
}
}
#[cfg(test)]
mod header_field {
use crate::{HasHeaderField, MarkersFile, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
assert!(MarkersFile::default().check_header()?);
Ok(())
}
#[test]
fn invalid() -> Result<(), OtToolsIoError> {
let mut mutated = MarkersFile::default();
mutated.header[0] = 0x00;
mutated.header[20] = 111;
assert!(!mutated.check_header()?);
Ok(())
}
}
impl HasFileVersionField for MarkersFile {
fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
Ok(self.datatype_version == MARKERS_FILE_VERSION)
}
}
#[cfg(test)]
mod file_version_field {
use crate::{HasFileVersionField, MarkersFile, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
assert!(MarkersFile::default().check_file_version()?);
Ok(())
}
#[test]
fn invalid() -> Result<(), OtToolsIoError> {
let mut mutated = MarkersFile {
datatype_version: 0,
..Default::default()
};
mutated.datatype_version = 0;
assert!(!mutated.check_file_version()?);
Ok(())
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Hash, IsDefaultCheck)]
pub struct SlotMarkers {
pub trim_offset: u32,
pub trim_end: u32,
pub loop_point: u32,
#[serde(with = "BigArray")]
pub slices: [Slice; 64],
pub slice_count: u32,
}
impl Default for SlotMarkers {
fn default() -> Self {
Self {
trim_offset: 0,
trim_end: 0,
loop_point: 0,
slices: from_fn(|_| Slice::default()),
slice_count: 0,
}
}
}
impl SwapBytes for SlotMarkers {
fn swap_bytes(self) -> Self {
let mut slices: [Slice; 64] = self.slices;
for (i, slice) in self.slices.iter().enumerate() {
slices[i] = slice.swap_bytes();
}
Self {
trim_offset: self.trim_offset.swap_bytes(),
trim_end: self.trim_end.swap_bytes(),
loop_point: self.loop_point.swap_bytes(),
slices,
slice_count: self.slice_count.swap_bytes(),
}
}
}
impl SlotMarkers {
fn validate(&self) -> Result<bool, MarkersError> {
for slice in self.slices.iter() {
slice.validate()?;
}
if self.trim_offset > self.trim_end {
return Err(MarkersError::Trim {
start: self.trim_offset,
end: self.trim_end,
});
}
let slice_count = self.slices.iter().filter(|x| !x.is_default()).count();
if self.slice_count != slice_count as u32 {
return Err(MarkersError::SliceCount {
value: self.slice_count,
});
}
if !crate::check_loop_point(self.loop_point, self.trim_offset, self.trim_end) {
return Err(MarkersError::Loop {
value: self.loop_point,
});
}
Ok(true)
}
}
#[cfg(test)]
mod slot_markers_validate {
use super::SlotMarkers;
use crate::OtToolsIoError;
#[test]
fn valid() -> Result<(), OtToolsIoError> {
assert!(SlotMarkers::default().validate()?);
Ok(())
}
#[test]
fn invalid_trim_offset() -> Result<(), OtToolsIoError> {
let x = SlotMarkers {
trim_offset: 100,
..SlotMarkers::default()
};
assert_eq!(
x.validate().unwrap_err().to_string(),
"invalid trim: start=100 end=0".to_string()
);
Ok(())
}
#[test]
fn invalid_slice_count() -> Result<(), OtToolsIoError> {
let x = SlotMarkers {
slice_count: 100,
..SlotMarkers::default()
};
assert_eq!(
x.validate().unwrap_err().to_string(),
"invalid slice count: 100".to_string()
);
Ok(())
}
#[test]
fn invalid_loop_point() -> Result<(), OtToolsIoError> {
let x = SlotMarkers {
loop_point: 100,
..SlotMarkers::default()
};
assert_eq!(
x.validate().unwrap_err().to_string(),
"invalid loop point: 100".to_string()
);
Ok(())
}
}