pathx

package
v0.35.0 Latest Latest
Warning

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

Go to latest
Published: Nov 6, 2026 License: MIT Imports: 4 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsSafeFileName added in v0.22.0

func IsSafeFileName(name string) bool

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

func MatchGlob(pattern, s string) bool

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 MatchSimpleGlob added in v0.35.0

func MatchSimpleGlob(pattern, s string) bool

MatchSimpleGlob 是**单星**名字通配匹配:只有**第一颗** `*` 是通配符, 其余字符(含第二颗起的 `*` 与 `?`)一律按字面量比较。

口径(与 full-backtracking 的 MatchGlob 是**两个语义各归其位**的原语,不要混用):

  • pattern 或 name 为空串 ⇒ **不匹配**(MatchGlob 里 "*" 匹配空串);
  • pattern 恒为 "*" ⇒ 一切非空 name 都匹配;
  • pattern 不含 `*` ⇒ 精确相等;
  • "*x*"(长度 > 2)⇒ Contains(name, "x");
  • 其余含 `*` ⇒ 第一颗星切前后缀,HasPrefix && HasSuffix && len(name) >= len(prefix)+len(suffix) ——注意第二颗起的 `*` 落在**字面量**里("a*b*c" 的后缀是字面量 "b*c")。

什么时候用它而不是 MatchGlob:消费方要的是**保守的单星匹配**(多星/`?` 不展开), 且**空串必须判不匹配**——两条都是 MatchGlob 的反语义,混用会静默改判。

用法:

pathx.MatchSimpleGlob("*", "kube-system") // true
pathx.MatchSimpleGlob("kube-*", "kube-system") // true
pathx.MatchSimpleGlob("kube-*", "")        // false(空串不匹配)
pathx.MatchSimpleGlob("a*b*c", "axb*c")    // true(第二颗星按字面量)

func MatchesPath

func MatchesPath(patterns []string, path string) bool

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"(文件名只要最后一段)

消毒规则(顺序即口径):

  1. **剥掉目录成分**:取最后一段,"/" 与 "\" 都算分隔符(跨平台同判)。
  2. **丢弃控制字符**:r < 0x20 或 r == 0x7f。
  3. **Windows 保留字符**(`: * ? " < > |`)替换为下划线 `_`。
  4. **去首尾空白与结尾的点**:结尾的点与空白在 Windows 上是目录项陷阱 ("file." 与 "file " 会指向 "file"),首部空白无此含义故只去空白。
  5. **空名 / 纯符号名回退**:清洗后为空,或通篇没有字母与数字("***" → "___") ⇒ 回退为 fallback(缺省 "file")。纯符号名落盘无碍但对展示无意义, 直接回退比留一串下划线干净。
  6. **超长截断**:按**字节**上限截断,且**保留扩展名**;截断点回落到 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

func SanitizeRel(p string) (cleaned string, ok bool)

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

func SanitizeRelStrict(p string) (cleaned string, ok bool)

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 字符。

Jump to

Keyboard shortcuts

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