tauri-plugin-deeprespond-im 1.0.0-alpha

深应智能 IM 的 Tauri 插件:本地存储、安全存储、窗口间的锁与消息、系统事件、通知、角标、文件
//! 视频(MP4、MOV)的上传前处理:只读盒子的头部(按偏移跳着读,不读整个文件),读出时长和宽高,
//! 找出要去掉的元数据盒子。规则与 Web SDK 的 `scrubVideo`(sdk/web/src/modules/files/sniff.ts)相同:
//! moov 和 trak 下的 udta、meta,顶层的 XMP uuid 盒子,把类型字段(盒子开头偏移 4 处)在上传时改写为 `free`。
//! 文件本身不改动:用户的原文件只读不写,改写在上传的数据流中进行(transfer.rs 的 patches)。

use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;

use serde::Serialize;

use crate::error::{Error, Result};

const XMP_UUID: [u8; 16] = [0xbe, 0x7a, 0xcf, 0xcb, 0x97, 0xa9, 0x42, 0xe8, 0x9c, 0x71, 0x99, 0x94, 0x91, 0xe3, 0xaf, 0xac];
/// 最多看多少个盒子:损坏或恶意的文件可能由大量很小的盒子组成,不为它读上百万次。
const MAX_BOXES: usize = 100_000;

#[derive(Debug, Default, Serialize, PartialEq)]
pub struct VideoMeta {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub width: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub height: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration_seconds: Option<u64>,
    /// 要改写为 free 的类型字段在文件中的偏移。
    pub patches: Vec<u64>,
}

struct Walker {
    file: File,
    meta: VideoMeta,
    boxes: usize,
}

impl Walker {
    fn read_at(&mut self, pos: u64, buf: &mut [u8]) -> bool {
        self.file.seek(SeekFrom::Start(pos)).is_ok() && self.file.read_exact(buf).is_ok()
    }

    fn u32_at(&mut self, pos: u64) -> Option<u32> {
        let mut b = [0u8; 4];
        self.read_at(pos, &mut b).then(|| u32::from_be_bytes(b))
    }

    fn u64_at(&mut self, pos: u64) -> Option<u64> {
        let mut b = [0u8; 8];
        self.read_at(pos, &mut b).then(|| u64::from_be_bytes(b))
    }

    fn walk(&mut self, start: u64, end: u64, depth: u32, parent: &[u8; 4]) {
        let mut i = start;
        while i + 8 <= end {
            self.boxes += 1;
            if self.boxes > MAX_BOXES {
                return;
            }
            let mut head = [0u8; 8];
            if !self.read_at(i, &mut head) {
                return;
            }
            let mut size = u32::from_be_bytes([head[0], head[1], head[2], head[3]]) as u64;
            let kind: [u8; 4] = [head[4], head[5], head[6], head[7]];
            let mut header = 8u64;
            if size == 1 {
                let Some(large) = self.u64_at(i + 8) else { return };
                size = large;
                header = 16;
            } else if size == 0 {
                size = end - i;
            }
            if size < header || i.checked_add(size).is_none_or(|e| e > end) {
                return;
            }
            if depth == 0 && &kind == b"uuid" {
                let mut id = [0u8; 16];
                if self.read_at(i + header, &mut id) && id == XMP_UUID {
                    self.meta.patches.push(i + 4);
                }
            } else if (parent == b"moov" || parent == b"trak") && (&kind == b"udta" || &kind == b"meta") {
                self.meta.patches.push(i + 4);
            } else if &kind == b"mvhd" {
                self.mvhd(i + header);
            } else if &kind == b"tkhd" && self.meta.width.is_none() {
                self.tkhd(i + header);
            }
            if &kind == b"moov" || &kind == b"trak" {
                self.walk(i + header, i + size, depth + 1, &kind);
            }
            i += size;
        }
    }

    /// mvhd:版本 1 的时间刻度在 +20、时长(64 位)在 +24;版本 0 的在 +12、+16。
    fn mvhd(&mut self, body: u64) {
        let mut version = [0u8; 1];
        if !self.read_at(body, &mut version) {
            return;
        }
        let (timescale, duration) = if version[0] == 1 {
            (self.u32_at(body + 20), self.u64_at(body + 24))
        } else {
            (self.u32_at(body + 12), self.u32_at(body + 16).map(u64::from))
        };
        if let (Some(timescale), Some(duration)) = (timescale, duration) {
            if timescale > 0 {
                self.meta.duration_seconds = Some((duration as f64 / timescale as f64).round().max(0.0) as u64);
            }
        }
    }

    /// tkhd:宽高为 16.16 定点数,版本 1 在 +88,版本 0 在 +76。
    fn tkhd(&mut self, body: u64) {
        let mut version = [0u8; 1];
        if !self.read_at(body, &mut version) {
            return;
        }
        let off = body + if version[0] == 1 { 88 } else { 76 };
        if let (Some(w), Some(h)) = (self.u32_at(off), self.u32_at(off + 4)) {
            let (w, h) = (w >> 16, h >> 16);
            if w > 0 && h > 0 {
                self.meta.width = Some(w);
                self.meta.height = Some(h);
            }
        }
    }
}

