Documentation
¶
Overview ¶
Package pathologize produces file and path names that are safe to use on every modern operating system and file system — not just the one the code happens to be running on.
The standard library (path/filepath, os) validates and manipulates paths against the rules of the host OS. That is exactly right when a file never leaves the machine that created it. But files rarely stay put anymore: they travel through network shares (SMB, NFS), cloud storage (S3, GCS, Azure Blob), and sync tools (Dropbox, Syncthing, Google Drive, iCloud, OneDrive). A name that is perfectly legal on Linux — "aux", "report:final", "notes." — can break, get silently renamed, or fail to sync the moment it reaches a Windows client or a case-insensitive file system.
Pathologize takes the union of the restrictions across modern systems and applies all of them at once, so a name it approves is portable everywhere. It is intentionally restrictive: it rejects names that are too long, contain characters invalid on any modern OS, or collide with reserved device names (CON, NUL, COM1, ...) even when the host OS would accept them. It does not impose obsolete constraints (such as DOS 8.3 limits) that no modern system still enforces.
Use Clean on a single file or directory name, CleanPath on a full path, IsClean to test whether a name is already portable, and Join to combine a trusted root directory with untrusted path parts without letting them escape it.
See the Wikipedia article on reserved filenames for background: https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Clean ¶
Clean returns a version of filename that is safe to use on every modern operating and file system. It strips characters that are invalid on any modern OS, replaces invalid UTF-8 sequences with U+FFFD, trims leading/trailing whitespace and trailing dots, truncates to the maximum length, and defuses reserved device names (CON, NUL, COM1, ...) by appending an underscore. If nothing usable remains, it returns a default name so the result is never empty.
Clean is idempotent: its result is always itself clean, so IsClean(Clean(x)) is true for any x. The result is always valid UTF-8, never empty, and never longer than 255 bytes.
Clean operates on a single path component. To clean a full path while preserving its directory structure, use CleanPath.
Example ¶
package main
import (
"fmt"
"github.com/spf13/pathologize"
)
func main() {
fmt.Println(pathologize.Clean("report:final.txt"))
fmt.Println(pathologize.Clean("CON.txt"))
fmt.Println(pathologize.Clean(" notes. "))
}
Output: reportfinal.txt CON_.txt notes
func CleanPath ¶
CleanPath mirrors the lexical semantics of path.Clean — collapsing "//", resolving "." and internal "..", dropping a trailing separator — while additionally sanitizing each component with Clean. Both '/' and '\' are treated as separators (output always uses '/'). A leading volume is preserved: a Windows drive prefix such as "C:" passes through verbatim, and a UNC host+share prefix such as \\server\share is preserved with its separators normalized ("//server/share"), a form Windows accepts; the host and share names are sanitized with Clean like every other component.
CleanPath makes a path VALID, not SAFE. Like path.Clean, it preserves a leading ".." in a relative path and preserves an absolute path as absolute — it does not neutralize path traversal or contain the result under any root. CleanPath is idempotent on an already-valid path (CleanPath(p) == p) and is not a defense against untrusted input; for that, sanitize untrusted values with Clean, or use Join to safely combine a trusted root with untrusted parts.
CleanPath limits each component to 255 bytes but places no limit on the total path length. Some systems limit total length — notably Windows APIs default to 260 characters unless long paths are enabled — and callers whose paths may approach such limits must enforce them themselves, since only the caller knows the full path a component will end up in.
Example ¶
package main
import (
"fmt"
"github.com/spf13/pathologize"
)
func main() {
fmt.Println(pathologize.CleanPath(`C:/Users/dir:e*c?t<o>r|y/CON..`))
}
Output: C:/Users/directory/CON_
func IsClean ¶
IsClean reports whether filename is already safe to use across all modern operating and file systems — that is, whether Clean would leave it unchanged. Use it to detect names that need attention (for example, to warn a user) without rewriting them.
Example ¶
package main
import (
"fmt"
"github.com/spf13/pathologize"
)
func main() {
fmt.Println(pathologize.IsClean("notes.txt"))
fmt.Println(pathologize.IsClean("CON.."))
}
Output: true false
func Join ¶ added in v1.1.0
Join safely combines a trusted root with one or more untrusted path parts. root is passed through verbatim — including any separators, drive prefix, or trailing slash — and is never sanitized; it must come from a trusted source such as a configured destination directory. Each part is fully sanitized: it is split on '/' and '\', and structural segments (empty, ".", "..") are dropped; all other segments are cleaned with Clean so invalid characters and reserved names are neutralized. If every segment of a part is dropped, the part contributes the default name so it still yields exactly one segment. This guarantees the result stays lexically under root — a part cannot escape it or inject an absolute path — which is what CleanPath, by design, does not guarantee.
If no parts are given, root is returned unchanged.
Example ¶
package main
import (
"fmt"
"github.com/spf13/pathologize"
)
func main() {
fmt.Println(pathologize.Join("/srv/uploads", "../../etc/passwd"))
}
Output: /srv/uploads/etc/passwd
Types ¶
This section is empty.