390 lines
16 KiB
Rust
390 lines
16 KiB
Rust
//! Synthetic files: a small OP1a writer (enough of ST 377-1 to exercise the demuxer without
|
||
//! ffmpeg) and robustness checks.
|
||
|
||
use crate::*;
|
||
|
||
fn ul(b: &[u8]) -> [u8; 16] {
|
||
let mut k = [0u8; 16];
|
||
k[..b.len()].copy_from_slice(b);
|
||
k
|
||
}
|
||
|
||
fn ber(n: usize) -> Vec<u8> {
|
||
let mut v = vec![0x83];
|
||
v.extend_from_slice(&(n as u32).to_be_bytes()[1..]);
|
||
v
|
||
}
|
||
|
||
fn klv(out: &mut Vec<u8>, key: [u8; 16], v: &[u8]) {
|
||
out.extend_from_slice(&key);
|
||
out.extend_from_slice(&ber(v.len()));
|
||
out.extend_from_slice(v);
|
||
}
|
||
|
||
struct LocalSet(Vec<u8>);
|
||
impl LocalSet {
|
||
fn new(uid: u8) -> Self {
|
||
let mut s = LocalSet(Vec::new());
|
||
s.p(0x3C0A, &uuid(uid));
|
||
s
|
||
}
|
||
fn p(&mut self, tag: u16, v: &[u8]) -> &mut Self {
|
||
self.0.extend_from_slice(&tag.to_be_bytes());
|
||
self.0.extend_from_slice(&(v.len() as u16).to_be_bytes());
|
||
self.0.extend_from_slice(v);
|
||
self
|
||
}
|
||
}
|
||
|
||
fn uuid(n: u8) -> [u8; 16] {
|
||
let mut u = [0xAAu8; 16];
|
||
u[15] = n;
|
||
u
|
||
}
|
||
|
||
fn umid(n: u8) -> [u8; 32] {
|
||
let mut u = [0x55u8; 32];
|
||
u[31] = n;
|
||
u
|
||
}
|
||
|
||
fn batch(items: &[&[u8]]) -> Vec<u8> {
|
||
let mut v = (items.len() as u32).to_be_bytes().to_vec();
|
||
v.extend_from_slice(&(items.first().map_or(0, |i| i.len()) as u32).to_be_bytes());
|
||
for i in items {
|
||
v.extend_from_slice(i);
|
||
}
|
||
v
|
||
}
|
||
|
||
fn set_key(t: u8) -> [u8; 16] {
|
||
ul(&[0x06, 0x0E, 0x2B, 0x34, 0x02, 0x53, 0x01, 0x01, 0x0D, 0x01, 0x01, 0x01, 0x01, 0x01, t, 0x00])
|
||
}
|
||
|
||
const PICTURE_DEF: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x01, 0x03, 0x02, 0x02, 0x01, 0, 0, 0];
|
||
const SOUND_DEF: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x01, 0x03, 0x02, 0x02, 0x02, 0, 0, 0];
|
||
const TC_DEF: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x01, 0x03, 0x02, 0x01, 0x01, 0, 0, 0];
|
||
const OP1A: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x01, 0x09, 0x00];
|
||
const PIC_KEY: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x01, 0x02, 0x01, 0x01, 0x0D, 0x01, 0x03, 0x01, 0x15, 0x01, 0x05, 0x00];
|
||
const SND_KEY: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x01, 0x02, 0x01, 0x01, 0x0D, 0x01, 0x03, 0x01, 0x16, 0x01, 0x03, 0x00];
|
||
|
||
fn partition(kind: u8, this: u64, body_sid: u32, index_sid: u32, hbc: u64) -> Vec<u8> {
|
||
let mut v = Vec::new();
|
||
v.extend_from_slice(&1u16.to_be_bytes());
|
||
v.extend_from_slice(&3u16.to_be_bytes());
|
||
v.extend_from_slice(&1u32.to_be_bytes());
|
||
v.extend_from_slice(&this.to_be_bytes());
|
||
v.extend_from_slice(&0u64.to_be_bytes());
|
||
v.extend_from_slice(&0u64.to_be_bytes());
|
||
v.extend_from_slice(&hbc.to_be_bytes());
|
||
v.extend_from_slice(&0u64.to_be_bytes());
|
||
v.extend_from_slice(&index_sid.to_be_bytes());
|
||
v.extend_from_slice(&0u64.to_be_bytes());
|
||
v.extend_from_slice(&body_sid.to_be_bytes());
|
||
v.extend_from_slice(&OP1A);
|
||
v.extend_from_slice(&batch(&[]));
|
||
let mut out = Vec::new();
|
||
klv(&mut out, ul(&[0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, kind, 0x04, 0x00]), &v);
|
||
out
|
||
}
|
||
|
||
fn track(uid: u8, id: u32, number: u32, seq: u8) -> Vec<u8> {
|
||
let mut s = LocalSet::new(uid);
|
||
s.p(0x4801, &id.to_be_bytes()).p(0x4804, &number.to_be_bytes()).p(0x4B01, &[0, 0, 0, 25, 0, 0, 0, 1]).p(0x4B02, &0i64.to_be_bytes()).p(0x4803, &uuid(seq));
|
||
s.0
|
||
}
|
||
|
||
fn sequence(uid: u8, def: [u8; 16], dur: i64, comp: u8) -> Vec<u8> {
|
||
let mut s = LocalSet::new(uid);
|
||
s.p(0x0201, &def).p(0x0202, &dur.to_be_bytes()).p(0x1001, &batch(&[&uuid(comp)]));
|
||
s.0
|
||
}
|
||
|
||
fn source_clip(uid: u8, def: [u8; 16], dur: i64, pkg: [u8; 32], track: u32) -> Vec<u8> {
|
||
let mut s = LocalSet::new(uid);
|
||
s.p(0x0201, &def).p(0x0202, &dur.to_be_bytes()).p(0x1201, &0i64.to_be_bytes()).p(0x1101, &pkg).p(0x1102, &track.to_be_bytes());
|
||
s.0
|
||
}
|
||
|
||
/// An OP1a file: `n` frame-wrapped content packages, each a picture element of `pic(i)` and a
|
||
/// 16-bit stereo sound element of 1920 frames (sample k of the file = k, -k on the channels);
|
||
/// a footer index with temporal offsets `to` and flags `flags`.
|
||
fn build(n: usize, pic: impl Fn(usize) -> Vec<u8>, to: &[i8], flags: &[u8]) -> Vec<u8> {
|
||
let mut md = Vec::new();
|
||
// primer: no dynamic tags
|
||
klv(&mut md, ul(&[0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x05, 0x01, 0x00]), &batch(&[]));
|
||
let mut pre = LocalSet::new(1);
|
||
pre.p(0x3B03, &uuid(2)).p(0x3B09, &OP1A);
|
||
klv(&mut md, set_key(0x2F), &pre.0);
|
||
let mut cs = LocalSet::new(2);
|
||
cs.p(0x1901, &batch(&[&uuid(3), &uuid(4)]));
|
||
klv(&mut md, set_key(0x18), &cs.0);
|
||
// material package: timecode, picture, sound
|
||
let mut mp = LocalSet::new(3);
|
||
mp.p(0x4401, &umid(1)).p(0x4402, &[0, b'M', 0, b'P']).p(0x4403, &batch(&[&uuid(10), &uuid(11), &uuid(12)]));
|
||
klv(&mut md, set_key(0x36), &mp.0);
|
||
let d = n as i64;
|
||
klv(&mut md, set_key(0x3B), &track(10, 1, 0, 20));
|
||
klv(&mut md, set_key(0x0F), &sequence(20, TC_DEF, d, 30));
|
||
let mut tc = LocalSet::new(30);
|
||
tc.p(0x0201, &TC_DEF).p(0x0202, &d.to_be_bytes()).p(0x1501, &90_000i64.to_be_bytes()).p(0x1502, &25u16.to_be_bytes()).p(0x1503, &[0]);
|
||
klv(&mut md, set_key(0x14), &tc.0);
|
||
klv(&mut md, set_key(0x3B), &track(11, 2, 0, 21));
|
||
klv(&mut md, set_key(0x0F), &sequence(21, PICTURE_DEF, d, 31));
|
||
klv(&mut md, set_key(0x11), &source_clip(31, PICTURE_DEF, d, umid(2), 2));
|
||
klv(&mut md, set_key(0x3B), &track(12, 3, 0, 22));
|
||
klv(&mut md, set_key(0x0F), &sequence(22, SOUND_DEF, d, 32));
|
||
klv(&mut md, set_key(0x11), &source_clip(32, SOUND_DEF, d, umid(2), 3));
|
||
// file package
|
||
let mut fp = LocalSet::new(4);
|
||
fp.p(0x4401, &umid(2)).p(0x4403, &batch(&[&uuid(13), &uuid(14)])).p(0x4701, &uuid(40));
|
||
klv(&mut md, set_key(0x37), &fp.0);
|
||
klv(&mut md, set_key(0x3B), &track(13, 2, 0x1501_0500, 23));
|
||
klv(&mut md, set_key(0x0F), &sequence(23, PICTURE_DEF, d, 33));
|
||
klv(&mut md, set_key(0x11), &source_clip(33, PICTURE_DEF, d, [0; 32], 0));
|
||
klv(&mut md, set_key(0x3B), &track(14, 3, 0x1601_0300, 24));
|
||
klv(&mut md, set_key(0x0F), &sequence(24, SOUND_DEF, d, 34));
|
||
klv(&mut md, set_key(0x11), &source_clip(34, SOUND_DEF, d, [0; 32], 0));
|
||
let mut mdesc = LocalSet::new(40);
|
||
mdesc.p(0x3001, &[0, 0, 0, 25, 0, 0, 0, 1]).p(0x3F01, &batch(&[&uuid(41), &uuid(42)]));
|
||
klv(&mut md, set_key(0x44), &mdesc.0);
|
||
let mut cdci = LocalSet::new(41);
|
||
cdci.p(0x3006, &2u32.to_be_bytes())
|
||
.p(0x3203, &64u32.to_be_bytes())
|
||
.p(0x3202, &32u32.to_be_bytes())
|
||
.p(0x320E, &[0, 0, 0, 2, 0, 0, 0, 1])
|
||
.p(0x3301, &8u32.to_be_bytes())
|
||
.p(0x3201, &ul(&[0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0A, 0x04, 0x01, 0x02, 0x02, 0x03, 0x06, 0x03, 0x00]));
|
||
klv(&mut md, set_key(0x28), &cdci.0);
|
||
let mut wave = LocalSet::new(42);
|
||
wave.p(0x3006, &3u32.to_be_bytes())
|
||
.p(0x3D03, &[0, 0, 0xBB, 0x80, 0, 0, 0, 1])
|
||
.p(0x3D07, &2u32.to_be_bytes())
|
||
.p(0x3D01, &16u32.to_be_bytes())
|
||
.p(0x3D0A, &4u16.to_be_bytes());
|
||
klv(&mut md, set_key(0x48), &wave.0);
|
||
let mut ecd = LocalSet::new(5);
|
||
ecd.p(0x2701, &umid(2)).p(0x3F07, &1u32.to_be_bytes()).p(0x3F06, &2u32.to_be_bytes());
|
||
klv(&mut md, set_key(0x23), &ecd.0);
|
||
|
||
let mut f = partition(2, 0, 0, 0, md.len() as u64);
|
||
f.extend_from_slice(&md);
|
||
let body_at = f.len() as u64;
|
||
f.extend_from_slice(&partition(3, body_at, 1, 0, 0));
|
||
let mut offsets = Vec::new();
|
||
let mut cp_base = None;
|
||
for i in 0..n {
|
||
let at = f.len() as u64;
|
||
let base = *cp_base.get_or_insert(at);
|
||
offsets.push(at - base);
|
||
klv(&mut f, PIC_KEY, &pic(i));
|
||
let mut snd = Vec::new();
|
||
for k in 0..1920 {
|
||
let s = ((i * 1920 + k) % 30000) as i16;
|
||
snd.extend_from_slice(&s.to_le_bytes());
|
||
snd.extend_from_slice(&(-s).to_le_bytes());
|
||
}
|
||
klv(&mut f, SND_KEY, &snd);
|
||
}
|
||
let footer_at = f.len() as u64;
|
||
f.extend_from_slice(&partition(4, footer_at, 0, 2, 0));
|
||
let mut ix = LocalSet::new(6);
|
||
let mut entries = Vec::new();
|
||
for i in 0..n {
|
||
let mut e = vec![to.get(i).copied().unwrap_or(0) as u8, 0, flags.get(i).copied().unwrap_or(0)];
|
||
e.extend_from_slice(&offsets[i].to_be_bytes());
|
||
entries.push(e);
|
||
}
|
||
let refs: Vec<&[u8]> = entries.iter().map(Vec::as_slice).collect();
|
||
ix.p(0x3F0B, &[0, 0, 0, 25, 0, 0, 0, 1])
|
||
.p(0x3F0C, &0i64.to_be_bytes())
|
||
.p(0x3F0D, &(n as i64).to_be_bytes())
|
||
.p(0x3F06, &2u32.to_be_bytes())
|
||
.p(0x3F07, &1u32.to_be_bytes())
|
||
.p(0x3F0A, &batch(&refs));
|
||
klv(&mut f, ul(&[0x06, 0x0E, 0x2B, 0x34, 0x02, 0x53, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x10, 0x01, 0x00]), &ix.0);
|
||
f
|
||
}
|
||
|
||
/// Stored order I0 P3 B1 B2 P6 B4 B5: display d is stored at d + TO[d].
|
||
fn ipbb() -> (Vec<i8>, Vec<u8>) {
|
||
(vec![0, 1, 1, -2, 1, 1, -2], vec![0xC0, 0x22, 0x33, 0x33, 0x22, 0x33, 0x33])
|
||
}
|
||
|
||
fn prores_like(i: usize) -> Vec<u8> {
|
||
let mut v = vec![0, 0, 0, 40];
|
||
v.extend_from_slice(b"icpf");
|
||
v.extend(std::iter::repeat_n(i as u8, 32));
|
||
v
|
||
}
|
||
|
||
#[test]
|
||
fn opens_synthetic_op1a() {
|
||
let (to, flags) = ipbb();
|
||
let f = build(7, |i| vec![i as u8; 100 + i], &to, &flags);
|
||
let m = open(&f).unwrap();
|
||
assert_eq!(m.operational_pattern, OperationalPattern::Generalized { item: 1, package: 1 });
|
||
assert_eq!(m.operational_pattern.name(), "OP1a");
|
||
assert_eq!(m.material_package_name.as_deref(), Some("MP"));
|
||
let tc = m.timecode.unwrap();
|
||
assert_eq!((tc.start, tc.rounded_base, tc.drop_frame), (90_000, 25, false));
|
||
assert_eq!(tc.format(), "01:00:00:00");
|
||
let v = m.track_of_kind(TrackKind::Picture).unwrap();
|
||
let t = &m.tracks[v];
|
||
assert_eq!(t.track_number, 0x1501_0500);
|
||
assert_eq!(t.edit_rate, Rational::new(25, 1));
|
||
assert_eq!(t.duration, Some(7));
|
||
assert_eq!(t.samples.len(), 7);
|
||
let p = t.picture.as_ref().unwrap();
|
||
assert_eq!((p.frame_width(), p.frame_height()), (64, 32));
|
||
assert_eq!(t.codec, Codec::ProRes { profile: Some(3) }, "coding label wins (the bytes do not sniff as anything)");
|
||
// presentation order from the temporal offsets
|
||
let pts: Vec<i64> = t.samples.iter().map(|s| s.pts).collect();
|
||
assert_eq!(pts, vec![0, 3, 1, 2, 6, 4, 5]);
|
||
assert_eq!(t.display_order, vec![0, 2, 3, 1, 5, 6, 4]);
|
||
assert!(t.temporal_offsets);
|
||
assert_eq!(t.sample_at(3), Some(1));
|
||
// the index marks only the first picture random-access (ProRes would be intra; this label says ProRes so all are key)
|
||
assert!(t.samples.iter().all(|s| s.key));
|
||
// sample bytes
|
||
for i in 0..7 {
|
||
assert_eq!(m.read_sample(&f, v, i).unwrap(), vec![i as u8; 100 + i]);
|
||
}
|
||
// sound: 7 × 1920 frames, sample k = k on the left, -k on the right
|
||
let a = m.track_of_kind(TrackKind::Sound).unwrap();
|
||
let s = &m.tracks[a];
|
||
assert_eq!(s.codec, Codec::Pcm);
|
||
assert_eq!(s.stored_sample_frames(), 7 * 1920);
|
||
assert_eq!(s.samples_per_edit_unit(), (48000, 25));
|
||
let pcm = m.read_pcm(&f, a, 1900, 50).unwrap();
|
||
for k in 0..50 {
|
||
assert_eq!(pcm[0][k], (1900 + k) as f32 / 32768.0);
|
||
assert_eq!(pcm[1][k], -((1900 + k) as f32) / 32768.0);
|
||
}
|
||
// past the end: zeros
|
||
let tail = m.read_pcm(&f, a, 7 * 1920 - 2, 4).unwrap();
|
||
assert_eq!(tail[0][2..], [0.0, 0.0]);
|
||
}
|
||
|
||
#[test]
|
||
fn key_frames_from_index_flags_for_long_gop() {
|
||
let (to, flags) = ipbb();
|
||
// AVC-looking essence (start code + AUD) and no coding label: sniffed as AVC
|
||
let f = build(7, |i| vec![0, 0, 0, 1, 9, 0x10, i as u8], &to, &flags);
|
||
// replace the ProRes coding label so the codec comes from the bytes
|
||
let mut f2 = f.clone();
|
||
let lbl = [0x04, 0x01, 0x02, 0x02, 0x03, 0x06, 0x03, 0x00];
|
||
let at = f2.windows(8).position(|w| w == lbl).unwrap();
|
||
f2[at..at + 8].copy_from_slice(&[0x04, 0x01, 0x02, 0x02, 0x01, 0x31, 0x40, 0x01]);
|
||
let m = open(&f2).unwrap();
|
||
let t = &m.tracks[m.track_of_kind(TrackKind::Picture).unwrap()];
|
||
assert_eq!(t.codec, Codec::Avc { intra: false });
|
||
let keys: Vec<bool> = t.samples.iter().map(|s| s.key).collect();
|
||
assert_eq!(keys, vec![true, false, false, false, false, false, false]);
|
||
assert_eq!(t.sync_before(5), 0);
|
||
assert!(t.indexed);
|
||
let b: Vec<bool> = t.samples.iter().map(|s| s.b_picture).collect();
|
||
assert_eq!(b, vec![false, false, true, true, false, true, true]);
|
||
}
|
||
|
||
#[test]
|
||
fn missing_temporal_offsets_with_b_pictures_ask_for_reordering() {
|
||
let (_, flags) = ipbb();
|
||
let m = open(&build(7, prores_like, &[], &flags)).unwrap();
|
||
let t = &m.tracks[0];
|
||
assert!(!t.temporal_offsets);
|
||
// ProRes is intra-only: the B flags are ignored for key frames, reordering still reported
|
||
assert!(t.samples.iter().all(|s| s.key));
|
||
}
|
||
|
||
#[test]
|
||
fn not_mxf_and_empty() {
|
||
assert!(matches!(open(&b"RIFF....WAVE".to_vec()), Err(Error::NotMxf(_))));
|
||
assert!(open(&Vec::new()).is_err());
|
||
assert!(!sniff(b"\x1aE\xdf\xa3"));
|
||
}
|
||
|
||
#[test]
|
||
fn truncation_never_panics_and_keeps_whole_frames() {
|
||
let (to, flags) = ipbb();
|
||
let f = build(7, prores_like, &to, &flags);
|
||
let mut opened = 0;
|
||
for cut in (0..f.len()).step_by(37) {
|
||
let part = f[..cut].to_vec();
|
||
if let Ok(m) = open(&part) {
|
||
opened += 1;
|
||
for (ti, t) in m.tracks.iter().enumerate() {
|
||
for i in 0..t.samples.len() {
|
||
// every listed sample is fully inside the file
|
||
assert!(m.read_sample(&part, ti, i).is_ok(), "cut {cut} track {ti} sample {i}");
|
||
}
|
||
if t.sound.is_some() {
|
||
let _ = m.read_pcm(&part, ti, 0, 5000);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
assert!(opened > 10, "truncated files with complete metadata still open ({opened})");
|
||
}
|
||
|
||
#[test]
|
||
fn mutation_never_panics() {
|
||
let (to, flags) = ipbb();
|
||
let f = build(5, prores_like, &to, &flags);
|
||
let mut seed = 0x1234_5678u64;
|
||
for _ in 0..600 {
|
||
let mut g = f.clone();
|
||
for _ in 0..4 {
|
||
seed ^= seed << 13;
|
||
seed ^= seed >> 7;
|
||
seed ^= seed << 17;
|
||
let at = (seed as usize) % g.len();
|
||
g[at] = (seed >> 32) as u8;
|
||
}
|
||
if let Ok(m) = open(&g) {
|
||
for (ti, t) in m.tracks.iter().enumerate() {
|
||
for i in 0..t.samples.len().min(8) {
|
||
let _ = m.read_sample(&g, ti, i);
|
||
}
|
||
let _ = m.read_pcm(&g, ti, 0, 100);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn drop_frame_timecode_format() {
|
||
let tc = Timecode { start: 107_892, rounded_base: 30, drop_frame: true };
|
||
assert_eq!(tc.format(), "01:00:00;00");
|
||
let tc = Timecode { start: 1800, rounded_base: 30, drop_frame: true };
|
||
assert_eq!(tc.format(), "00:01:00;02");
|
||
let tc = Timecode { start: 90_000 + 25 * 61 + 3, rounded_base: 25, drop_frame: false };
|
||
assert_eq!(tc.format(), "01:01:01:03");
|
||
}
|
||
|
||
#[test]
|
||
fn hostile_pcm_requests_fail_before_allocation() {
|
||
let (to, flags) = ipbb();
|
||
let bytes = build(5, prores_like, &to, &flags);
|
||
let mut file = open(&bytes).unwrap();
|
||
let track = file.track_of_kind(TrackKind::Sound).unwrap();
|
||
file.tracks[track].sound.as_mut().unwrap().channels = u32::MAX;
|
||
assert!(file.read_pcm(&bytes, track, 0, 100).is_err());
|
||
file.tracks[track].sound.as_mut().unwrap().channels = 2;
|
||
file.tracks[track].sound.as_mut().unwrap().block_align = u32::MAX;
|
||
assert!(file.read_pcm(&bytes, track, 0, 100).is_err());
|
||
assert!(file.read_pcm(&bytes, track, 0, usize::MAX).is_err());
|
||
assert!(crate::decode_pcm(&[], SoundFormat::Pcm, usize::MAX, 16, 4).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn a_corrupt_index_position_cannot_allocate_a_giant_sparse_table() {
|
||
let (to, flags) = ipbb();
|
||
let mut bytes = build(5, prores_like, &to, &flags);
|
||
let prefix = [0x3f, 0x0c, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0];
|
||
let at = bytes.windows(prefix.len()).position(|w| w == prefix).unwrap();
|
||
bytes[at + 4..at + 12].copy_from_slice(&49_999_999i64.to_be_bytes());
|
||
let file = open(&bytes).unwrap();
|
||
assert_eq!(file.tracks[file.track_of_kind(TrackKind::Picture).unwrap()].samples.len(), 5);
|
||
}
|