// 脱敏策略与执行。
//
// 读—改—写走同一套 IR:`redact` 只删条目,不碰没被策略点名的任何东西,
// 所以"删掉 GPS 之后照片还能不能看、颜色对不对"这件事由构造保证,
// 不由运气保证。
///|
/// 哪些类别要清除。每一类对应 `sensitive_category` 的一个返回值。
///
/// 默认策略 `privacy` 保留时间戳:拍摄日期对相册、排序、法律证据都太常用,
/// 而它单独存在时定位不到人。要连时间一起清掉用 `strict`。
pub(all) struct Policy {
location : Bool
identity : Bool
device_id : Bool
maker_note : Bool
comments : Bool
timestamps : Bool
/// 整包载体:JPEG 的 APP1/XMP 段、PNG 的 XMP 文本块,以及 IFD0 里的
/// XMP / Photoshop / IPTC-NAA 条目。
///
/// 单独一个开关,是因为它管的不是"某一条敏不敏感",而是"这张图还有第二份元数据"。
/// EXIF 里的 DateTimeOriginal 删掉、XMP 里那份原样留着,等于没删——
/// 而报告上看不出任何异常。99 张真实照片里有 34 张带 XMP 包。
carrier : Bool
} derive(Debug, Eq)
///|
pub extend Policy with Eq::{not_equal, equal}
///|
pub extend Policy with @debug.Debug::{to_repr}
///|
/// 常规发布用:坐标、署名、序列号、厂商私有块、备注,以及整包 XMP。
///
/// XMP 默认也删:它是同一批事实的第二份副本,留着就等于没删。
pub fn privacy_policy() -> Policy {
{
location: true,
identity: true,
device_id: true,
maker_note: true,
comments: true,
timestamps: false,
carrier: true,
}
}
///|
/// 对外交付证据前的最强档:连时间戳一起清。
pub fn strict_policy() -> Policy {
{
location: true,
identity: true,
device_id: true,
maker_note: true,
comments: true,
timestamps: true,
carrier: true,
}
}
///|
/// 什么都不清,只用来做只读审计的对照。
pub fn empty_policy() -> Policy {
{
location: false,
identity: false,
device_id: false,
maker_note: false,
comments: false,
timestamps: false,
carrier: false,
}
}
///|
/// 策略是否覆盖某一类。
///
/// 这里刻意不写 `_ => false`:新增一类敏感信息时,编译要在这里停下来,
/// 否则默认行为是"不删",而"不删"在脱敏库里等于泄露。
pub fn Policy::covers(self : Policy, category : Sensitive) -> Bool {
match category {
Location => self.location
Identity => self.identity
DeviceId => self.device_id
MakerNote => self.maker_note
Comments => self.comments
Timestamps => self.timestamps
Carrier => self.carrier
}
}
///|
/// 一条被点名(或将被删除)的条目。
pub(all) struct Removed {
dir : Dir
tag : Int
name : String
category : Sensitive
} derive(Debug, Eq)
///|
pub extend Removed with Eq::{not_equal, equal}
///|
pub extend Removed with @debug.Debug::{to_repr}
///|
/// 一次脱敏的交代:删了什么、有没有连带丢掉缩略图。
///
/// `dropped_thumbnail` 为真表示原文件的 APP1 里挂着缩略图链,
/// 回写时它被整条丢掉——缩略图常常自带一份 GPS,
/// "删了主 GPS 却留着缩略图"是最常见的假脱敏。
///
/// 缩略图消失还有第二条路,都得算进同一个旗标:IFD0 里单挂着
/// `0x0201`/`0x0202` 这一对指针、却没有 IFD1 链的文件(语料
/// `exif_gps.jpg` 就是这样,而且它声明的长度是 `0xffffffff`)。
/// 回写时这两个条目一定没了——它们指的字节不会被重发,留着就是悬空引用。
/// 少了这一半,报告上就看不出"顺手没了两条",而这正是脱敏工具最不该有
/// 的那种沉默。
pub(all) struct Redaction {
removed : Array[Removed]
dropped_thumbnail : Bool
/// 容器里的 XMP 包有没有被整段摘掉。
///
/// 和 `dropped_thumbnail` 一样,这一项没法用 `removed` 里的一条记录表达:
/// XMP 不归 IFD 管,条目清单里根本没有它。
/// 那个 Bool 同样要区分"丢了"和"本来就没有"。
dropped_xmp : Bool
/// JPEG 的 APP13 里那包 Photoshop 资源(IPTC-IIM 住在里面)有没有被整段摘掉。
///
/// 与 `dropped_xmp` 同构、同一条规矩:成包的载体进不了逐条清单,
/// 所以"丢了"与"本来就没有"必须由单独的字段交代。
/// 裸 TIFF 的 IPTC 是 IFD0 里的一条(`0x83bb`),它本来就在 `removed` 里,
/// 所以这一项只对 JPEG 容器为真。
dropped_iptc : Bool
} derive(Debug, Eq)
///|
pub extend Redaction with Eq::{not_equal, equal}
///|
pub extend Redaction with @debug.Debug::{to_repr}
///|
/// 只报告,不修改:把会被删的东西先给用户看一遍。
pub fn TiffBlock::findings(self : TiffBlock, p : Policy) -> Array[Removed] {
let out : Array[Removed] = []
collect(self.ifd0, Ifd0, p, out)
collect(self.exif, Exif, p, out)
collect(self.gps, Gps, p, out)
collect(self.interop, Interop, p, out)
out
}
///|
fn collect(
entries : Array[IfdEntry],
dir : Dir,
p : Policy,
out : Array[Removed],
) -> Unit {
for e in entries {
match sensitive_category(dir, e.tag) {
Some(cat) =>
if p.covers(cat) {
out.push({
dir,
tag: e.tag,
name: tag_name(dir, e.tag),
category: cat,
})
}
None => ()
}
}
}
///|
/// 执行脱敏,同时交出报告。
pub fn TiffBlock::redact(
self : TiffBlock,
p : Policy,
) -> (TiffBlock, Redaction) {
let report = self.findings(p)
let block = {
little: self.little,
ifd0: keep_entries(self.ifd0, Ifd0, p),
exif: keep_entries(self.exif, Exif, p),
gps: keep_entries(self.gps, Gps, p),
interop: keep_entries(self.interop, Interop, p),
has_ifd1: false,
}
// 裸 TIFF 里 XMP 是 IFD0 的一条(0x02bc),已经被上面的按条过滤处理了;
// 这里的 dropped_xmp 只说容器里那种成包的 XMP,dropped_iptc 同理(那是
// JPEG 的 APP13 那一种),裸 TIFF 的 IPTC 走的是 0x83bb 那一条。
//
// 缩略图有两条消失的路,都要算进 dropped_thumbnail:
// 一条是 IFD1 链(回写时整条不重发),另一条是 IFD0 里的
// 0x0201/0x0202 这一对指针(moonmeta_encode.mbt 的 entries_of 一定把它们
// 摘掉——它们指的字节不会被重发,留着就是悬空引用)。
// 第二条只在**真的重写**时成立:一条都没删时容器层原样返回输入,
// 什么也没丢,这时候报"丢了缩略图"就是谎话,而且会逼着那次
// 本不该发生的重写发生(空策略必须是恒等)。
let thumb_ptrs = find_entry(self.ifd0, tag_thumb_offset) != None ||
find_entry(self.ifd0, tag_thumb_length) != None
(
block,
{
removed: report,
dropped_thumbnail: self.has_ifd1 || (thumb_ptrs && !report.is_empty()),
dropped_xmp: false,
dropped_iptc: false,
},
)
}
///|
fn keep_entries(
entries : Array[IfdEntry],
dir : Dir,
p : Policy,
) -> Array[IfdEntry] {
let out : Array[IfdEntry] = []
for e in entries {
let drop = match sensitive_category(dir, e.tag) {
Some(cat) => p.covers(cat)
None => false
}
if !drop {
out.push(e)
}
}
out
}
///|
/// 一步到位:解析 → 脱敏 → 回写。返回新的 TIFF 块字节与报告。
pub fn redact_tiff(
data : Bytes,
p : Policy,
) -> (Bytes, Redaction) raise MetaError {
let block = decode_tiff(data)
let (kept, report) = block.redact(p)
(encode_tiff(kept), report)
}