tarx

package
v0.42.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 10, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package tarx 目录 tar 传输三件套:同口径预扫 / 流式写出 / 防御式解包, 外加可选的 sha256 manifest 自证(#326)。收编自 jumpserver-exec 目录传 输层手搓的三件同型逻辑(readDirTar/safeExtractPath/PrescanDir/逐文件摘 要清单),任何「把目录从 A 搬到 B」的工具都要重新面对这三个问题:

  • **防御式解包** Extract:绝对路径与 `..` 穿越条目**跳过并计数、点名 前三个**(SkippedNames),绝不写 root 之外——Zip Slip 是所有解包不 可信归档的工具的共同威胁;非常规条目(symlink/设备/socket/fifo) 同样跳过并计数(链接目标逃逸 root 的问题面整类消失);
  • **同口径预扫** Scan:本地行走,按「流式写入器会怎么分类」数 files/bytes/skipped,给演练答复与流截止时间放大当底数——口径漂移 的坑(预扫与写入器各走各的分类,演练就在对它要预演的那次运行撒 谎)由「同一棵分类树、同一份实现」钉死:Scan 与 Write 共用 walkRel 与同一套分类分支,Scan↔Write 的 Stats 逐字段相等有奇偶测试随包钉住;
  • **manifest 自证** Write 可在流尾夹带逐文件 sha256 清单(sha256sum 文本格式,远端可直接 `sha256sum -c` 人工核对),配 VerifyManifest 在另一端程序化核对——「传输完成」与「传输被证明」是两回事。

用法:

st, _ := tarx.Scan(root)            // 演练:这次传输有多大
st, _ := tarx.Write(w, root, tarx.WithManifest()) // 流式写出 + 自证清单
st, _ := tarx.Extract(r, root)      // 防御式解包(SkippedNames 点名恶意条目)
err := tarx.VerifyManifest(root)    // 另一端核对(ErrMismatch 指名文件)

边界声明

  • **不做压缩**:gzip 等由调用方包一层(w/r 两端各自包,本包只见 tar 字节流);
  • **不做远端执行**:`sha256sum -c` 之类核对命令是调用方的事(manifest 的文本格式就是为了让远端现有的工具能直接消费);
  • **Extract 不做 staging/原子交付**:解到哪、何时可见是调用方的文件 系统策略(典型做法:先解到 root-adjacent 临时目录再 rename 交付);
  • 不背「root 内既有符号链接把写入引到 root 外」的 TOCTOU:Extract 的穿越防线针对**归档条目名**;root 目录本身的可信性是调用方的前置 条件(fresh 目录/staging 是推荐姿势)。

Stats 语义:三个原语共用一个 Stats(Files/Dirs/Bytes/Skipped/ SkippedNames)——Scan 数的是「Write 会怎么流式分类」,Extract 数的是 「流里的条目怎么落了地」,同型字段逐一对得上;manifest 条目是流协议 开销、不算源树内容,不计入 Stats。SkippedNames 只留**前 3 个**点名样本 (恶意归档动辄上百条穿越,全量名单是调用方日志的事,库只负责给出足够 排查的头几个)。

Index

Constants

View Source
const ManifestName = "oekit-tarx-manifest.txt"

ManifestName WithManifest 夹带的清单条目名(解包后位于 root/<ManifestName>; 核对完是否删除由调用方定——一般 `os.Remove` 一行)。

Variables

View Source
var (
	// ErrNoManifest 目录里没有清单(root/<ManifestName> 不存在)——
	// 没用 WithManifest 的流解出来的树自然没有,调用方据此区分「没自证」
	// 与「自证失败」。
	ErrNoManifest = errors.New("tarx: manifest 不存在")
	// ErrMismatch 清单核对失败:文件缺失、内容被篡改、清单行格式非法
	// ——错误消息点名具体文件/行号。
	ErrMismatch = errors.New("tarx: manifest 核对失败")
)

manifest 核对错误哨兵(errors.Is 判定;语义见各注释)。

Functions

func VerifyManifest

func VerifyManifest(root string) error

VerifyManifest 对解包后的树核对清单(读 root/<ManifestName>,逐文件 sha256 比对)。

判定口径(与 sha256sum -c 一致):

  • 清单列出的文件缺失或摘要不符 → ErrMismatch(消息指名文件);
  • 清单行格式非法(前 64 字节非 hex 等)→ ErrMismatch(消息指名行号);
  • 树里多出的文件不核对(sha256sum -c 同语义——清单只承诺「列出的 都在且对」;「树比清单大」的检测是 Extract Stats 的事);
  • 清单文件本身缺失 → ErrNoManifest。

Types

type Stats

type Stats struct {
	Files        int      // 常规文件数
	Dirs         int      // 目录数(不含 root 本身)
	Bytes        int64    // 常规文件内容字节合计
	Skipped      int      // 流会丢下/解包会拒收的条目数(非常规类型、穿越条目)
	SkippedNames []string // 前 3 个被跳过条目的点名样本(穿越条目/非常规类型混排,按遇序)
}

Stats 三件套共用的口径计数。Scan 与 Write 对同一棵树的 Stats 逐字段相 等(奇偶测试随包交付);Extract 的同型字段描述流内条目的落地情况。

func Extract

func Extract(r io.Reader, root string) (Stats, error)

Extract 从 tar 流解包到 root 目录,返回落地口径的 Stats。

防线(按遇序逐条目判定):

  • 条目名经 safeJoin 判定——绝对路径(/etc/passwd、Windows 盘符)、 `..` 穿越(../x、a/../../x)一律拒绝:Skipped 计数 + SkippedNames 点名(前 3),**检测并拒绝而非静默改写**(恶意条目必须可见地死掉, 审计面才不失真);
  • tar.Reader 自动弃置拒收条目的内容(流位置正确推进,恶意条目的 载荷不会落地也不会卡死后续条目);
  • 常规文件 create-or-truncate 写入(mode 取 header,umask 照常生效); 父目录缺失时按需补建(合法流可能子先于父出现);
  • 目录条目 MkdirAll(`.` 条目视作 root 本身,不计数不报错);
  • 其余类型(symlink/hardlink/char/block/fifo/未知)跳过并计数点名。

错误语义:tar 流损坏/截断(含 Write 出错未写结束块的流)原样上抛—— 已落地的条目保留(调用方按 staging 策略处置),Stats 反映死前落地量; root 不存在时自动创建。

func Scan

func Scan(root string) (Stats, error)

Scan 预扫 root 目录树,返回与 Write 同口径的 Stats。

root 不存在/不可读时返回行走错误(filepath.WalkDir 原样上抛,含路径上 下文)。Scan 不打开文件内容——Bytes 取 FileInfo.Size(与 Write 的 header.Size 同源同值);两次 Scan 之间树内容变化(写入/删除)是调用方 的并发前置条件,本包不做快照。

func Write

func Write(w io.Writer, root string, opts ...WriteOption) (Stats, error)

Write 流式写出 root 目录树为 tar 流,返回与 Scan 同口径的 Stats。

行走为字典序(与 Scan 同序同分类,奇偶测试钉住);常规文件写 TypeReg 条目(mode/mtime 取自文件系统,大小 = FileInfo.Size)、目录写 TypeDir 条目、其余类型(symlink/socket/设备/fifo)跳过并计入 Skipped。

错误语义:行走/读文件错误原样上抛(含路径上下文),此时**不写 tar 结 束块**——流不完整,接收端的 Extract 会以 tar 读错误告终,绝不静默交付 半棵树;root 不存在/不可读同理。空目录树(root 内无条目)是合法输入, 产出仅含结束块的空 tar。

type WriteOption

type WriteOption func(*writeConfig)

WriteOption Write 的构造选项(仓规禁裸 Option)。

func WithManifest

func WithManifest() WriteOption

WithManifest 流尾夹带逐文件 sha256 清单(ManifestName 条目,sha256sum 文本格式:`<hex> <name>`)。清单覆盖源树全部常规文件;manifest 条目本 身是流协议开销,不计入 Stats。远端人工核对可直接 `sha256sum -c`,程序 化核对用 VerifyManifest。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL