github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/osutil/mountinfo_linux.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package osutil
    21  
    22  import (
    23  	"bufio"
    24  	"bytes"
    25  	"fmt"
    26  	"io"
    27  	"os"
    28  	"sort"
    29  	"strconv"
    30  	"strings"
    31  )
    32  
    33  // MountInfoEntry contains data from /proc/$PID/mountinfo
    34  //
    35  // For details please refer to mountinfo documentation at
    36  // https://www.kernel.org/doc/Documentation/filesystems/proc.txt
    37  type MountInfoEntry struct {
    38  	MountID        int
    39  	ParentID       int
    40  	DevMajor       int
    41  	DevMinor       int
    42  	Root           string
    43  	MountDir       string
    44  	MountOptions   map[string]string
    45  	OptionalFields []string
    46  	FsType         string
    47  	MountSource    string
    48  	SuperOptions   map[string]string
    49  }
    50  
    51  func flattenMap(m map[string]string) string {
    52  	keys := make([]string, 0, len(m))
    53  	for key := range m {
    54  		keys = append(keys, key)
    55  	}
    56  	sort.Strings(keys)
    57  	var buf bytes.Buffer
    58  	for i, key := range keys {
    59  		if i > 0 {
    60  			buf.WriteRune(',')
    61  		}
    62  		if m[key] != "" {
    63  			fmt.Fprintf(&buf, "%s=%s", escape(key), escape(m[key]))
    64  		} else {
    65  			buf.WriteString(escape(key))
    66  		}
    67  	}
    68  	return buf.String()
    69  }
    70  
    71  func flattenList(l []string) string {
    72  	var buf bytes.Buffer
    73  	for i, item := range l {
    74  		if i > 0 {
    75  			buf.WriteRune(',')
    76  		}
    77  		buf.WriteString(escape(item))
    78  	}
    79  	return buf.String()
    80  }
    81  
    82  func (mi *MountInfoEntry) String() string {
    83  	maybeSpace := " "
    84  	if len(mi.OptionalFields) == 0 {
    85  		maybeSpace = ""
    86  	}
    87  	return fmt.Sprintf("%d %d %d:%d %s %s %s %s%s- %s %s %s",
    88  		mi.MountID, mi.ParentID, mi.DevMajor, mi.DevMinor, escape(mi.Root),
    89  		escape(mi.MountDir), flattenMap(mi.MountOptions), flattenList(mi.OptionalFields),
    90  		maybeSpace, escape(mi.FsType), escape(mi.MountSource),
    91  		flattenMap(mi.SuperOptions))
    92  }
    93  
    94  var mountInfoMustMockInTests = true
    95  
    96  // LoadMountInfo loads list of mounted entries from /proc/self/mountinfo. This
    97  // can be mocked by using osutil.MockMountInfo to hard-code a specific mountinfo
    98  // file content to be loaded by this function
    99  func LoadMountInfo() ([]*MountInfoEntry, error) {
   100  	if mockedMountInfo != nil {
   101  		return ReadMountInfo(bytes.NewBufferString(*mockedMountInfo))
   102  	}
   103  	if IsTestBinary() && mountInfoMustMockInTests {
   104  		// if we are in testing and we didn't mock a mountinfo panic, since the
   105  		// mountinfo is used in many places and really should be mocked for tests
   106  		panic("/proc/self/mountinfo must be mocked in tests")
   107  	}
   108  
   109  	f, err := os.Open(procSelfMountInfo)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	defer f.Close()
   114  	return ReadMountInfo(f)
   115  }
   116  
   117  // ReadMountInfo reads and parses a mountinfo file.
   118  func ReadMountInfo(reader io.Reader) ([]*MountInfoEntry, error) {
   119  	scanner := bufio.NewScanner(reader)
   120  	var entries []*MountInfoEntry
   121  	for scanner.Scan() {
   122  		s := scanner.Text()
   123  		entry, err := ParseMountInfoEntry(s)
   124  		if err != nil {
   125  			return nil, err
   126  		}
   127  		entries = append(entries, entry)
   128  	}
   129  	if err := scanner.Err(); err != nil {
   130  		return nil, err
   131  	}
   132  	return entries, nil
   133  }
   134  
   135  // ParseMountInfoEntry parses a single line of /proc/$PID/mountinfo file.
   136  func ParseMountInfoEntry(s string) (*MountInfoEntry, error) {
   137  	var e MountInfoEntry
   138  	var err error
   139  	fields := strings.FieldsFunc(s, func(r rune) bool { return r == ' ' })
   140  
   141  	// The format is variable-length, but at least 10 fields are mandatory.
   142  	// The (7) below is a list of optional field which is terminated with (8).
   143  	// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
   144  	// (1)(2)(3)   (4)   (5)      (6)      (7)   (8) (9)   (10)         (11)
   145  	if len(fields) < 10 {
   146  		return nil, fmt.Errorf("incorrect number of fields, expected at least 10 but found %d", len(fields))
   147  	}
   148  	// Parse MountID (decimal number).
   149  	e.MountID, err = strconv.Atoi(fields[0])
   150  	if err != nil {
   151  		return nil, fmt.Errorf("cannot parse mount ID: %q", fields[0])
   152  	}
   153  	// Parse ParentID (decimal number).
   154  	e.ParentID, err = strconv.Atoi(fields[1])
   155  	if err != nil {
   156  		return nil, fmt.Errorf("cannot parse parent mount ID: %q", fields[1])
   157  	}
   158  	// Parses DevMajor:DevMinor pair (decimal numbers separated by colon).
   159  	subFields := strings.FieldsFunc(fields[2], func(r rune) bool { return r == ':' })
   160  	if len(subFields) != 2 {
   161  		return nil, fmt.Errorf("cannot parse device major:minor number pair: %q", fields[2])
   162  	}
   163  	e.DevMajor, err = strconv.Atoi(subFields[0])
   164  	if err != nil {
   165  		return nil, fmt.Errorf("cannot parse device major number: %q", subFields[0])
   166  	}
   167  	e.DevMinor, err = strconv.Atoi(subFields[1])
   168  	if err != nil {
   169  		return nil, fmt.Errorf("cannot parse device minor number: %q", subFields[1])
   170  	}
   171  	// NOTE: All string fields use the same escape/unescape logic as fstab files.
   172  	// Parse Root, MountDir and MountOptions fields.
   173  	e.Root = unescape(fields[3])
   174  	e.MountDir = unescape(fields[4])
   175  	e.MountOptions = parseMountOpts(unescape(fields[5]))
   176  	// Optional fields are terminated with a "-" value and start
   177  	// after the mount options field. Skip ahead until we see the "-"
   178  	// marker.
   179  	var i int
   180  	for i = 6; i < len(fields) && fields[i] != "-"; i++ {
   181  	}
   182  	if i == len(fields) {
   183  		return nil, fmt.Errorf("list of optional fields is not terminated properly")
   184  	}
   185  	e.OptionalFields = fields[6:i]
   186  	for j := range e.OptionalFields {
   187  		e.OptionalFields[j] = unescape(e.OptionalFields[j])
   188  	}
   189  	// Parse the last three fixed fields.
   190  	tailFields := fields[i+1:]
   191  	if len(tailFields) != 3 {
   192  		return nil, fmt.Errorf("incorrect number of tail fields, expected 3 but found %d", len(tailFields))
   193  	}
   194  	e.FsType = unescape(tailFields[0])
   195  	e.MountSource = unescape(tailFields[1])
   196  	e.SuperOptions = parseMountOpts(unescape(tailFields[2]))
   197  	return &e, nil
   198  }
   199  
   200  func parseMountOpts(opts string) map[string]string {
   201  	result := make(map[string]string)
   202  	for _, opt := range strings.Split(opts, ",") {
   203  		keyValue := strings.SplitN(opt, "=", 2)
   204  		key := keyValue[0]
   205  		if len(keyValue) == 2 {
   206  			value := keyValue[1]
   207  			result[key] = value
   208  		} else {
   209  			result[key] = ""
   210  		}
   211  	}
   212  	return result
   213  }