88 "os"
99 "path/filepath"
1010 "regexp"
11+ "strconv"
1112 "strings"
1213
1314 "golang.org/x/sys/unix"
@@ -39,7 +40,7 @@ const (
3940 // (9) mount source: filesystem specific information or "none".
4041 mountinfoMountSource = 9
4142 // (10) super options: per super block options.
42- // mountinfoSuperOptions = 10
43+ mountinfoSuperOptions = 10
4344)
4445
4546// Stat returns the mountpoint's stat information.
@@ -53,7 +54,8 @@ func mounts() ([]Mount, []string, error) {
5354 filename := "/proc/self/mountinfo"
5455 lines , err := readLines (filename )
5556 if err != nil {
56- return nil , nil , err
57+ // wrapcheck: add context to the error.
58+ return nil , nil , fmt .Errorf ("reading mountinfo %q: %w" , filename , err )
5759 }
5860
5961 ret := make ([]Mount , 0 , len (lines ))
@@ -105,9 +107,9 @@ func mounts() ([]Mount, []string, error) {
105107 }
106108 d .DeviceType = deviceType (d )
107109
108- // resolve /dev/mapper/* device names
110+ // Resolve /dev/mapper/* device names.
109111 if strings .HasPrefix (d .Device , "/dev/mapper/" ) {
110- re := regexp .MustCompile (`^\ /dev\ /mapper\ /(.*)-(.*)` )
112+ re := regexp .MustCompile (`^/dev/mapper/(.*)-(.*)` )
111113 match := re .FindAllStringSubmatch (d .Device , - 1 )
112114 if len (match ) > 0 && len (match [0 ]) == 3 {
113115 d .Device = filepath .Join ("/dev" , match [0 ][1 ], match [0 ][2 ])
@@ -120,6 +122,51 @@ func mounts() ([]Mount, []string, error) {
120122 return ret , warnings , nil
121123}
122124
125+ // splitMountInfoFields splits a mountinfo line into its fields.
126+ // It treats spaces and tabs as field separators and decodes certain octal escapes.
127+ func splitMountInfoFields (line string ) []string {
128+ var fields []string
129+ var buf strings.Builder
130+
131+ for i := 0 ; i < len (line ); i ++ {
132+ c := line [i ]
133+
134+ // Treat both space and tab as separators
135+ if c == ' ' || c == '\t' {
136+ if buf .Len () > 0 {
137+ fields = append (fields , buf .String ())
138+ buf .Reset ()
139+ }
140+ continue
141+ }
142+
143+ if c == '\\' && i + 3 < len (line ) {
144+ oct := line [i + 1 : i + 4 ]
145+ if v , err := strconv .ParseInt (oct , 8 , 0 ); err == nil {
146+ switch byte (v ) {
147+ case ' ' , '\t' , '\n' :
148+ buf .WriteByte (byte (v ))
149+ i += 3
150+ continue
151+ default :
152+ // keep unknown escapes as-is
153+ buf .WriteString ("\\ " + oct )
154+ i += 3
155+ continue
156+ }
157+ }
158+ }
159+
160+ buf .WriteByte (c )
161+ }
162+
163+ if buf .Len () > 0 {
164+ fields = append (fields , buf .String ())
165+ }
166+
167+ return fields
168+ }
169+
123170// parseMountInfoLine parses a line of /proc/self/mountinfo and returns the
124171// amount of parsed fields and their values.
125172func parseMountInfoLine (line string ) (int , [11 ]string ) {
@@ -130,43 +177,64 @@ func parseMountInfoLine(line string) (int, [11]string) {
130177 return 0 , fields
131178 }
132179
180+ all := splitMountInfoFields (line )
181+
133182 var i int
134- for _ , f := range strings .Fields (line ) {
135- // when parsing the optional fields, loop until we find the separator
183+ sawSep := false
184+ sawSup := false
185+
186+ for _ , f := range all {
187+ if i >= len (fields ) {
188+ break
189+ }
190+
136191 if i == mountinfoOptionalFields {
137- // (6) optional fields: zero or more fields of the form
138- // "tag[:value]"; see below.
139- // (7) separator: the end of the optional fields is marked
140- // by a single hyphen.
192+ // (6) optional fields: zero or more fields of the form "tag[:value]"; see below.
193+ // (7) separator: the end of the optional fields is marked by a single hyphen.
141194 if f != "-" {
142- if fields [i ] == "" {
143- fields [i ] += f
144- } else {
145- fields [i ] += " " + f
146- }
147-
148- // keep reading until we reach the separator
195+ // Join tokens with spaces for mountinfoOptionalFields.
196+ fields [i ] = strings .TrimSpace (fields [i ] + " " + f )
149197 continue
150198 }
151-
152- // separator found, continue parsing
199+ // Found separator.
200+ sawSep = true
153201 i ++
202+ fields [i ] = f
203+ i ++
204+ continue
205+ }
206+
207+ if i == mountinfoSuperOptions {
208+ // join tokens with spaces for WSL2 path=... they are splitted around spaces.
209+ fields [i ] = strings .TrimSpace (fields [i ] + " " + f )
210+ sawSup = true
211+ continue
154212 }
155213
214+ // Default case: copy with unescape for certain fields
156215 switch i {
157- case mountinfoMountPoint :
158- fallthrough
159- case mountinfoMountSource :
160- fallthrough
161- case mountinfoFsType :
216+ case mountinfoMountPoint , mountinfoMountSource , mountinfoFsType :
162217 fields [i ] = unescapeFstab (f )
163-
164218 default :
165219 fields [i ] = f
166220 }
221+ i ++
222+ }
167223
224+ // Handle malformed line (no "-" found).
225+ if ! sawSep && len (all ) > mountinfoOptionalFields {
226+ i = mountinfoOptionalFields
227+ }
228+
229+ // When super options are present, the index is one less than 11.
230+ if sawSup {
168231 i ++
169232 }
170233
234+ // clear trailing empties.
235+ for j := i + 1 ; j < len (fields ); j ++ {
236+ fields [j ] = ""
237+ }
238+
171239 return i , fields
172240}
0 commit comments