Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mountinfo: implement unescape() using strings.Replacer() #143

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 23 additions & 63 deletions mountinfo/mountinfo_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ func GetMountsFromReader(r io.Reader, filter FilterFunc) ([]*Info, error) {
s := bufio.NewScanner(r)
out := []*Info{}
for s.Scan() {
var err error

/*
See http://man7.org/linux/man-pages/man5/proc.5.html

Expand Down Expand Up @@ -85,29 +83,15 @@ func GetMountsFromReader(r io.Reader, filter FilterFunc) ([]*Info, error) {
Parent: toInt(fields[1]),
Major: toInt(major),
Minor: toInt(minor),
Root: unescape(fields[3]),
Mountpoint: unescape(fields[4]),
Options: fields[5],
Optional: strings.Join(fields[6:sepIdx], " "), // zero or more optional fields
FSType: unescape(fields[sepIdx+1]),
Source: unescape(fields[sepIdx+2]),
VFSOptions: fields[sepIdx+3],
}

p.Mountpoint, err = unescape(fields[4])
if err != nil {
return nil, fmt.Errorf("parsing '%s' failed: mount point: %w", fields[4], err)
}
p.FSType, err = unescape(fields[sepIdx+1])
if err != nil {
return nil, fmt.Errorf("parsing '%s' failed: fstype: %w", fields[sepIdx+1], err)
}
p.Source, err = unescape(fields[sepIdx+2])
if err != nil {
return nil, fmt.Errorf("parsing '%s' failed: source: %w", fields[sepIdx+2], err)
}

p.Root, err = unescape(fields[3])
if err != nil {
return nil, fmt.Errorf("parsing '%s' failed: root: %w", fields[3], err)
}

// Run the filter after parsing all fields.
var skip, stop bool
if filter != nil {
Expand Down Expand Up @@ -188,6 +172,22 @@ func PidMountInfo(pid int) ([]*Info, error) {
return GetMountsFromReader(f, nil)
}

// Linux /proc/mounts shows current mounts.
// Same format as /etc/fstab. Quoting getmntent(3):
//
// Since fields in the mtab and fstab files are separated by whitespace,
// octal escapes are used to represent the four characters space (\040),
// tab (\011), newline (\012) and backslash (\134) in those files when
// they occur in one of the four strings in a mntent structure.
//
// http://linux.die.net/man/3/getmntent
var fstabUnescape = strings.NewReplacer(
`\040`, "\040",
`\011`, "\011",
`\012`, "\012",
`\134`, "\134",
)

// A few specific characters in mountinfo path entries (root and mountpoint)
// are escaped using a backslash followed by a character's ascii code in octal.
//
Expand All @@ -197,53 +197,13 @@ func PidMountInfo(pid int) ([]*Info, error) {
// backslash (aka \\) -- as \134
//
// This function converts path from mountinfo back, i.e. it unescapes the above sequences.
func unescape(path string) (string, error) {
func unescape(path string) string {
// try to avoid copying
if strings.IndexByte(path, '\\') == -1 {
return path, nil
}

// The following code is UTF-8 transparent as it only looks for some
// specific characters (backslash and 0..7) with values < utf8.RuneSelf,
// and everything else is passed through as is.
buf := make([]byte, len(path))
bufLen := 0
for i := 0; i < len(path); i++ {
if path[i] != '\\' {
buf[bufLen] = path[i]
bufLen++
continue
}
s := path[i:]
if len(s) < 4 {
// too short
return "", fmt.Errorf("bad escape sequence %q: too short", s)
}
c := s[1]
switch c {
case '0', '1', '2', '3', '4', '5', '6', '7':
v := c - '0'
for j := 2; j < 4; j++ { // one digit already; two more
if s[j] < '0' || s[j] > '7' {
return "", fmt.Errorf("bad escape sequence %q: not a digit", s[:3])
}
x := s[j] - '0'
v = (v << 3) | x
}
if v > 255 {
return "", fmt.Errorf("bad escape sequence %q: out of range" + s[:3])
}
buf[bufLen] = v
bufLen++
i += 3
continue
default:
return "", fmt.Errorf("bad escape sequence %q: not a digit" + s[:3])

}
return path
}

return string(buf[:bufLen]), nil
return fstabUnescape.Replace(path)
}

// toInt converts a string to an int, and ignores any numbers parsing errors,
Expand Down
71 changes: 43 additions & 28 deletions mountinfo/mountinfo_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -733,41 +733,56 @@ func TestParseMountinfoExtraCases(t *testing.T) {
func TestUnescape(t *testing.T) {
testCases := []struct {
input, output string
isErr bool
}{
{"", "", false},
{"/", "/", false},
{"/some/longer/path", "/some/longer/path", false},
{"/path\\040with\\040spaces", "/path with spaces", false},
{"/path/with\\134backslash", "/path/with\\backslash", false},
{"/tab\\011in/path", "/tab\tin/path", false},
{`/path/"with'quotes`, `/path/"with'quotes`, false},
{`/path/"with'quotes,\040space,\011tab`, `/path/"with'quotes, space, tab`, false},
{`\12`, "", true},
{`\134`, `\`, false},
{`"'"'"'`, `"'"'"'`, false},
{`/\1345`, `/\5`, false},
{`/\12x`, "", true},
{`\0`, "", true},
{`\x`, "", true},
{"\\\\", "", true},
{"", ""},
{"/", "/"},
{"/some/longer/path", "/some/longer/path"},
{"/path\\040with\\040spaces", "/path with spaces"},
{"/path/with\\134backslash", "/path/with\\backslash"},
{"/tab\\011in/path", "/tab\tin/path"},
{`/path/"with'quotes`, `/path/"with'quotes`},
{`/path/"with'quotes,\040space,\011tab`, `/path/"with'quotes, space, tab`},
{`\134`, `\`},
{`"'"'"'`, `"'"'"'`},
{`/\1345`, `/\5`},
}

for _, tc := range testCases {
res, err := unescape(tc.input)
if tc.isErr == true {
if err == nil {
t.Errorf("Input %q, want error, got nil", tc.input)
}
// no more checks
continue
}
res := unescape(tc.input)
if res != tc.output {
t.Errorf("Input %q, want %q, got %q", tc.input, tc.output, res)
}
if err != nil {
t.Errorf("Input %q, want nil, got error %v", tc.input, err)
continue
}
}

func BenchmarkUnescape(b *testing.B) {
// allow the replacer to be built once: https://github.com/golang/go/blob/go1.16.7/src/strings/replace.go#L96
_ = unescape("/path\\040with\\040spaces")

testCases := []string{
"",
"/",
"/some/longer/path",
"/path\\040with\\040spaces",
"/path/with\\134backslash",
"/tab\\011in/path",
`/path/"with'quotes`,
`/path/"with'quotes,\040space,\011tab`,
`\12`,
`\134`,
`"'"'"'`,
`/\1345`,
`/\12x`,
`\0`,
`\x`,
"\\\\",
}

b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for x := 0; x < len(testCases); x++ {
_ = unescape(testCases[x])
}
}
}
Loading