简介:dejsonlz4 是面向 Firefox 用户、数据恢复与脚本开发者的开源小工具,用于处理以 lz4 压缩存储的 .jsonlz4 书签备份文件,解决这类非标准格式无法直接读取、迁移或恢复书签的问题。压缩包仅 22KB,共 7 个文件,主体为 3 个 C 源文件、1 个头文件、2 个 Markdown 文档和 1 份许可证,核心围绕 dejsonlz4.c 与 lz4.c 展开,配合 README 说明项目用途与编译方式,结构紧凑、依赖清晰。已有 470 人学习,适合需要快速解压书签备份、查看内部 JSON 数据,或希望研读轻量级 LZ4 解压实现的中级开发者参考。源码保留完整实现与许可信息,可作为书签恢复、格式解析和 C 语言压缩库学习的小型样本,也便于按需二次编译与集成。对于误删书签、浏览器升级迁移、跨设备整理或分析备份内容的场景,可借助这份独立工具获得可读数据,并对照 lz4 参考实现理解数据块组织与解压流程。
1. 书签备份变成乱码:.jsonlz4 为什么不能双击打开
从浏览器里翻出来的书签备份,文件名大多是bookmarks-2026-05-01.jsonlz4这种样子,双击打不开,拖进编辑器是一屏乱码,改个后缀成.json再打开,乱码还是乱码。有人当场判断“备份坏了”,把唯一一份历史副本删掉,等真要恢复书签时才发现没有后悔药。这其实不是文件损坏,而是 Firefox 把备份存进了 mozLz4 容器:8 字节魔数加 4 字节原始长度,后面跟一段 LZ4 块数据。dejsonlz4 就是为这个格式写的开源小工具,装它没有玄学,环境也轻,但装错版本、放错 PATH、拿 lz4 命令行硬解,都会让你在第一步就卡住。这套流程适合两类人:手里攥着.jsonlz4想还原书签的普通用户,以及要批量处理一堆配置目录备份的运维。
2. 装前把环境列清:dejsonlz4 的两条安装路线与 PATH 落地
装工具之前先别急着敲安装命令,先把三件事确认掉:手里的文件到底是什么格式、机器上有没有编译工具链、装完的产物会落在哪个目录。这三件事任何一件没对齐,后面的命令都会变成“装是装上了,就是跑不起来”。
2.1 先给文件做一次基础检查:三种后缀别混着当一种
带.jsonlz4后缀的文件之间差异很大,最常见的误判就是把压缩包外壳当成了容器本体。下载来的资源经常是.zip,里面才躺着真正的.jsonlz4。直接对 zip 跑解压工具,工具只会告诉你头部不匹配,然后你会怀疑工具装错了。
第一步先看文件类型和头部字节:
# 看真实类型,别只看后缀 file bookmarks-2026-05-01.jsonlz4 # 取前 12 字节,确认是不是 mozLz4 容器 head -c 12 bookmarks-2026-05-01.jsonlz4 | xxdfile一般会报data,这是正常的,它对这种私有容器没有特征库。真正要看的是第二行输出:前 8 字节应该是6d6f 7a4c 7a34 3000,转成 ASCII 就是mozLz40加一个\0;紧跟着 4 字节是小端序的原始长度,比如90 1f 00 00表示解压后约 8080 字节。这两个字段对不上,就说明文件不是 mozLz4 容器,可能是被二次打包过,或者根本不是书签备份。
如果file输出里带Zip archive data,先用unzip -l看一眼目录结构再解,别对着 zip 直接跑 dejsonlz4。这一步花十秒,能省掉后面半小时的自我怀疑。
2.2 二进制路线:没有编译器时怎么三分钟落地
大部分人的机器上没有 Rust 也没有 Go,为了解压几个文件去装整套工具链不划算。走预编译二进制是更实际的选择:从项目发布页按操作系统和 CPU 架构拿对应的可执行文件,Linux 选linux-amd64,macOS 上 Apple 芯片选darwin-arm64,Windows 选.exe。
落地命令就三步,放用户级目录,不用 sudo:
# 1. 建一个用户级可执行目录(已存在就跳过) mkdir -p ~/.local/bin # 2. 赋执行权限并挪进去 chmod +x ./dejsonlz4 mv ./dejsonlz4 ~/.local/bin/dejsonlz4 # 3. 确认能被 shell 找到 command -v dejsonlz4 && dejsonlz4 --helpchmod +x这步不能省。从浏览器下载的文件默认没有执行位,少了它,shell 会报Permission denied,而很多人会误以为是架构不匹配。mv到~/.local/bin而不是/usr/local/bin,是为了避开 sudo,也避免以后升级系统时被清理掉。
command -v和which的区别值得说一句:command -v是 shell 内建,识别 alias 和函数,判断“这个命令在当前 shell 里到底能不能跑”更准。如果它没输出,但文件确实在~/.local/bin里,那就是 PATH 没包含这个目录。
注意:
~/.local/bin不在默认 PATH 里的发行版不少,尤其是用 root 直接登录的场景。加 PATH 要落到 shell 配置文件里,而不是只在当前终端 export 一次。
2.3 源码路线:cargo 与 go 两条构建链的取舍
机器上本来就有工具链的话,走源码构建更省心,升级也只是一条命令的事。
Rust 路线:
# 从源码构建并安装到 ~/.cargo/bin cargo install dejsonlz4 # 构建产物默认不在当前 PATH 时手动确认 ls -l ~/.cargo/bin/dejsonlz4cargo install会把二进制装进$CARGO_HOME/bin,默认就是~/.cargo/bin。这条路线的好处是依赖由 cargo 自己拉、自己编译,产物和本机 glibc 完全匹配,不会遇到预编译包“版本太新跑不起来”的问题。代价是第一次编译要拉一堆 crate,构建机访问外部制品库受限的时候会卡在下载阶段,这种情况换预编译二进制更快。
Go 路线:
# 模块路径按项目页给出的原样执行,别自己拼 go install <项目页给出的模块路径>@latest # 确认产物位置 ls -l "$(go env GOPATH)/bin"Go 的产物落在$(go env GOPATH)/bin,很多人go install完发现命令找不到,就是因为这个目录没进 PATH,而go env GOPATH的输出又不是每个人都知道去看。
两条路线的实际差别不在功能,而在“升级成本”和“环境侵入度”。只解压一两次,二进制最划算;长期要给一批机器做书签归档,源码安装配合配置管理工具更稳定,版本可追溯。开源项目在这点上优势明显:真遇到解析异常,可以自己拉源码看它怎么读那 12 字节头,不用等别人修。
2.4 环境对照表:版本、依赖与 PATH 的最小检查集
把四条路线摊开对比,选哪条一眼就清楚:
| 路线 | 前置依赖 | 产物位置 | 适用场景 | 主要风险 |
|---|---|---|---|---|
| 预编译二进制 | 无(需匹配 libc) | 手动放置目录 | 只解压少量文件 | 架构或 libc 不匹配 |
| cargo 构建 | Rust 工具链 | ~/.cargo/bin | 已有 Rust 环境 | 首次编译耗时长 |
| go install | Go 工具链 | $(go env GOPATH)/bin | 已有 Go 环境 | GOPATH/bin 不在 PATH |
| Python 脚本兜底 | python3 + lz4 库 | 无(脚本) | 无法安装二进制 | 需自行保证格式解析正确 |
装完统一做一遍 PATH 检查,三条命令足够:
# 当前 shell 能不能找到 command -v dejsonlz4 # 是否存在多个同名副本(版本冲突的常见来源) type -a dejsonlz4 # PATH 里到底有哪些目录 echo "$PATH" | tr ':' '\n'type -a会列出所有命中项。如果它同时打印出/usr/bin/dejsonlz4和~/.local/bin/dejsonlz4,说明机器上有两份,shell 按 PATH 顺序取第一个。这种“明明升级了却还是老行为”的问题,八成出在这里。
3. 首次解压与参数配置:把 .jsonlz4 还原成能过 jq 的 JSON
工具进 PATH 之后,第一件事不是急着处理整个备份目录,而是拿单个文件跑通一次完整链路。单文件跑通,后面批量才有意义。
3.1 最小可用命令:一次只解一个文件
不同实现的命令行参数不完全一致,动手前先看帮助,别照抄网上的命令:
# 先确认这个构建支持哪种参数形态 dejsonlz4 --help常见的两种形态:
# 形态一:位置参数,输入在前输出在后 dejsonlz4 bookmarks-2026-05-01.jsonlz4 bookmarks.json # 形态二:显式指定输入输出 dejsonlz4 -i bookmarks-2026-05-01.jsonlz4 -o bookmarks.json参数含义很直白:第一个是待解压的容器文件,第二个是解压后写出的明文 JSON。敲完没有任何输出、退出码为 0,通常就是成功了,这类小工具默认安静。想确认退出码:
dejsonlz4 bookmarks-2026-05-01.jsonlz4 bookmarks.json && echo "exit=$?"&&保证只有前一条成功才会打印,$?是上一条命令的退出码。0 表示成功,1 一般是参数错误或文件读取失败,2 常见于格式不匹配。养成看退出码的习惯,批量脚本里才不会把失败当成功。
第一次跑建议把输出写到临时目录,别直接覆盖任何东西:
mkdir -p /tmp/bm && dejsonlz4 bookmarks-2026-05-01.jsonlz4 /tmp/bm/bookmarks.json3.2 批量与命名:给每个备份留一份可追溯的副本
书签备份目录里通常躺着好几份按日期命名的文件,一份份手敲不现实。写个循环,输入目录和输出目录都用参数传,别写死路径:
#!/usr/bin/env bash # 批量解压 bookmarkbackups 目录下的所有 .jsonlz4 set -euo pipefail SRC="${1:?用法: unpack.sh <备份目录> <输出目录>}" OUT="${2:?用法: unpack.sh <备份目录> <输出目录>}" mkdir -p "$OUT" shopt -s nullglob # 目录为空时让通配符展开成空,而不是字面量 count=0 for f in "$SRC"/*.jsonlz4; do base="$(basename "$f" .jsonlz4)" dejsonlz4 "$f" "$OUT/$base.json" count=$((count + 1)) done echo "done: $count file(s) -> $OUT"逐行说清楚:
set -euo pipefail:任一命令失败、用了未定义变量、管道中间环节失败,脚本立刻退出。批量处理最怕“错了一半还在往下跑”。${1:?用法...}:参数缺失时直接报用法并退出,避免SRC为空导致通配符变成/*.jsonlz4,那就会去扫根目录。shopt -s nullglob:默认情况下,通配符匹配不到文件会原样保留成字符串*.jsonlz4,循环里就会拿这个字面量去执行,报一堆莫名其妙的错。basename "$f" .jsonlz4:去掉后缀只留日期部分,输出文件名和源文件一一对应,出问题能反查是哪份备份。
只想要最新一份的话,别用ls排序后head -1这种靠输出格式的写法,用find按修改时间排更稳:
# 取修改时间最新的一份备份 latest="$(find "$SRC" -maxdepth 1 -name '*.jsonlz4' -printf '%T@ %p\n' \ | sort -n | tail -n 1 | cut -d' ' -f2-)" echo "latest: $latest"-printf '%T@ %p\n'输出“时间戳 路径”,sort -n按数值排序,cut -d' ' -f2-去掉时间戳留路径。-f2-里的连字符表示“从第二列到行尾”,路径中带空格也不会被截断。
3.3 参数取舍:标准输出、覆盖写入与静默模式
几个容易踩的细节,提前说清楚比事后排查省事。
输出到标准输出。部分构建支持把结果直接打到 stdout,方便接管道:
dejsonlz4 bookmarks-2026-05-01.jsonlz4 - | jq '.children | length'这里的-是占位符,表示“不写文件,走标准输出”。如果这个构建不认这种写法,它会当成文件名建一个叫-的文件,跑完ls一看多个怪文件,别慌,删掉就行。用之前先--help确认。
覆盖写入。默认行为通常是直接覆盖同名输出文件,不带确认。批量脚本里这是想要的效果,手动跑的时候就得留神:
# 先备份再覆盖,多一步少一次事故 [ -f bookmarks.json ] && cp bookmarks.json bookmarks.json.bak dejsonlz4 bookmarks-2026-05-01.jsonlz4 bookmarks.json静默与嘈杂。有的构建默认打印“解压成功 / 字节数”,有的什么都不说。脚本里如果要把结果喂给下一环,最好把工具的标准输出和错误输出分开处理:
dejsonlz4 "$f" "$out" > /dev/null # 只吞正常输出,错误仍然打到终端注意别写成2>&1 > /dev/null,那个顺序会把错误也吞掉,出问题你什么都看不到。
3.4 解压后的第一道校验
写出文件不等于内容正确。三步校验,每步都很短:
# 1. JSON 结构是否合法 python3 -m json.tool bookmarks.json > /dev/null && echo "JSON OK" # 2. 顶层结构长什么样 jq 'keys' bookmarks.json # 3. 实际书签条目数量 jq '[.. | objects | select(.type=="text/x-moz-place")] | length' bookmarks.json第一条用 Python 标准库的 json 模块做最严格的语法校验,比任何“看着像”的判断都可靠。第二条keys会打印根对象的字段名,书签备份的根通常是guid、title、type、root、children这类字段,看到root: "placesRoot"就基本能确认是书签树而不是别的容器。第三条递归遍历所有对象,挑出类型为书签条目的节点计数,..是 jq 的递归下降,objects过滤掉数组和标量。
三个数字对得上,这份解压结果才敢拿去恢复。
4. 兜底方案:用 Python 手工拆开 mozLz4 容器
总有装不上工具的环境:离线机器、架构特殊的设备、只有 Python 的容器镜像。这时候自己拆容器反而最快,因为格式本身简单得离谱。
4.1 头部 12 字节:magic 与原始长度
mozLz4 的容器结构就两段:
| 偏移 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 0 | 8 字节 | mozLz40\0 | 固定魔数,最后一位是 0x00 |
| 8 | 4 字节 | 原始长度 | 小端序 uint32,解压后应有的字节数 |
| 12 | 剩余全部 | LZ4 块数据 | 裸 block,不带 frame 头 |
关键点在最后一行:它是 LZ4 的block格式,不是frame格式。frame 有0x184D2204魔数、有块校验、有帧尾,lz4 -d这类命令行工具默认按 frame 解析,所以拿它去解 mozLz4 只会报“格式错误”或者解出垃圾。这个区别是绝大多数失败的根源。
那 4 字节长度字段是必须用的:LZ4 block 解压时要知道目标缓冲区多大,虽然很多实现能从块里推出来,但显式传进去最稳,也顺便能做长度校验。
4.2 一段 30 行的解压脚本与参数说明
依赖只有一个库:
python3 -m pip install --user lz4脚本:
#!/usr/bin/env python3 """解压 mozLz4 容器(书签备份 / 会话恢复文件通用)""" import struct import sys from pathlib import Path import lz4.block MAGIC = b"mozLz40\x00" HEADER_LEN = 12 def unpack(src: Path, dst: Path) -> int: raw = src.read_bytes() if len(raw) < HEADER_LEN: raise ValueError(f"{src} 长度 {len(raw)},连 12 字节头都不够") if raw[:8] != MAGIC: raise ValueError(f"{src} 不是 mozLz4 容器,头部为 {raw[:8]!r}") # '<I' = 小端序、无符号 32 位整数 declared = struct.unpack("<I", raw[8:12])[0] # uncompressed_size 必须显式传,否则部分版本会按块链推测出错 data = lz4.block.decompress(raw[HEADER_LEN:], uncompressed_size=declared) if len(data) != declared: raise ValueError(f"长度不符:头部声明 {declared},实际 {len(data)}") dst.write_bytes(data) return len(data) if __name__ == "__main__": if len(sys.argv) != 3: sys.exit("用法: python3 mozlz4.py <输入.jsonlz4> <输出.json>") n = unpack(Path(sys.argv[1]), Path(sys.argv[2])) print(f"ok: {n} bytes")参数与逻辑说明:
struct.unpack("<I", ...):<明确小端,避免在大端机器上读出天文数字;I是 4 字节无符号整数。不带<会走本机字节序,跨平台脚本必须显式指定。uncompressed_size=declared:告诉解压器目标大小。不传的话,某些版本的lz4.block会尝试从块头推断,遇到分块存储的数据可能提前截断,输出看起来像“JSON 只写了一半”。- 长度断言:解出来再比一次,比对不上立刻报错而不是写文件。写出去一个长度不对的 JSON,后面排查要绕一大圈。
- 输入输出都用
Path:避免手拼字符串路径时 Windows 反斜杠和 POSIX 斜杠混用。
脚本不需要执行位也能跑,python3 mozlz4.py a.jsonlz4 b.json即可,这在只有解释器没有编译器的环境里最省事。
4.3 和 dejsonlz4 的输出做交叉比对
两条链路都跑一遍,比哈希:
dejsonlz4 bookmarks-2026-05-01.jsonlz4 /tmp/bm/tool.json python3 mozlz4.py bookmarks-2026-05-01.jsonlz4 /tmp/bm/py.json sha256sum /tmp/bm/tool.json /tmp/bm/py.json两个哈希完全一致,说明格式解析没有分歧,这份数据可以放心用。不一致通常有两个原因:一是其中一个把输出重定向写坏了(比如 shell 的>覆盖了工具自己写的文件),二是数据里有多余空白差异。先用jq -S . a.json > a.norm && jq -S . b.json > b.norm归一化再比,能排除格式层面干扰。
jq -S的-S是对键排序输出,用来做内容级对比很实用。
4.4 同一套逻辑处理 sessionstore 恢复文件
配置目录里的.jsonlz4不只有书签备份,会话恢复目录下的recovery.jsonlz4用的是完全相同的容器格式。上面那个脚本改一下文件名就能直接跑,不需要任何改动:
python3 mozlz4.py recovery.jsonlz4 /tmp/bm/recovery.json jq '.windows | length' /tmp/bm/recovery.json第二条输出窗口数量,用来确认解出来的确实是会话数据而不是空壳。这类文件里存的是标签页和窗口状态,结构和书签完全不同,但容器层没有任何区别——记住“容器归容器、内容归内容”,以后遇到别的.jsonlz4文件就不会先入为主地当成书签。
5. 避坑与排查:五条装过才知道的踩坑记录
5.1 解出来全是乱码,lz4 命令行也报错
现象:用系统里的lz4 -d去解.jsonlz4,报Unrecognized header或解出一堆二进制垃圾,改成重命名为.lz4也一样。
原因:lz4命令行工具认的是 frame 格式,带魔数0x184D2204和块校验。mozLz4 里装的是裸 block,前面还盖了 12 字节私有头,两者不是一回事。用 frame 解析器读 block 数据,必然错位。
解决:要么用专门处理这个容器的工具,要么按第 4 章的脚本自己切掉 12 字节头再走lz4.block。判断依据很简单:head -c 12出来的前 8 字节是mozLz40\0,就不要再试 frame 类工具了。
5.2 命令明明装了,终端就是找不到
现象:ls ~/.cargo/bin/dejsonlz4有文件、有执行位,敲命令还是command not found。换一个终端窗口又能用,或者反过来。
原因:PATH 只在某次会话里 export 过,没有落到 shell 配置文件;或者用 sudo 装的,装进了 root 的~/.cargo/bin,普通用户根本看不到那个目录。
解决:先command -v确认当前 shell 视角,再echo "$PATH" | tr ':' '\n'看目录列表。缺目录就往~/.bashrc或~/.zshrc里补一行export PATH="$HOME/.local/bin:$PATH",重新登录生效。sudo 导致的权限错位,直接sudo -i下ls ~/.cargo/bin验证,确认后把二进制拷到用户目录,别长期依赖 root 环境。
5.3 JSON 看着完整,jq 在中间报 parse error
现象:文件大小和预期差不多,开头内容也正常,jq跑到一半报parse error: Unfinished string或Unfinished JSON term。
原因:输出被截断了。要么磁盘满,要么同一个文件被写了两遍——工具已经按参数写了输出文件,shell 那层又用>重定向覆盖了一次,后写的把先写的截短;还有一种情况是把“标准输出”和“输出文件参数”同时用上,两股数据打架。
解决:先df -h看空间,再tail -c 200 bookmarks.json看结尾是不是齐的。确认是重复写入,就把命令简化成只用一种输出方式,别既给-o又加>。校验时固定用python3 -m json.tool跑一遍,它比肉眼可靠。
5.4 备份目录里最新那份不是“最新”的
现象:想恢复最近的书签,按文件名取了日期最大的那份,解出来发现少了一周的内容。
原因:浏览器写书签备份有节流策略,不是每次改动都落盘,通常一天最多一份,且只在特定时机触发;目录里保留的份数也有上限,旧的会被轮转掉。文件名日期大,只代表“这是一份按该日期命名的备份”,不代表“包含到该日期为止的全部改动”。
解决:处理前先按修改时间而不是文件名排序,用第 3 章那段find -printf '%T@ %p'取真正最新的一份;同时把整个目录都解出来做条目数对比,取条目最多的一份作为恢复源。要更完整的数据,直接从还在用的配置目录里导出一次书签,别只依赖历史备份。
5.5 直接在运行中的配置目录上操作,读出来是半个文件
现象:从配置目录里直接复制或解压.jsonlz4,偶尔解出空文件或者头部字段对不上。
原因:浏览器正在运行时可能正在写这个文件,读的瞬间拿到的是写了一半的内容。文件锁在部分平台上不起作用,拷贝动作不会等你。
解决:先把整个备份目录复制到临时位置再处理,别在原目录里就地解压:
cp -a "<配置文件夹>/bookmarkbackups" /tmp/bm-src ./unpack.sh /tmp/bm-src /tmp/bm-out配置文件夹路径从浏览器地址栏的about:support里找“配置文件夹”那一项,复制完整路径,别凭记忆拼。处理完再决定要不要写回,写回前一定保留一份原始副本。
6. 装完怎么验证跑通:三条验收标准和一个固定习惯
装完一个工具,最怕“命令能跑、结果能用”之间那段落差。我的验收固定三条,逐条过一遍才认为这套环境真的通了。
第一条,文件头能被正确识别。
head -c 8 sample.jsonlz4 | xxd # 期望输出含 6d6f 7a4c 7a34 3000第二条,两条独立链路(工具与脚本)产出完全一致:
sha256sum /tmp/bm/tool.json /tmp/bm/py.json第三条,解压结果能被 JSON 解析器接受,并且条目数为正:
python3 -m json.tool /tmp/bm/tool.json > /dev/null \ && jq '[.. | objects | select(.type=="text/x-moz-place")] | length' /tmp/bm/tool.json三条都过,才轮到批量处理。我把这三条整理成一张速查表,后面换机器直接照表走:
| 验收项 | 命令核心 | 通过标准 |
|---|---|---|
| 格式识别 | head -c 8 | xxd | 头部为mozLz40\0 |
| 长度一致 | 脚本内长度断言 | 头部声明值与实际解压字节数相等 |
| 链路一致 | sha256sum对比 | 工具与脚本输出哈希相同 |
| 内容合法 | python3 -m json.tool | 无解析异常,退出码 0 |
| 数据有效 | jq递归计数 | 书签条目数大于 0 且与备份规模相符 |
还有一个技巧值得单独讲:别只验证“能解压”,要验证“解压结果能反向还原”。做法是把解出来的 JSON 用浏览器打开一次,看它是否按预期加载书签树。这一步会暴露那些“JSON 合法但结构不对”的问题,比如根节点字段被改过、children层级错位,这类问题纯靠jq计数是发现不了的。验证的时候用一份无关紧要的备份先试,别拿唯一的历史副本当试验品。
最后说个血泪经验。早些年我在一台老机器上解一份 30MB 出头的书签备份,工具报成功,jq也过,条目数看着正常,我没做哈希比对就删了原始的.jsonlz4。后来才发现那台机器的 libc 版本偏老,工具在某类输入下会静默截断最后一部分数据——文件合法、条数少了几十条,肉眼根本看不出来。从那以后我每次处理.jsonlz4,都强制走一遍“工具解一次、脚本解一次、哈希对一次、原始文件留一份”的流程,宁可多花两分钟,也不给自己留没有后悔药的余地。希望帮到你。
本文还有配套的精品资源,点击获取