Help us shape the future of Go! Take the 2026 Go Developer Survey

pathx

package
v0.25.0 Latest Latest
Warning

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

Go to latest
Published: Oct 24, 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 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