pub(super) mod atom;
pub(super) mod constants;
pub(super) mod read;
mod r#ref;
pub(crate) mod write;
use super::AtomIdent;
use crate::error::LoftyError;
use crate::mp4::ilst::atom::AtomDataStorage;
use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE};
use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::{Tag, TagType};
use crate::traits::{Accessor, SplitAndMergeTag, TagExt};
use atom::{AdvisoryRating, Atom, AtomData};
use std::borrow::Cow;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use lofty_attr::tag;
const ARTIST: AtomIdent<'_> = AtomIdent::Fourcc(*b"\xa9ART");
const TITLE: AtomIdent<'_> = AtomIdent::Fourcc(*b"\xa9nam");
const ALBUM: AtomIdent<'_> = AtomIdent::Fourcc(*b"\xa9alb");
const GENRE: AtomIdent<'_> = AtomIdent::Fourcc(*b"\xa9gen");
const COMMENT: AtomIdent<'_> = AtomIdent::Fourcc(*b"\xa9cmt");
const ADVISORY_RATING: AtomIdent<'_> = AtomIdent::Fourcc(*b"rtng");
macro_rules! impl_accessor {
($($name:ident => $const:ident;)+) => {
paste::paste! {
$(
fn $name(&self) -> Option<Cow<'_, str>> {
if let Some(atom) = self.atom(&$const) {
if let Some(AtomData::UTF8(val) | AtomData::UTF16(val)) = atom.data().next() {
return Some(Cow::Borrowed(val));
}
}
None
}
fn [<set_ $name>](&mut self, value: String) {
self.replace_atom(Atom {
ident: $const,
data: AtomDataStorage::Single(AtomData::UTF8(value)),
})
}
fn [<remove_ $name>](&mut self) {
self.remove_atom(&$const)
}
)+
}
}
}
#[derive(Default, PartialEq, Debug, Clone)]
#[tag(description = "An MP4 ilst atom", supported_formats(MP4))]
pub struct Ilst {
pub(crate) atoms: Vec<Atom<'static>>,
}
impl Ilst {
pub fn atom(&self, ident: &AtomIdent<'_>) -> Option<&Atom<'static>> {
self.atoms.iter().find(|a| &a.ident == ident)
}
pub fn insert_atom(&mut self, atom: Atom<'_>) {
self.atoms.push(atom.into_owned());
}
pub fn replace_atom(&mut self, atom: Atom<'_>) {
self.remove_atom(&atom.ident);
self.atoms.push(atom.into_owned());
}
pub fn remove_atom(&mut self, ident: &AtomIdent<'_>) {
self.atoms
.iter()
.position(|a| &a.ident == ident)
.map(|p| self.atoms.remove(p));
}
pub fn retain<F>(&mut self, f: F)
where
F: FnMut(&Atom<'_>) -> bool,
{
self.atoms.retain(f)
}
pub fn pictures(&self) -> impl Iterator<Item = &Picture> + Clone {
const COVR: AtomIdent<'_> = AtomIdent::Fourcc(*b"covr");
self.atoms.iter().filter_map(|a| match a.ident {
COVR => {
if let Some(AtomData::Picture(pic)) = a.data().next() {
Some(pic)
} else {
None
}
},
_ => None,
})
}
pub fn insert_picture(&mut self, mut picture: Picture) {
picture.pic_type = PictureType::Other;
self.atoms.push(Atom {
ident: AtomIdent::Fourcc(*b"covr"),
data: AtomDataStorage::Single(AtomData::Picture(picture)),
})
}
pub fn remove_pictures(&mut self) {
self.atoms
.retain(|a| !matches!(a.data().next(), Some(AtomData::Picture(_))))
}
pub fn advisory_rating(&self) -> Option<AdvisoryRating> {
self.atom(&ADVISORY_RATING)
.into_iter()
.flat_map(Atom::data)
.filter_map(|data| match data {
AtomData::SignedInteger(si) => u8::try_from(*si).ok(),
AtomData::Unknown { data, .. } => data.first().copied(),
_ => None,
})
.find_map(|rating| AdvisoryRating::try_from(rating).ok())
}
pub fn set_advisory_rating(&mut self, advisory_rating: AdvisoryRating) {
let byte = advisory_rating.as_u8();
self.replace_atom(Atom {
ident: ADVISORY_RATING,
data: AtomDataStorage::Single(AtomData::SignedInteger(i32::from(byte))),
})
}
pub fn track_total(&self) -> Option<u16> {
self.extract_number(*b"trkn", 6)
}
pub fn disc_number(&self) -> Option<u16> {
self.extract_number(*b"disk", 4)
}
pub fn disc_total(&self) -> Option<u16> {
self.extract_number(*b"disk", 6)
}
fn extract_number(&self, fourcc: [u8; 4], expected_size: usize) -> Option<u16> {
if let Some(atom) = self.atom(&AtomIdent::Fourcc(fourcc)) {
match atom.data().next() {
Some(AtomData::Unknown { code: 0, data }) if data.len() >= expected_size => {
return Some(u16::from_be_bytes([
data[expected_size - 2],
data[expected_size - 1],
]))
},
_ => {},
}
}
None
}
}
impl<'a> IntoIterator for &'a Ilst {
type Item = &'a Atom<'static>;
type IntoIter = std::slice::Iter<'a, Atom<'static>>;
fn into_iter(self) -> Self::IntoIter {
self.atoms.iter()
}
}
impl IntoIterator for Ilst {
type Item = Atom<'static>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.atoms.into_iter()
}
}
impl Accessor for Ilst {
impl_accessor!(
artist => ARTIST;
title => TITLE;
album => ALBUM;
genre => GENRE;
comment => COMMENT;
);
fn track(&self) -> Option<u32> {
self.extract_number(*b"trkn", 4).map(u32::from)
}
fn set_track(&mut self, value: u32) {
let value = (value as u16).to_be_bytes();
let track_total = self.track_total().unwrap_or(0).to_be_bytes();
let data = vec![0, 0, value[0], value[1], track_total[0], track_total[1]];
self.replace_atom(Atom::unknown_implicit(AtomIdent::Fourcc(*b"trkn"), data));
}
fn remove_track(&mut self) {
self.remove_atom(&AtomIdent::Fourcc(*b"trkn"));
}
fn track_total(&self) -> Option<u32> {
self.extract_number(*b"trkn", 6).map(u32::from)
}
fn set_track_total(&mut self, value: u32) {
let value = (value as u16).to_be_bytes();
let track = self.track().unwrap_or(1).to_be_bytes();
let data = vec![0, 0, track[0], track[1], value[0], value[1]];
self.replace_atom(Atom::unknown_implicit(AtomIdent::Fourcc(*b"trkn"), data));
}
fn remove_track_total(&mut self) {
let track_num = self.track();
self.remove_atom(&AtomIdent::Fourcc(*b"trkn"));
if let Some(track_num) = track_num {
let track_bytes = (track_num as u16).to_be_bytes();
let data = vec![0, 0, track_bytes[0], track_bytes[1], 0, 0];
self.replace_atom(Atom::unknown_implicit(AtomIdent::Fourcc(*b"trkn"), data));
}
}
fn disk(&self) -> Option<u32> {
self.extract_number(*b"disk", 4).map(u32::from)
}
fn set_disk(&mut self, value: u32) {
let value = (value as u16).to_be_bytes();
let disk_total = self.disk_total().unwrap_or(0).to_be_bytes();
let data = vec![0, 0, value[0], value[1], disk_total[0], disk_total[1]];
self.replace_atom(Atom::unknown_implicit(AtomIdent::Fourcc(*b"disk"), data));
}
fn remove_disk(&mut self) {
self.remove_atom(&AtomIdent::Fourcc(*b"disk"));
}
fn disk_total(&self) -> Option<u32> {
self.extract_number(*b"disk", 6).map(u32::from)
}
fn set_disk_total(&mut self, value: u32) {
let value = (value as u16).to_be_bytes();
let disk = self.disk().unwrap_or(1).to_be_bytes();
let data = vec![0, 0, disk[0], disk[1], value[0], value[1]];
self.replace_atom(Atom::unknown_implicit(AtomIdent::Fourcc(*b"disk"), data));
}
fn remove_disk_total(&mut self) {
let disk_num = self.disk();
self.remove_atom(&AtomIdent::Fourcc(*b"disk"));
if let Some(disk_num) = disk_num {
let disk_bytes = (disk_num as u16).to_be_bytes();
let data = vec![0, 0, disk_bytes[0], disk_bytes[1], 0, 0];
self.replace_atom(Atom::unknown_implicit(AtomIdent::Fourcc(*b"disk"), data));
}
}
fn year(&self) -> Option<u32> {
if let Some(atom) = self.atom(&AtomIdent::Fourcc(*b"\xa9day")) {
if let Some(AtomData::UTF8(text)) = atom.data().next() {
return text.chars().take(4).collect::<String>().parse::<u32>().ok();
}
}
None
}
fn set_year(&mut self, value: u32) {
self.replace_atom(Atom::text(
AtomIdent::Fourcc(*b"\xa9day"),
value.to_string(),
));
}
fn remove_year(&mut self) {
self.remove_atom(&AtomIdent::Fourcc(*b"Year"));
}
}
impl TagExt for Ilst {
type Err = LoftyError;
type RefKey<'a> = &'a AtomIdent<'a>;
fn len(&self) -> usize {
self.atoms.len()
}
fn contains<'a>(&'a self, key: Self::RefKey<'a>) -> bool {
self.atoms.iter().any(|atom| &atom.ident == key)
}
fn is_empty(&self) -> bool {
self.atoms.is_empty()
}
fn save_to_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
let mut f = OpenOptions::new().read(true).write(true).open(path)?;
self.save_to(&mut f)
}
fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
self.as_ref().write_to(file)
}
fn dump_to<W: Write>(&self, writer: &mut W) -> std::result::Result<(), Self::Err> {
self.as_ref().dump_to(writer)
}
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
TagType::MP4ilst.remove_from_path(path)
}
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
TagType::MP4ilst.remove_from(file)
}
fn clear(&mut self) {
self.atoms.clear();
}
}
impl SplitAndMergeTag for Ilst {
fn split_tag(&mut self) -> Tag {
let mut tag = Tag::new(TagType::MP4ilst);
self.atoms.retain_mut(|atom| {
let Atom { ident, data } = atom;
let value = match data.first_mut() {
AtomData::UTF8(text) | AtomData::UTF16(text) => {
ItemValue::Text(std::mem::take(text))
},
AtomData::Picture(picture) => {
tag.pictures
.push(std::mem::replace(picture, TOMBSTONE_PICTURE));
return false; },
AtomData::Bool(b) => {
let text = if *b { "1".to_owned() } else { "0".to_owned() };
ItemValue::Text(text)
},
AtomData::Unknown { code: 0, data } if data.len() >= 6 => {
if let AtomIdent::Fourcc(ref fourcc) = ident {
match fourcc {
b"trkn" => {
let current = u16::from_be_bytes([data[2], data[3]]);
let total = u16::from_be_bytes([data[4], data[5]]);
tag.insert_text(ItemKey::TrackNumber, current.to_string());
tag.insert_text(ItemKey::TrackTotal, total.to_string());
return false; },
b"disk" => {
let current = u16::from_be_bytes([data[2], data[3]]);
let total = u16::from_be_bytes([data[4], data[5]]);
tag.insert_text(ItemKey::DiscNumber, current.to_string());
tag.insert_text(ItemKey::DiscTotal, total.to_string());
return false; },
_ => {},
}
}
return true; },
_ => {
return true; },
};
let key = ItemKey::from_key(
TagType::MP4ilst,
&match ident {
AtomIdent::Fourcc(fourcc) => {
fourcc.iter().map(|b| *b as char).collect::<String>()
},
AtomIdent::Freeform { mean, name } => {
format!("----:{mean}:{name}")
},
},
);
tag.items.push(TagItem::new(key, value));
false });
tag
}
fn merge_tag(&mut self, tag: Tag) {
fn convert_to_uint(space: &mut Option<u16>, cont: &str) {
if let Ok(num) = cont.parse::<u16>() {
*space = Some(num);
}
}
fn create_int_pair(tag: &mut Ilst, ident: [u8; 4], pair: (Option<u16>, Option<u16>)) {
match pair {
(None, None) => {},
_ => {
let current = pair.0.unwrap_or(0).to_be_bytes();
let total = pair.1.unwrap_or(0).to_be_bytes();
tag.atoms.push(Atom {
ident: AtomIdent::Fourcc(ident),
data: AtomDataStorage::Single(AtomData::Unknown {
code: 0,
data: vec![0, 0, current[0], current[1], total[0], total[1], 0, 0],
}),
})
},
}
}
let mut tracks: (Option<u16>, Option<u16>) = (None, None);
let mut discs: (Option<u16>, Option<u16>) = (None, None);
for item in tag.items {
let key = item.item_key;
if let Ok(ident) = TryInto::<AtomIdent<'_>>::try_into(&key) {
let data = match item.item_value {
ItemValue::Text(text) => text,
_ => continue,
};
match key {
ItemKey::TrackNumber => convert_to_uint(&mut tracks.0, data.as_str()),
ItemKey::TrackTotal => convert_to_uint(&mut tracks.1, data.as_str()),
ItemKey::DiscNumber => convert_to_uint(&mut discs.0, data.as_str()),
ItemKey::DiscTotal => convert_to_uint(&mut discs.1, data.as_str()),
ItemKey::FlagCompilation => {
if let Ok(num) = data.as_str().parse::<u8>() {
let data = match num {
0 => false,
1 => true,
_ => {
continue;
},
};
self.atoms.push(Atom {
ident: ident.into_owned(),
data: AtomDataStorage::Single(AtomData::Bool(data)),
})
}
},
_ => self.atoms.push(Atom {
ident: ident.into_owned(),
data: AtomDataStorage::Single(AtomData::UTF8(data)),
}),
}
}
}
for mut picture in tag.pictures {
picture.pic_type = PictureType::Other;
self.atoms.push(Atom {
ident: AtomIdent::Fourcc([b'c', b'o', b'v', b'r']),
data: AtomDataStorage::Single(AtomData::Picture(picture)),
})
}
create_int_pair(self, *b"trkn", tracks);
create_int_pair(self, *b"disk", discs);
}
}
impl From<Ilst> for Tag {
fn from(mut input: Ilst) -> Self {
input.split_tag()
}
}
impl From<Tag> for Ilst {
fn from(input: Tag) -> Self {
let mut ilst = Self::default();
ilst.merge_tag(input);
ilst
}
}
#[cfg(test)]
mod tests {
use crate::mp4::ilst::atom::AtomDataStorage;
use crate::mp4::read::AtomReader;
use crate::mp4::{AdvisoryRating, Atom, AtomData, AtomIdent, Ilst, Mp4File};
use crate::tag::utils::test_utils;
use crate::tag::utils::test_utils::read_path;
use crate::{Accessor, AudioFile, ItemKey, ParseOptions, Tag, TagExt, TagType};
use std::io::{Cursor, Read, Seek, Write};
fn read_ilst(path: &str) -> Ilst {
let tag = crate::tag::utils::test_utils::read_path(path);
let len = tag.len();
let cursor = Cursor::new(tag);
let mut reader = AtomReader::new(cursor).unwrap();
super::read::parse_ilst(&mut reader, len as u64).unwrap()
}
fn verify_atom(ilst: &Ilst, ident: [u8; 4], data: &AtomData) {
let atom = ilst.atom(&AtomIdent::Fourcc(ident)).unwrap();
assert_eq!(atom.data().next().unwrap(), data);
}
#[test]
fn parse_ilst() {
let mut expected_tag = Ilst::default();
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"trkn"),
AtomData::Unknown {
code: 0,
data: vec![0, 0, 0, 1, 0, 0, 0, 0],
},
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"disk"),
AtomData::Unknown {
code: 0,
data: vec![0, 0, 0, 1, 0, 2],
},
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9ART"),
AtomData::UTF8(String::from("Bar artist")),
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9alb"),
AtomData::UTF8(String::from("Baz album")),
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9cmt"),
AtomData::UTF8(String::from("Qux comment")),
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9day"),
AtomData::UTF8(String::from("1984")),
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9gen"),
AtomData::UTF8(String::from("Classical")),
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9nam"),
AtomData::UTF8(String::from("Foo title")),
));
let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/ilst/test.ilst");
let len = tag.len();
let cursor = Cursor::new(tag);
let mut reader = AtomReader::new(cursor).unwrap();
let parsed_tag = super::read::parse_ilst(&mut reader, len as u64).unwrap();
assert_eq!(expected_tag, parsed_tag);
}
#[test]
fn ilst_re_read() {
let parsed_tag = read_ilst("tests/tags/assets/ilst/test.ilst");
let mut writer = Vec::new();
parsed_tag.dump_to(&mut writer).unwrap();
let cursor = Cursor::new(&writer[8..]);
let mut reader = AtomReader::new(cursor).unwrap();
let temp_parsed_tag =
super::read::parse_ilst(&mut reader, (writer.len() - 8) as u64).unwrap();
assert_eq!(parsed_tag, temp_parsed_tag);
}
#[test]
fn ilst_to_tag() {
let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/ilst/test.ilst");
let len = tag.len();
let cursor = Cursor::new(tag);
let mut reader = AtomReader::new(cursor).unwrap();
let ilst = super::read::parse_ilst(&mut reader, len as u64).unwrap();
let tag: Tag = ilst.into();
crate::tag::utils::test_utils::verify_tag(&tag, true, true);
assert_eq!(tag.get_string(&ItemKey::DiscNumber), Some("1"));
assert_eq!(tag.get_string(&ItemKey::DiscTotal), Some("2"));
}
#[test]
fn tag_to_ilst() {
let mut tag = crate::tag::utils::test_utils::create_tag(TagType::MP4ilst);
tag.insert_text(ItemKey::DiscNumber, String::from("1"));
tag.insert_text(ItemKey::DiscTotal, String::from("2"));
let ilst: Ilst = tag.into();
verify_atom(
&ilst,
*b"\xa9nam",
&AtomData::UTF8(String::from("Foo title")),
);
verify_atom(
&ilst,
*b"\xa9ART",
&AtomData::UTF8(String::from("Bar artist")),
);
verify_atom(
&ilst,
*b"\xa9alb",
&AtomData::UTF8(String::from("Baz album")),
);
verify_atom(
&ilst,
*b"\xa9cmt",
&AtomData::UTF8(String::from("Qux comment")),
);
verify_atom(
&ilst,
*b"\xa9gen",
&AtomData::UTF8(String::from("Classical")),
);
verify_atom(
&ilst,
*b"trkn",
&AtomData::Unknown {
code: 0,
data: vec![0, 0, 0, 1, 0, 0, 0, 0],
},
);
verify_atom(
&ilst,
*b"disk",
&AtomData::Unknown {
code: 0,
data: vec![0, 0, 0, 1, 0, 2, 0, 0],
},
)
}
#[test]
fn issue_34() {
let ilst = read_ilst("tests/tags/assets/ilst/issue_34.ilst");
verify_atom(
&ilst,
*b"\xa9ART",
&AtomData::UTF8(String::from("Foo artist")),
);
verify_atom(
&ilst,
*b"plID",
&AtomData::Unknown {
code: 21,
data: 88888_u64.to_be_bytes().to_vec(),
},
)
}
#[test]
fn advisory_rating() {
let ilst = read_ilst("tests/tags/assets/ilst/advisory_rating.ilst");
verify_atom(
&ilst,
*b"\xa9ART",
&AtomData::UTF8(String::from("Foo artist")),
);
assert_eq!(ilst.advisory_rating(), Some(AdvisoryRating::Explicit));
}
#[test]
fn trailing_padding() {
const ILST_START: usize = 97;
const ILST_END: usize = 131;
const PADDING_SIZE: usize = 990;
let file_bytes = read_path("tests/files/assets/ilst_trailing_padding.m4a");
assert!(Mp4File::read_from(
&mut Cursor::new(&file_bytes),
ParseOptions::new().read_properties(false)
)
.is_ok());
let mut ilst;
let old_free_size;
{
let ilst_bytes = &file_bytes[ILST_START..ILST_END];
old_free_size =
u32::from_be_bytes(file_bytes[ILST_END..ILST_END + 4].try_into().unwrap());
assert_eq!(old_free_size, PADDING_SIZE as u32);
let cursor = Cursor::new(ilst_bytes);
let mut reader = AtomReader::new(cursor).unwrap();
ilst = super::read::parse_ilst(&mut reader, ilst_bytes.len() as u64).unwrap();
}
let mut file = tempfile::tempfile().unwrap();
file.write_all(&file_bytes).unwrap();
file.rewind().unwrap();
ilst.set_title(String::from("Exactly 21 Characters"));
ilst.save_to(&mut file).unwrap();
file.rewind().unwrap();
let mut file_bytes = Vec::new();
file.read_to_end(&mut file_bytes).unwrap();
let new_data_size = 24_u32 + 21;
let new_ilst_end = ILST_END + new_data_size as usize;
let file_atom = &file_bytes[new_ilst_end..new_ilst_end + 8];
match file_atom {
[size @ .., b'f', b'r', b'e', b'e'] => assert_eq!(
old_free_size - new_data_size,
u32::from_be_bytes(size.try_into().unwrap())
),
_ => unreachable!(),
}
file.rewind().unwrap();
assert!(Mp4File::read_from(&mut file, ParseOptions::new().read_properties(false)).is_ok());
}
#[test]
fn read_non_full_meta_atom() {
let file_bytes = read_path("tests/files/assets/non_full_meta_atom.m4a");
let file = Mp4File::read_from(
&mut Cursor::new(file_bytes),
ParseOptions::new().read_properties(false),
)
.unwrap();
assert!(file.ilst_tag.is_some());
}
#[test]
fn write_non_full_meta_atom() {
let file_bytes = read_path("tests/files/assets/non_full_meta_atom.m4a");
let mut file = tempfile::tempfile().unwrap();
file.write_all(&file_bytes).unwrap();
file.rewind().unwrap();
let mut tag = Ilst::default();
tag.insert_atom(Atom {
ident: AtomIdent::Fourcc(*b"\xa9ART"),
data: AtomDataStorage::Single(AtomData::UTF8(String::from("Foo artist"))),
});
tag.save_to(&mut file).unwrap();
file.rewind().unwrap();
let mp4_file = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap();
assert!(mp4_file.ilst_tag.is_some());
verify_atom(
&mp4_file.ilst_tag.unwrap(),
*b"\xa9ART",
&AtomData::UTF8(String::from("Foo artist")),
);
}
#[test]
fn multi_value_atom() {
let ilst = read_ilst("tests/tags/assets/ilst/multi_value_atom.ilst");
let artist_atom = ilst.atom(&AtomIdent::Fourcc(*b"\xa9ART")).unwrap();
assert_eq!(
artist_atom.data,
AtomDataStorage::Multiple(vec![
AtomData::UTF8(String::from("Foo artist")),
AtomData::UTF8(String::from("Bar artist")),
])
);
verify_atom(
&ilst,
*b"\xa9gen",
&AtomData::UTF8(String::from("Classical")),
);
}
#[test]
fn zero_sized_ilst() {
let file = Mp4File::read_from(
&mut Cursor::new(test_utils::read_path("tests/files/assets/zero/zero.ilst")),
ParseOptions::new().read_properties(false),
)
.unwrap();
assert_eq!(file.ilst(), Some(&Ilst::default()));
}
}