Documentation
¶
Overview ¶
Package pathx 提供文件路径匹配与清洗工具。
MatchesPath 支持标准 glob 模式和 /** 目录前缀匹配, 适用于 .gitignore 风格的路径过滤场景(代码审查忽略路径、构建排除等)。
MatchGlob 是**名字**(而非路径段)的通配匹配:'*' 可跨 '/','?' 匹配一个 rune。与 MatchesPath 是两个语义各归其位的原语——分支名、ref 名、仓库全名 这类对象本身含 '/',用路径段语义答不了("*" 匹配不到 "owner/repo", 「排除所有」这类配置会静默失效)。
SanitizeRel / SanitizeRelStrict 提供仓库相对路径的清洗与 traversal 拒绝: 路径来自外部输入(webhook 载荷)时,清洗逻辑每次都要重写且各处口径容易 漂移(实测同一仓内三档:宽松 / 严格 / 只看 Clean 后的结果)——本包给基线, 严格档额外拒绝显式 ".." 段。
SanitizeFileName 提供**文件名**(展示名 / 存储名 / 上传原始名)的消毒: 与 SanitizeRel 是同族但语义相反的两个原语——SanitizeRel 问「这路径合法吗」 (越界即拒绝),SanitizeFileName 问「这名字能不能安全显示和存储」 (目录成分丢弃、控制字符丢弃、Windows 保留字符替换、超长按字节截断并保扩展名)。
IsSafeFileName 是**判定器**(不合规则说不合格,不改写):这个名字能不能原样 作为某目录下的单个文件名用。它与 SanitizeFileName 语义相反——后者是改写器, 拿它做回环校验会把「本来就不合法」与「被消毒规则改了」混为一谈。
用法:
pathx.MatchesPath([]string{"vendor/**", "*.test"}, "vendor/foo/bar.go") // true
pathx.MatchesPath([]string{"*.go"}, "main.go") // true
pathx.MatchGlob("feature/*", "feature/x/y") // true('*' 跨 '/')
pathx.MatchGlob("*", "owner/repo") // true
cleaned, ok := pathx.SanitizeRel("src/./main.go") // "src/main.go", true
cleaned, ok := pathx.SanitizeRel("../../etc/passwd") // "", false
pathx.SanitizeFileName("a/b/c.txt") // "c.txt"
pathx.SanitizeFileName(`C:\Users\me\报告.txt`) // "报告.txt"
pathx.IsSafeFileName("report.txt") // true
pathx.IsSafeFileName("..") // false
Package pathx 提供文件路径匹配工具。
Index ¶
- func IsSafeFileName(name string) bool
- func MatchGlob(pattern, s string) bool
- func MatchesPath(patterns []string, path string) bool
- func SanitizeFileName(name string, opts ...FileNameOption) string
- func SanitizeRel(p string) (cleaned string, ok bool)
- func SanitizeRelStrict(p string) (cleaned string, ok bool)
- type FileNameOption
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsSafeFileName ¶ added in v0.22.0
IsSafeFileName 判断 name 是否可安全地作为「某目录下的单个文件名」使用。
与同族另两个原语是**三件不同的事**,别混用:
- SanitizeRel / SanitizeRelStrict 答的是「这条**路径**合法吗、越不越界」—— 它答不了「单段文件名」这件事(它本身就允许多段相对路径);
- SanitizeFileName 是**改写器**:回答「这名字能不能安全地显示和存储」, 目录成分一律丢弃、消毒后总有名字可给(没有 ok=false 这一说);
- IsSafeFileName 是**判定器**:回答「这个名字能不能原样作为某个目录下的 单个文件名用」——不合规则直接说不合格,不改写。
为什么需要它:消费方把外部输入(上传名、归档名、bundle 名)拼进 filepath.Join(dir, name) 时要的正是这个判定,而最自然的写法 filepath.Base(name) != name 是**错的**——filepath.Base("..") == ".."、 filepath.Base(".") == ".",两个都会被放行,而 name 随后被 Join 拼进路径, ".." 把路径指到 dir 的父目录。
判定口径(与 SanitizeRelStrict 严格档一致,外加「必须是单段」这一条):
- 不含路径分隔符:**"/" 与 "\" 都算分隔符**(按运行平台解释会让同一段载荷 在 Linux 与 Windows 上得到不同结论,而 Linux 才是绝大多数服务端环境, 那正是双点反斜杠形绕过最容易漏掉的地方);
- 不是 "." / ".." 段、不是绝对路径、不是 Windows 盘符("C:\foo"、"C:foo");
- **fail-closed**:首两字符形如盘符的名字("C:foo.bundle")在 Windows 上是 盘符相对路径,同样拒绝——宁可拒,不可让同一段输入在不同平台上得到不同结论。
用法:
pathx.IsSafeFileName("report.txt") // true
pathx.IsSafeFileName("a/b.txt") // false(含分隔符,不是单段)
pathx.IsSafeFileName("..") // false(filepath.Base 放行的那两个)
pathx.IsSafeFileName("C:foo") // false(Windows 盘符相对路径)
func MatchGlob ¶ added in v0.22.0
MatchGlob 通配匹配:`*` 匹配任意字符序列(**可跨 `/`**),`?` 匹配单个字符 (按 **rune** 计,不是字节)。
与 MatchesPath 是**两个语义各归其位**的原语,不要混用:
- MatchesPath 是 filepath.Match 语义:`*` **不跨** `/`,用于**路径段** 匹配(".gitignore 风格"的路径过滤);
- MatchGlob 用于**名字**匹配——分支名("feature/x")、ref 名 ("refs/heads/x")、仓库全名("owner/repo")这类对象本身含 `/`, 用 filepath.Match 语义答不了:"feature/*" 匹配不到 "feature/x"。
为什么需要它:`*` 不跨 `/` 这一点让「排除所有」这类配置**彻底失效且毫无 察觉**——exclude_globs 写 ["*"](期望排除全部)时,因为 `*` 匹配不到 "owner/repo",这条配置等于没配;同理 include_globs 写 ["*"] 会把仓库 **全部过滤掉**(一个都匹配不上)。配置语法合法、无报错、只是不生效。
口径(与本包既有原语的风格一致,写死在这里):
- `*` 匹配任意**字符序列**(可跨 `/`,也可匹配空);
- `?` 匹配**一个 rune**(不是字节——按字节切会把中文名切出半个字符);
- 其余字符按**字面量**比较,**不做 shell 风格转义**(`\` 只是普通字符, 不引入 `[...]` 字符类——那会让「模式」本身变成需要另一套解析器的语言)。
用法:
pathx.MatchGlob("*", "owner/repo") // true(排除全部)
pathx.MatchGlob("feature/*", "feature/x") // true
pathx.MatchGlob("feature/*", "feature/x/y") // true(* 跨 /)
pathx.MatchGlob("a?c", "abc") // true
pathx.MatchGlob("a?c", "a中c") // true(按 rune 计)
pathx.MatchGlob("a?c", "ac") // false
func MatchesPath ¶
MatchesPath 检查路径是否匹配任意一个 glob 模式。 支持标准 filepath.Match 模式以及 /** 目录前缀匹配。
模式示例:
- "*.go" → 匹配根目录下的 .go 文件
- "vendor/*" → 匹配 vendor 目录下的直接子项
- "node_modules/**" → 匹配 node_modules 下所有层级的文件
- "test/*.txt" → 匹配 test 目录下的 .txt 文件
用法:
pathx.MatchesPath([]string{"vendor/**", "*.test"}, "vendor/foo/bar.go") // true
pathx.MatchesPath([]string{"*.go"}, "main.go") // true
func SanitizeFileName ¶ added in v0.21.0
func SanitizeFileName(name string, opts ...FileNameOption) string
SanitizeFileName 消毒**展示 / 存储用**的文件名。
与 SanitizeRel 是同族但**语义相反**的两个原语,不要混用:
- SanitizeRel 管**路径**(仓库相对路径):回答「这个路径合法吗、越不越界」, 越界与绝对路径一律**拒绝**(返回 ok=false)。
- SanitizeFileName 管**文件名**(展示名 / 存储名 / 上传原始名):回答 「这个名字能不能安全地显示和存储」,目录成分一律**丢弃**(只取最后一段), 不存在 ok=false 这一说——消毒后总有名字可给(空则回退)。
同一段输入在两者下的结论必然不同,这是刻意的:
pathx.SanitizeRel("C:\\Users\\me\\报告.txt") // "", false(Windows 盘符绝对路径 → 拒绝)
pathx.SanitizeFileName("C:\\Users\\me\\报告.txt") // "报告.txt"(文件名只要最后一段)
消毒规则(顺序即口径):
- **剥掉目录成分**:取最后一段,"/" 与 "\" 都算分隔符(跨平台同判)。
- **丢弃控制字符**:r < 0x20 或 r == 0x7f。
- **Windows 保留字符**(`: * ? " < > |`)替换为下划线 `_`。
- **去首尾空白与结尾的点**:结尾的点与空白在 Windows 上是目录项陷阱 ("file." 与 "file " 会指向 "file"),首部空白无此含义故只去空白。
- **空名 / 纯符号名回退**:清洗后为空,或通篇没有字母与数字("***" → "___") ⇒ 回退为 fallback(缺省 "file")。纯符号名落盘无碍但对展示无意义, 直接回退比留一串下划线干净。
- **超长截断**:按**字节**上限截断,且**保留扩展名**;截断点回落到 rune 边界,不会产出半个 UTF-8 字符。
非 ASCII 字符(中日韩等)一律保留——展示名与存储名的核心诉求就是别把 中文名弄丢。非法 UTF-8 字节替换为 U+FFFD,保证输出是合法 UTF-8。
用法:
pathx.SanitizeFileName("a/b/c.txt") // "c.txt"
pathx.SanitizeFileName(`C:\Users\me\报告.txt`) // "报告.txt"
pathx.SanitizeFileName("a:b*c?.txt") // "a_b_c_.txt"
pathx.SanitizeFileName("...") // "file"(纯符号名回退)
pathx.SanitizeFileName("报告.txt",
pathx.WithMaxFileNameLength(20)) // "报告.txt"(按字节卡)
func SanitizeRel ¶ added in v0.20.0
SanitizeRel 清洗仓库相对路径:统一分隔符为 "/"、消除 "." 与 ".." 段, 绝对路径或越出根目录("..")时 ok=false。
定位:仓库相对路径常来自外部输入(webhook 载荷里的文件路径), 清洗与 traversal 拒绝是同一件事的两面——消费方各写各的清洗逻辑时, 口径会漂移(实测同一仓内三档:宽松 / 严格 / 只看 Clean 后的结果)。 本原语给基线,调用方在基线上选严格档。
分隔符口径:**"/" 与 "\" 都当作分隔符**。仓库路径来自外部载荷时 Windows 风格分隔符同样是路径的一部分;若按运行平台解释(Linux 上 "\" 只是普通字符),同一段载荷在 Linux 与 Windows 上会得到不同结论—— 而 Linux 才是绝大多数服务端环境,那正是 "..\..\" 形绕过最容易漏掉的地方。
判定口径:
- 绝对路径("/foo"、UNC "//host/share"、Windows 盘符 "C:\foo")→ ok=false;
- 越出根目录("../x"、"/../x"、"a/../../x")→ ok=false;
- 清洗后为空("."、"a/.." —— 指向仓库根目录本身,非具体条目)→ ok=false;
- 空串 / 纯空白 → ok=false。
".." 段的处理:宽松档下 ".." 抵消前一段("a/../b" → "b"),但抵消后 仍要留在根目录内("a/../../b" 越界 → ok=false)。
用法:
cleaned, ok := pathx.SanitizeRel("src/./main.go") // "src/main.go", true
cleaned, ok := pathx.SanitizeRel("../../etc/passwd") // "", false
func SanitizeRelStrict ¶ added in v0.20.0
SanitizeRelStrict 是 SanitizeRel 的严格档:除 SanitizeRel 的全部判定外, **输入中显式出现的 ".." 段一律拒绝**,即使它能被 Clean 抵消掉 ("a/../b" 在宽松档得到 "b",在严格档直接 ok=false)。
为什么需要这一档:宽松档的 ".." 抵消是纯文本语义,看不见符号链接。 路径先过一层符号链接再按相对路径解析时,文本上合法的 "a/../b" 可能 指向预期之外的位置——安全档宁可拒绝,也不做这层推断。
用法:
cleaned, ok := pathx.SanitizeRelStrict("a/../b") // "", false(宽松档是 "b")
Types ¶
type FileNameOption ¶ added in v0.21.0
type FileNameOption func(*fileNameConfig)
FileNameOption 是 SanitizeFileName 的可选配置。
命名带 FileName 前缀而非裸 Option:本包与 pathx 其它导出符号同处一个 import 组,裸 Option 会与 co-import 的其它包的 Option 撞名(矩阵硬规则)。
func WithFileNameFallback ¶ added in v0.21.0
func WithFileNameFallback(s string) FileNameOption
WithFileNameFallback 设置空名 / 纯符号名的回退名。
回退名**原样返回**(不参与消毒、不受长度上限约束)——它是调用方给的 常量,不是外部输入,原样透传可预期。
func WithMaxFileNameLength ¶ added in v0.21.0
func WithMaxFileNameLength(n int) FileNameOption
WithMaxFileNameLength 设置文件名长度上限,单位**字节**。
n <= 0 表示**不限长**(与矩阵内并发闸门「cap<=0 视为不限」的既有口径一致)。
为什么按字节而不是字符数:文件系统按字节卡(ext4 单目录项 255 字节), 按字符数卡会在多字节文件名下超限落盘失败。截断本身按 rune 边界回落, 不会产出半个 UTF-8 字符。