1mod fields;
33mod fixup;
34mod tags;
35mod tree;
36
37pub use fixup::FixupStats;
38pub use tree::{EditNode, EditTree, HeaderForm, Payload};
39
40use crate::boxes::FourCC;
41use crate::parser::parse_boxes;
42use std::io::{Read, Seek, SeekFrom, Write};
43
44pub enum Command {
48 Remove { path: String },
50 RemoveAll { fourcc: String },
52 Insert {
55 parent: String,
56 bytes: Vec<u8>,
57 position: Option<usize>,
58 },
59 Replace { path: String, bytes: Vec<u8> },
61 Set {
65 path: String,
66 field: String,
67 value: String,
68 },
69 SetTag { tag: String, value: String },
72 Faststart,
82}
83
84#[derive(Debug, Default)]
86pub struct EditStats {
87 pub bytes_written: u64,
88 pub chunk_offsets_adjusted: usize,
89 pub chunk_offsets_unmapped: usize,
91}
92
93#[derive(Default)]
95pub struct Editor {
96 commands: Vec<Command>,
97}
98
99impl Editor {
100 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn add_command(&mut self, cmd: Command) -> &mut Self {
105 self.commands.push(cmd);
106 self
107 }
108
109 pub fn remove(&mut self, path: impl Into<String>) -> &mut Self {
111 self.add_command(Command::Remove { path: path.into() })
112 }
113
114 pub fn remove_all(&mut self, fourcc: impl Into<String>) -> &mut Self {
116 self.add_command(Command::RemoveAll {
117 fourcc: fourcc.into(),
118 })
119 }
120
121 pub fn set_field(
123 &mut self,
124 path: impl Into<String>,
125 field: impl Into<String>,
126 value: impl Into<String>,
127 ) -> &mut Self {
128 self.add_command(Command::Set {
129 path: path.into(),
130 field: field.into(),
131 value: value.into(),
132 })
133 }
134
135 pub fn faststart(&mut self) -> &mut Self {
137 self.add_command(Command::Faststart)
138 }
139
140 pub fn set_tag(
142 &mut self,
143 tag: impl Into<String>,
144 value: impl Into<String>,
145 ) -> anyhow::Result<&mut Self> {
146 let tag = tag.into();
147 tags::tag_fourcc(&tag)?; self.add_command(Command::SetTag {
149 tag,
150 value: value.into(),
151 });
152 Ok(self)
153 }
154
155 pub fn process<R: Read + Seek, W: Write>(
158 &self,
159 src: &mut R,
160 dst: &mut W,
161 ) -> anyhow::Result<EditStats> {
162 let file_len = src.seek(SeekFrom::End(0))?;
163 let boxes = parse_boxes(src, 0, file_len)?;
164 let mut edit_tree = tree::build_tree(src, &boxes, file_len)?;
165
166 if !self.commands.is_empty() {
167 guard_unsupported(&edit_tree)?;
168 }
169
170 for cmd in &self.commands {
171 apply_command(src, &mut edit_tree, cmd)?;
172 }
173
174 let map = tree::layout(&edit_tree);
176
177 let moved = map.iter().any(|m| m.new_offset != m.old_offset);
179 let fixup_stats = if moved {
180 fixup::fix_chunk_offsets(src, &mut edit_tree, &map)?
181 } else {
182 FixupStats::default()
183 };
184
185 let bytes_written = tree::write_tree(src, &edit_tree, dst)?;
186
187 Ok(EditStats {
188 bytes_written,
189 chunk_offsets_adjusted: fixup_stats.entries_adjusted,
190 chunk_offsets_unmapped: fixup_stats.entries_unmapped,
191 })
192 }
193
194 pub fn process_file(
197 &self,
198 input: impl AsRef<std::path::Path>,
199 output: impl AsRef<std::path::Path>,
200 ) -> anyhow::Result<EditStats> {
201 let input = input.as_ref();
202 let output = output.as_ref();
203 anyhow::ensure!(
204 input != output,
205 "in-place editing is not supported; choose a different output path"
206 );
207 let mut src = std::fs::File::open(input)?;
208 let mut dst = std::io::BufWriter::new(std::fs::File::create(output)?);
209 let stats = self.process(&mut src, &mut dst)?;
210 std::io::Write::flush(&mut dst)?;
211 Ok(stats)
212 }
213}
214
215fn guard_unsupported(tree: &EditTree) -> anyhow::Result<()> {
217 fn scan(nodes: &[EditNode]) -> Option<&'static str> {
218 for n in nodes {
219 match &n.typ.0 {
220 b"moof" => return Some("fragmented MP4 (moof)"),
221 b"sidx" => return Some("indexed segments (sidx)"),
222 b"iloc" => return Some("HEIF item locations (iloc)"),
223 _ => {}
224 }
225 if let Some(kids) = n.children()
226 && let Some(hit) = scan(kids)
227 {
228 return Some(hit);
229 }
230 }
231 None
232 }
233 if let Some(kind) = scan(&tree.roots) {
234 anyhow::bail!(
235 "editing is not supported for {} yet: byte offsets inside these \
236 structures are not fixed up, and editing would corrupt the file",
237 kind
238 );
239 }
240 Ok(())
241}
242
243fn apply_command<R: Read + Seek>(
246 src: &mut R,
247 tree: &mut EditTree,
248 cmd: &Command,
249) -> anyhow::Result<()> {
250 match cmd {
251 Command::Remove { path } => {
252 let (siblings, idx) = resolve_parent_mut(&mut tree.roots, path)?;
253 siblings.remove(idx);
254 }
255
256 Command::RemoveAll { fourcc } => {
257 let cc = seg_fourcc(fourcc)?;
258 remove_all(&mut tree.roots, &cc);
259 }
260
261 Command::Insert {
262 parent,
263 bytes,
264 position,
265 } => {
266 let node = EditNode::from_raw(bytes)?;
267 let target = resolve_node_mut(&mut tree.roots, parent)?;
268 let children = target
269 .children_mut()
270 .ok_or_else(|| anyhow::anyhow!("'{}' is not a container", parent))?;
271 let at = position.unwrap_or(children.len()).min(children.len());
272 children.insert(at, node);
273 }
274
275 Command::Replace { path, bytes } => {
276 let node = EditNode::from_raw(bytes)?;
277 let (siblings, idx) = resolve_parent_mut(&mut tree.roots, path)?;
278 siblings[idx] = node;
279 }
280
281 Command::Set { path, field, value } => {
282 let node = resolve_node_mut(&mut tree.roots, path)?;
283 set_field_on_node(src, node, field, value)?;
284 }
285
286 Command::SetTag { tag, value } => {
287 let cc = tags::tag_fourcc(tag)?;
288 let moov = tree
289 .roots
290 .iter_mut()
291 .find(|n| &n.typ.0 == b"moov")
292 .ok_or_else(|| anyhow::anyhow!("no moov box: cannot set tags"))?;
293 tags::set_tag_in_moov(moov, &cc, value)?;
294 }
295
296 Command::Faststart => {
297 let moov_idx = tree
298 .roots
299 .iter()
300 .position(|n| &n.typ.0 == b"moov")
301 .ok_or_else(|| anyhow::anyhow!("no moov box: cannot faststart"))?;
302 let Some(mdat_idx) = tree.roots.iter().position(|n| &n.typ.0 == b"mdat") else {
303 return Ok(()); };
305 if moov_idx < mdat_idx {
306 return Ok(()); }
308 let moov = tree.roots.remove(moov_idx);
309 let at = usize::from(tree.roots.first().is_some_and(|n| &n.typ.0 == b"ftyp"));
312 tree.roots.insert(at, moov);
313 }
314 }
315 Ok(())
316}
317
318fn set_field_on_node<R: Read + Seek>(
319 src: &mut R,
320 node: &mut EditNode,
321 field: &str,
322 value: &str,
323) -> anyhow::Result<()> {
324 let mut payload = match &node.payload {
326 Payload::Bytes(b) => b.clone(),
327 Payload::Extent(e) => {
328 let mut buf = vec![0u8; e.len as usize];
329 src.seek(SeekFrom::Start(e.offset))?;
330 src.read_exact(&mut buf)?;
331 buf
332 }
333 Payload::Container { .. } => {
334 anyhow::bail!("'{}' is a container; --set applies to leaf boxes", node.typ)
335 }
336 };
337 anyhow::ensure!(!payload.is_empty(), "'{}' has an empty payload", node.typ);
338
339 let version = payload[0];
340 let (offset, kind) = fields::field_spec(&node.typ.0, version, field).ok_or_else(|| {
341 anyhow::anyhow!(
342 "no known field '{}' in '{}' (version {})",
343 field,
344 node.typ,
345 version
346 )
347 })?;
348 fields::patch_field(&mut payload, offset, kind, value)?;
349 node.payload = Payload::Bytes(payload);
350 Ok(())
351}
352
353fn remove_all(nodes: &mut Vec<EditNode>, cc: &FourCC) {
354 nodes.retain(|n| &n.typ != cc);
355 for n in nodes {
356 if let Some(kids) = n.children_mut() {
357 remove_all(kids, cc);
358 }
359 }
360}
361
362fn parse_segment(seg: &str) -> anyhow::Result<(FourCC, usize)> {
366 if let Some(open) = seg.find('[') {
367 let close = seg
368 .rfind(']')
369 .ok_or_else(|| anyhow::anyhow!("unclosed '[' in path segment '{}'", seg))?;
370 let idx: usize = seg[open + 1..close]
371 .parse()
372 .map_err(|_| anyhow::anyhow!("bad index in path segment '{}'", seg))?;
373 Ok((seg_fourcc(&seg[..open])?, idx))
374 } else {
375 Ok((seg_fourcc(seg)?, 0))
376 }
377}
378
379fn seg_fourcc(seg: &str) -> anyhow::Result<FourCC> {
382 let mut bytes = Vec::with_capacity(4);
383 for ch in seg.chars() {
384 if ch == '©' {
385 bytes.push(0xA9);
386 } else {
387 anyhow::ensure!(ch.is_ascii(), "invalid character in fourcc '{}'", seg);
388 bytes.push(ch as u8);
389 }
390 }
391 anyhow::ensure!(bytes.len() == 4, "'{}' is not a 4-character box type", seg);
392 Ok(FourCC(bytes.try_into().unwrap()))
393}
394
395fn find_child_idx(nodes: &[EditNode], cc: &FourCC, nth: usize) -> Option<usize> {
396 nodes
397 .iter()
398 .enumerate()
399 .filter(|(_, n)| &n.typ == cc)
400 .map(|(i, _)| i)
401 .nth(nth)
402}
403
404fn resolve_node_mut<'a>(
406 roots: &'a mut Vec<EditNode>,
407 path: &str,
408) -> anyhow::Result<&'a mut EditNode> {
409 let (siblings, idx) = resolve_parent_mut(roots, path)?;
410 Ok(&mut siblings[idx])
411}
412
413fn resolve_parent_mut<'a>(
416 roots: &'a mut Vec<EditNode>,
417 path: &str,
418) -> anyhow::Result<(&'a mut Vec<EditNode>, usize)> {
419 anyhow::ensure!(!path.is_empty(), "empty box path");
420 let segments: Vec<&str> = path.split('/').collect();
421
422 let mut current: &'a mut Vec<EditNode> = roots;
423 for (depth, seg) in segments.iter().enumerate() {
424 let (cc, nth) = parse_segment(seg)?;
425 let idx = find_child_idx(current, &cc, nth)
426 .ok_or_else(|| anyhow::anyhow!("box '{}' not found (in path '{}')", seg, path))?;
427
428 if depth == segments.len() - 1 {
429 return Ok((current, idx));
430 }
431
432 current = current[idx]
433 .children_mut()
434 .ok_or_else(|| anyhow::anyhow!("'{}' has no children (in path '{}')", seg, path))?;
435 }
436 unreachable!("loop always returns on the last segment");
437}