/// 读出视频的宽高、时长和要改写的盒子(files_prepare_video)。
pub fn prepare_video(real: &Path) -> Result<VideoMeta> {
    let file = File::open(real).map_err(|e| Error::from_io_read(&e))?;
    let size = file.metadata().map_err(|e| Error::from_io_read(&e))?.len();
    let mut walker = Walker { file, meta: VideoMeta::default(), boxes: 0 };
    walker.walk(0, size, 0, b"    ");
    Ok(walker.meta)
}

#[cfg(test)]
pub(crate) mod tests {
    use super::*;

    pub fn mp4_box(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
        let mut out = ((body.len() + 8) as u32).to_be_bytes().to_vec();
        out.extend_from_slice(kind);
        out.extend_from_slice(body);
        out
    }

    /// 合成的小 MP4:ftyp、XMP uuid、moov(mvhd、trak(tkhd、udta)、udta、meta)、mdat。返回文件内容和应当改写的偏移。
    pub fn synthetic_mp4() -> (Vec<u8>, Vec<u64>) {
        let ftyp = mp4_box(b"ftyp", b"isom\0\0\0\x01isommp41");
        let mut xmp_body = XMP_UUID.to_vec();
        xmp_body.extend_from_slice(b"<x:xmpmeta GPS/>");
        let xmp = mp4_box(b"uuid", &xmp_body);
        // 普通的 uuid 盒子不动
        let other_uuid = mp4_box(b"uuid", &[7u8; 20]);
        let mut mvhd = vec![0u8; 100];
        mvhd[12..16].copy_from_slice(&1000u32.to_be_bytes());
        mvhd[16..20].copy_from_slice(&5400u32.to_be_bytes());
        let mvhd = mp4_box(b"mvhd", &mvhd);
        let mut tkhd = vec![0u8; 84];
        tkhd[76..80].copy_from_slice(&(640u32 << 16).to_be_bytes());
        tkhd[80..84].copy_from_slice(&(360u32 << 16).to_be_bytes());
        let tkhd = mp4_box(b"tkhd", &tkhd);
        let trak_udta = mp4_box(b"udta", b"\xa9xyz+39.9-116.4/");
        let mut trak_body = tkhd.clone();
        trak_body.extend_from_slice(&trak_udta);
        let trak = mp4_box(b"trak", &trak_body);
        let moov_udta = mp4_box(b"udta", b"location");
        let moov_meta = mp4_box(b"meta", b"\0\0\0\0hdlr");
        let mut moov_body = mvhd.clone();
        moov_body.extend_from_slice(&trak);
        moov_body.extend_from_slice(&moov_udta);
        moov_body.extend_from_slice(&moov_meta);
        let moov = mp4_box(b"moov", &moov_body);
        let mdat = mp4_box(b"mdat", &[0x55u8; 3000]);

        let mut file = Vec::new();
        file.extend_from_slice(&ftyp);
        let xmp_at = file.len() as u64;
        file.extend_from_slice(&xmp);
        file.extend_from_slice(&other_uuid);
        let moov_at = file.len() as u64;
        let trak_at = moov_at + 8 + mvhd.len() as u64;
        let trak_udta_at = trak_at + 8 + tkhd.len() as u64;
        let moov_udta_at = trak_at + trak.len() as u64;
        let moov_meta_at = moov_udta_at + moov_udta.len() as u64;
        file.extend_from_slice(&moov);
        file.extend_from_slice(&mdat);
        (file, vec![xmp_at + 4, trak_udta_at + 4, moov_udta_at + 4, moov_meta_at + 4])
    }

    #[test]
    fn walks_boxes_like_scrub_video() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("a.mp4");
        let (bytes, expected) = synthetic_mp4();
        std::fs::write(&path, &bytes).unwrap();
        let meta = prepare_video(&path).unwrap();
        assert_eq!((meta.width, meta.height, meta.duration_seconds), (Some(640), Some(360), Some(5)));
        let mut patches = meta.patches.clone();
        patches.sort();
        let mut expected = expected;
        expected.sort();
        assert_eq!(patches, expected);
        for p in &patches {
            let kind = &bytes[*p as usize..*p as usize + 4];
            assert!(kind == b"udta" || kind == b"meta" || kind == b"uuid");
        }
        // 文件没有被改动
        assert_eq!(std::fs::read(&path).unwrap(), bytes);
    }

    #[test]
    fn broken_box_stops_walk() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("b.mp4");
        // 第二个盒子声明的大小超出文件:停止,已读出的照常返回
        let mut bytes = mp4_box(b"ftyp", b"isom");
        bytes.extend_from_slice(&1000u32.to_be_bytes());
        bytes.extend_from_slice(b"moov");
        std::fs::write(&path, &bytes).unwrap();
        assert_eq!(prepare_video(&path).unwrap(), VideoMeta::default());
        // 64 位大小的盒子
        let mut large = 1u32.to_be_bytes().to_vec();
        large.extend_from_slice(b"mdat");
        large.extend_from_slice(&24u64.to_be_bytes());
        large.extend_from_slice(&[0u8; 8]);
        large.extend_from_slice(&mp4_box(b"udta", b"x"));
        std::fs::write(&path, &large).unwrap();
        assert!(prepare_video(&path).unwrap().patches.is_empty());
    }
}