Encode checkpoint and debug path identities
This commit is contained in:
@@ -10,6 +10,38 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EncodePathComponent returns a filesystem-safe, injective representation of
|
||||
// one logical path component.
|
||||
func EncodePathComponent(value string) string {
|
||||
if value == "" {
|
||||
return "%"
|
||||
}
|
||||
|
||||
const hexadecimal = "0123456789ABCDEF"
|
||||
var out strings.Builder
|
||||
for index := 0; index < len(value); index++ {
|
||||
byteValue := value[index]
|
||||
switch {
|
||||
case byteValue >= 'a' && byteValue <= 'z', byteValue >= 'A' && byteValue <= 'Z', byteValue >= '0' && byteValue <= '9', byteValue == '-', byteValue == '_':
|
||||
out.WriteByte(byteValue)
|
||||
case byteValue == '.' && safePathDot(value, index):
|
||||
out.WriteByte(byteValue)
|
||||
default:
|
||||
out.WriteByte('%')
|
||||
out.WriteByte(hexadecimal[byteValue>>4])
|
||||
out.WriteByte(hexadecimal[byteValue&0x0f])
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func safePathDot(value string, index int) bool {
|
||||
if value == "." || value == ".." {
|
||||
return false
|
||||
}
|
||||
return (index == 0 || value[index-1] != '.') && (index+1 == len(value) || value[index+1] != '.')
|
||||
}
|
||||
|
||||
func SafePath(root, name string) (string, error) {
|
||||
root = strings.TrimSpace(root)
|
||||
if root == "" {
|
||||
|
||||
@@ -15,6 +15,39 @@ func TestSafePathRejectsUnsafeNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodePathComponentIsInjectiveAndSafe(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
seen := make(map[string]string)
|
||||
for _, test := range []struct {
|
||||
value string
|
||||
want string
|
||||
}{
|
||||
{value: "", want: "%"},
|
||||
{value: ".", want: "%2E"},
|
||||
{value: "..", want: "%2E%2E"},
|
||||
{value: "_", want: "_"},
|
||||
{value: "a..b", want: "a%2E%2Eb"},
|
||||
{value: "safe.identifier-9", want: "safe.identifier-9"},
|
||||
{value: "left/right", want: "left%2Fright"},
|
||||
{value: "%", want: "%25"},
|
||||
{value: "~", want: "%7E"},
|
||||
{value: " a ", want: "%20a%20"},
|
||||
{value: "é", want: "%C3%A9"},
|
||||
} {
|
||||
got := EncodePathComponent(test.value)
|
||||
if got != test.want {
|
||||
t.Errorf("EncodePathComponent(%q) = %q, want %q", test.value, got, test.want)
|
||||
}
|
||||
if previous, ok := seen[got]; ok {
|
||||
t.Errorf("EncodePathComponent(%q) = %q, collides with %q", test.value, got, previous)
|
||||
}
|
||||
seen[got] = test.value
|
||||
if _, err := SafePath(root, "components/"+got); err != nil {
|
||||
t.Errorf("EncodePathComponent(%q) produced unsafe component %q: %v", test.value, got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBytesIsAtomicAndUsesRequestedModes(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := WriteBytes(root, "nested/value", []byte("value"), 0o700, 0o600); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user