Преглед изворни кода

Bump libcontainer dep

Docker-DCO-1.1-Signed-off-by: Andrew Page <admwiggin@gmail.com> (github: tianon)
Tianon Gravi пре 11 година
родитељ
комит
f4db3985d9

+ 1 - 1
hack/vendor.sh

@@ -63,4 +63,4 @@ mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar
 
 clone git github.com/godbus/dbus v1
 clone git github.com/coreos/go-systemd v2
-clone git github.com/docker/libcontainer e6a43c1c2b9f769deb96348a0a93417cd48a36d8
+clone git github.com/docker/libcontainer bc06326a5e7decdc4191d1367de8439b9d83c450

+ 3 - 3
vendor/src/github.com/docker/libcontainer/.travis.yml

@@ -21,10 +21,10 @@ install:
     - if [ -z "$TRAVIS_GLOBAL_WTF" ]; then go env; fi
     - go get -d -v ./...
     - if [ "$TRAVIS_GLOBAL_WTF" ]; then
-          export DOCKER_PATH="${GOPATH%%:*}/src/github.com/dotcloud/docker";
+          export DOCKER_PATH="${GOPATH%%:*}/src/github.com/docker/docker";
           mkdir -p "$DOCKER_PATH/hack/make";
-          ( cd "$DOCKER_PATH/hack/make" && wget -c 'https://raw.githubusercontent.com/dotcloud/docker/master/hack/make/'{.validate,validate-dco,validate-gofmt} );
-          sed -i 's!dotcloud/docker!docker/libcontainer!' "$DOCKER_PATH/hack/make/.validate";
+          ( cd "$DOCKER_PATH/hack/make" && wget -c 'https://raw.githubusercontent.com/docker/docker/master/hack/make/'{.validate,validate-dco,validate-gofmt} );
+          sed -i 's!docker/docker!docker/libcontainer!' "$DOCKER_PATH/hack/make/.validate";
       fi
 
 script:

+ 1 - 1
vendor/src/github.com/docker/libcontainer/CONTRIBUTORS_GUIDE.md

@@ -176,7 +176,7 @@ One way to automate this, is customise your get ``commit.template`` by adding
 a ``prepare-commit-msg`` hook to your libcontainer checkout:
 
 ```
-curl -o .git/hooks/prepare-commit-msg https://raw.githubusercontent.com/dotcloud/docker/master/contrib/prepare-commit-msg.hook && chmod +x .git/hooks/prepare-commit-msg
+curl -o .git/hooks/prepare-commit-msg https://raw.githubusercontent.com/docker/docker/master/contrib/prepare-commit-msg.hook && chmod +x .git/hooks/prepare-commit-msg
 ```
 
 * Note: the above script expects to find your GitHub user name in ``git config --get github.user``

+ 4 - 0
vendor/src/github.com/docker/libcontainer/cgroups/fs/apply_raw.go

@@ -150,6 +150,10 @@ func (raw *data) parent(subsystem string) (string, error) {
 }
 
 func (raw *data) path(subsystem string) (string, error) {
+	// If the cgroup name/path is absolute do not look relative to the cgroup of the init process.
+	if filepath.IsAbs(raw.cgroup) {
+		return filepath.Join(raw.root, subsystem, raw.cgroup), nil
+	}
 	parent, err := raw.parent(subsystem)
 	if err != nil {
 		return "", err

+ 19 - 0
vendor/src/github.com/docker/libcontainer/cgroups/fs/utils_test.go

@@ -5,6 +5,8 @@ import (
 	"os"
 	"path/filepath"
 	"testing"
+
+	"github.com/docker/libcontainer/cgroups"
 )
 
 const (
@@ -66,3 +68,20 @@ func TestGetCgroupParamsInt(t *testing.T) {
 		t.Fatal("Expecting error, got none")
 	}
 }
+
+func TestAbsolutePathHandling(t *testing.T) {
+	testCgroup := cgroups.Cgroup{
+		Name:   "bar",
+		Parent: "/foo",
+	}
+	cgroupData := data{
+		root:   "/sys/fs/cgroup",
+		cgroup: "/foo/bar",
+		c:      &testCgroup,
+		pid:    1,
+	}
+	expectedPath := filepath.Join(cgroupData.root, "cpu", testCgroup.Parent, testCgroup.Name)
+	if path, err := cgroupData.path("cpu"); path != expectedPath || err != nil {
+		t.Fatalf("expected path %s but got %s %s", expectedPath, path, err)
+	}
+}

+ 8 - 1
vendor/src/github.com/docker/libcontainer/label/label.go

@@ -2,6 +2,13 @@
 
 package label
 
+// InitLabels returns the process label and file labels to be used within
+// the container.  A list of options can be passed into this function to alter
+// the labels.
+func InitLabels(options []string) (string, string, error) {
+	return "", "", nil
+}
+
 func GenLabels(options string) (string, string, error) {
 	return "", "", nil
 }
@@ -22,7 +29,7 @@ func Relabel(path string, fileLabel string, relabel string) error {
 	return nil
 }
 
-func GetPidCon(pid int) (string, error) {
+func GetPidLabel(pid int) (string, error) {
 	return "", nil
 }
 

+ 43 - 13
vendor/src/github.com/docker/libcontainer/label/label_selinux.go

@@ -9,30 +9,49 @@ import (
 	"github.com/docker/libcontainer/selinux"
 )
 
-func GenLabels(options string) (string, string, error) {
+// InitLabels returns the process label and file labels to be used within
+// the container.  A list of options can be passed into this function to alter
+// the labels.  The labels returned will include a random MCS String, that is
+// guaranteed to be unique.
+func InitLabels(options []string) (string, string, error) {
 	if !selinux.SelinuxEnabled() {
 		return "", "", nil
 	}
 	var err error
 	processLabel, mountLabel := selinux.GetLxcContexts()
 	if processLabel != "" {
-		var (
-			s = strings.Fields(options)
-			l = len(s)
-		)
-		if l > 0 {
-			pcon := selinux.NewContext(processLabel)
-			for i := 0; i < l; i++ {
-				o := strings.Split(s[i], "=")
-				pcon[o[0]] = o[1]
+		pcon := selinux.NewContext(processLabel)
+		mcon := selinux.NewContext(mountLabel)
+		for _, opt := range options {
+			if opt == "disable" {
+				return "", "", nil
+			}
+			if i := strings.Index(opt, ":"); i == -1 {
+				return "", "", fmt.Errorf("Bad SELinux Option")
+			}
+			con := strings.SplitN(opt, ":", 2)
+			pcon[con[0]] = con[1]
+			if con[0] == "level" || con[0] == "user" {
+				mcon[con[0]] = con[1]
 			}
-			processLabel = pcon.Get()
-			mountLabel, err = selinux.CopyLevel(processLabel, mountLabel)
 		}
+		processLabel = pcon.Get()
+		mountLabel = mcon.Get()
 	}
 	return processLabel, mountLabel, err
 }
 
+// DEPRECATED: The GenLabels function is only to be used during the transition to the official API.
+func GenLabels(options string) (string, string, error) {
+	return InitLabels(strings.Fields(options))
+}
+
+// FormatMountLabel returns a string to be used by the mount command.
+// The format of this string will be used to alter the labeling of the mountpoint.
+// The string returned is suitable to be used as the options field of the mount command.
+// If you need to have additional mount point options, you can pass them in as
+// the first parameter.  Second parameter is the label that you wish to apply
+// to all content in the mount point.
 func FormatMountLabel(src, mountLabel string) string {
 	if mountLabel != "" {
 		switch src {
@@ -45,6 +64,8 @@ func FormatMountLabel(src, mountLabel string) string {
 	return src
 }
 
+// SetProcessLabel takes a process label and tells the kernel to assign the
+// label to the next program executed by the current process.
 func SetProcessLabel(processLabel string) error {
 	if selinux.SelinuxEnabled() {
 		return selinux.Setexeccon(processLabel)
@@ -52,6 +73,9 @@ func SetProcessLabel(processLabel string) error {
 	return nil
 }
 
+// GetProcessLabel returns the process label that the kernel will assign
+// to the next program executed by the current process.  If "" is returned
+// this indicates that the default labeling will happen for the process.
 func GetProcessLabel() (string, error) {
 	if selinux.SelinuxEnabled() {
 		return selinux.Getexeccon()
@@ -59,6 +83,7 @@ func GetProcessLabel() (string, error) {
 	return "", nil
 }
 
+// SetFileLabel modifies the "path" label to the specified file label
 func SetFileLabel(path string, fileLabel string) error {
 	if selinux.SelinuxEnabled() && fileLabel != "" {
 		return selinux.Setfilecon(path, fileLabel)
@@ -83,17 +108,22 @@ func Relabel(path string, fileLabel string, relabel string) error {
 	return selinux.Chcon(path, fileLabel, true)
 }
 
-func GetPidCon(pid int) (string, error) {
+// GetPidLabel will return the label of the process running with the specified pid
+func GetPidLabel(pid int) (string, error) {
 	if !selinux.SelinuxEnabled() {
 		return "", nil
 	}
 	return selinux.Getpidcon(pid)
 }
 
+// Init initialises the labeling system
 func Init() {
 	selinux.SelinuxEnabled()
 }
 
+// ReserveLabel will record the fact that the MCS label has already been used.
+// This will prevent InitLabels from using the MCS label in a newly created
+// container
 func ReserveLabel(label string) error {
 	selinux.ReserveLabel(label)
 	return nil

+ 48 - 0
vendor/src/github.com/docker/libcontainer/label/label_selinux_test.go

@@ -0,0 +1,48 @@
+// +build selinux,linux
+
+package label
+
+import (
+	"testing"
+
+	"github.com/docker/libcontainer/selinux"
+)
+
+func TestInit(t *testing.T) {
+	if selinux.SelinuxEnabled() {
+		var testNull []string
+		plabel, mlabel, err := InitLabels(testNull)
+		if err != nil {
+			t.Log("InitLabels Failed")
+			t.Fatal(err)
+		}
+		testDisabled := []string{"disable"}
+		plabel, mlabel, err = InitLabels(testDisabled)
+		if err != nil {
+			t.Log("InitLabels Disabled Failed")
+			t.Fatal(err)
+		}
+		if plabel != "" {
+			t.Log("InitLabels Disabled Failed")
+			t.Fatal()
+		}
+		testUser := []string{"user:user_u", "role:user_r", "type:user_t", "level:s0:c1,c15"}
+		plabel, mlabel, err = InitLabels(testUser)
+		if err != nil {
+			t.Log("InitLabels User Failed")
+			t.Fatal(err)
+		}
+		if plabel != "user_u:user_r:user_t:s0:c1,c15" || mlabel != "user_u:object_r:svirt_sandbox_file_t:s0:c1,c15" {
+			t.Log("InitLabels User Failed")
+			t.Log(plabel, mlabel)
+			t.Fatal(err)
+		}
+
+		testBadData := []string{"user", "role:user_r", "type:user_t", "level:s0:c1,c15"}
+		plabel, mlabel, err = InitLabels(testBadData)
+		if err == nil {
+			t.Log("InitLabels Bad Failed")
+			t.Fatal(err)
+		}
+	}
+}

+ 10 - 3
vendor/src/github.com/docker/libcontainer/namespaces/init.go

@@ -9,7 +9,6 @@ import (
 	"strings"
 	"syscall"
 
-	"github.com/docker/docker/pkg/user"
 	"github.com/docker/libcontainer"
 	"github.com/docker/libcontainer/apparmor"
 	"github.com/docker/libcontainer/console"
@@ -21,6 +20,7 @@ import (
 	"github.com/docker/libcontainer/security/restrict"
 	"github.com/docker/libcontainer/syncpipe"
 	"github.com/docker/libcontainer/system"
+	"github.com/docker/libcontainer/user"
 	"github.com/docker/libcontainer/utils"
 )
 
@@ -119,7 +119,7 @@ func Init(container *libcontainer.Config, uncleanRootfs, consolePath string, syn
 		return fmt.Errorf("restore parent death signal %s", err)
 	}
 
-	return system.Execv(args[0], args[0:], container.Env)
+	return system.Execv(args[0], args[0:], os.Environ())
 }
 
 // RestoreParentDeathSignal sets the parent death signal to old.
@@ -152,7 +152,7 @@ func RestoreParentDeathSignal(old int) error {
 
 // SetupUser changes the groups, gid, and uid for the user inside the container
 func SetupUser(u string) error {
-	uid, gid, suppGids, err := user.GetUserGroupSupplementary(u, syscall.Getuid(), syscall.Getgid())
+	uid, gid, suppGids, home, err := user.GetUserGroupSupplementaryHome(u, syscall.Getuid(), syscall.Getgid(), "/")
 	if err != nil {
 		return fmt.Errorf("get supplementary groups %s", err)
 	}
@@ -169,6 +169,13 @@ func SetupUser(u string) error {
 		return fmt.Errorf("setuid %s", err)
 	}
 
+	// if we didn't get HOME already, set it based on the user's HOME
+	if envHome := os.Getenv("HOME"); envHome == "" {
+		if err := os.Setenv("HOME", home); err != nil {
+			return fmt.Errorf("set HOME %s", err)
+		}
+	}
+
 	return nil
 }
 

+ 11 - 18
vendor/src/github.com/docker/libcontainer/namespaces/nsenter.go

@@ -3,7 +3,6 @@
 package namespaces
 
 /*
-#include <dirent.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <linux/limits.h>
@@ -12,7 +11,6 @@ package namespaces
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sys/stat.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <getopt.h>
@@ -145,36 +143,31 @@ void nsenter() {
 	char ns_dir[PATH_MAX];
 	memset(ns_dir, 0, PATH_MAX);
 	snprintf(ns_dir, PATH_MAX - 1, "/proc/%d/ns/", init_pid);
-	struct dirent *dent;
-	DIR *dir = opendir(ns_dir);
-	if (dir == NULL) {
-		fprintf(stderr, "nsenter: Failed to open directory \"%s\" with error: \"%s\"\n", ns_dir, strerror(errno));
-		exit(1);
-	}
 
-	while((dent = readdir(dir)) != NULL) {
-		if(strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0 || strcmp(dent->d_name, "user") == 0) {
-			continue;
-		}
-
-		// Get and open the namespace for the init we are joining..
+	char* namespaces[] = {"ipc", "uts", "net", "pid", "mnt"};
+	const int num = sizeof(namespaces) / sizeof(char*);
+	int i;
+	for (i = 0; i < num; i++) {
 		char buf[PATH_MAX];
 		memset(buf, 0, PATH_MAX);
-		snprintf(buf, PATH_MAX - 1, "%s%s", ns_dir, dent->d_name);
+		snprintf(buf, PATH_MAX - 1, "%s%s", ns_dir, namespaces[i]);
 		int fd = open(buf, O_RDONLY);
 		if (fd == -1) {
-			fprintf(stderr, "nsenter: Failed to open ns file \"%s\" for ns \"%s\" with error: \"%s\"\n", buf, dent->d_name, strerror(errno));
+			// Ignore nonexistent namespaces.
+			if (errno == ENOENT)
+				continue;
+
+			fprintf(stderr, "nsenter: Failed to open ns file \"%s\" for ns \"%s\" with error: \"%s\"\n", buf, namespaces[i], strerror(errno));
 			exit(1);
 		}
 
 		// Set the namespace.
 		if (setns(fd, 0) == -1) {
-			fprintf(stderr, "nsenter: Failed to setns for \"%s\" with error: \"%s\"\n", dent->d_name, strerror(errno));
+			fprintf(stderr, "nsenter: Failed to setns for \"%s\" with error: \"%s\"\n", namespaces[i], strerror(errno));
 			exit(1);
 		}
 		close(fd);
 	}
-	closedir(dir);
 
 	// We must fork to actually enter the PID namespace.
 	int child = fork();

+ 1 - 1
vendor/src/github.com/docker/libcontainer/system/sysconfig.go

@@ -1,4 +1,4 @@
-// +build linux,cgo
+// +build cgo
 
 package system
 

+ 1 - 1
vendor/src/github.com/docker/libcontainer/system/sysconfig_notcgo.go

@@ -1,4 +1,4 @@
-// +build linux,!cgo
+// +build !cgo
 
 package system
 

+ 1 - 0
vendor/src/github.com/docker/libcontainer/user/MAINTAINERS

@@ -0,0 +1 @@
+Tianon Gravi <admwiggin@gmail.com> (@tianon)

+ 258 - 0
vendor/src/github.com/docker/libcontainer/user/user.go

@@ -0,0 +1,258 @@
+package user
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+	"strconv"
+	"strings"
+)
+
+const (
+	minId = 0
+	maxId = 1<<31 - 1 //for 32-bit systems compatibility
+)
+
+var (
+	ErrRange = fmt.Errorf("Uids and gids must be in range %d-%d", minId, maxId)
+)
+
+type User struct {
+	Name  string
+	Pass  string
+	Uid   int
+	Gid   int
+	Gecos string
+	Home  string
+	Shell string
+}
+
+type Group struct {
+	Name string
+	Pass string
+	Gid  int
+	List []string
+}
+
+func parseLine(line string, v ...interface{}) {
+	if line == "" {
+		return
+	}
+
+	parts := strings.Split(line, ":")
+	for i, p := range parts {
+		if len(v) <= i {
+			// if we have more "parts" than we have places to put them, bail for great "tolerance" of naughty configuration files
+			break
+		}
+
+		switch e := v[i].(type) {
+		case *string:
+			// "root", "adm", "/bin/bash"
+			*e = p
+		case *int:
+			// "0", "4", "1000"
+			// ignore string to int conversion errors, for great "tolerance" of naughty configuration files
+			*e, _ = strconv.Atoi(p)
+		case *[]string:
+			// "", "root", "root,adm,daemon"
+			if p != "" {
+				*e = strings.Split(p, ",")
+			} else {
+				*e = []string{}
+			}
+		default:
+			// panic, because this is a programming/logic error, not a runtime one
+			panic("parseLine expects only pointers!  argument " + strconv.Itoa(i) + " is not a pointer!")
+		}
+	}
+}
+
+func ParsePasswd() ([]*User, error) {
+	return ParsePasswdFilter(nil)
+}
+
+func ParsePasswdFilter(filter func(*User) bool) ([]*User, error) {
+	f, err := os.Open("/etc/passwd")
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return parsePasswdFile(f, filter)
+}
+
+func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
+	var (
+		s   = bufio.NewScanner(r)
+		out = []*User{}
+	)
+
+	for s.Scan() {
+		if err := s.Err(); err != nil {
+			return nil, err
+		}
+
+		text := strings.TrimSpace(s.Text())
+		if text == "" {
+			continue
+		}
+
+		// see: man 5 passwd
+		//  name:password:UID:GID:GECOS:directory:shell
+		// Name:Pass:Uid:Gid:Gecos:Home:Shell
+		//  root:x:0:0:root:/root:/bin/bash
+		//  adm:x:3:4:adm:/var/adm:/bin/false
+		p := &User{}
+		parseLine(
+			text,
+			&p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell,
+		)
+
+		if filter == nil || filter(p) {
+			out = append(out, p)
+		}
+	}
+
+	return out, nil
+}
+
+func ParseGroup() ([]*Group, error) {
+	return ParseGroupFilter(nil)
+}
+
+func ParseGroupFilter(filter func(*Group) bool) ([]*Group, error) {
+	f, err := os.Open("/etc/group")
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return parseGroupFile(f, filter)
+}
+
+func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
+	var (
+		s   = bufio.NewScanner(r)
+		out = []*Group{}
+	)
+
+	for s.Scan() {
+		if err := s.Err(); err != nil {
+			return nil, err
+		}
+
+		text := s.Text()
+		if text == "" {
+			continue
+		}
+
+		// see: man 5 group
+		//  group_name:password:GID:user_list
+		// Name:Pass:Gid:List
+		//  root:x:0:root
+		//  adm:x:4:root,adm,daemon
+		p := &Group{}
+		parseLine(
+			text,
+			&p.Name, &p.Pass, &p.Gid, &p.List,
+		)
+
+		if filter == nil || filter(p) {
+			out = append(out, p)
+		}
+	}
+
+	return out, nil
+}
+
+// Given a string like "user", "1000", "user:group", "1000:1000", returns the uid, gid, list of supplementary group IDs, and home directory, if available and/or applicable.
+func GetUserGroupSupplementaryHome(userSpec string, defaultUid, defaultGid int, defaultHome string) (int, int, []int, string, error) {
+	var (
+		uid      = defaultUid
+		gid      = defaultGid
+		suppGids = []int{}
+		home     = defaultHome
+
+		userArg, groupArg string
+	)
+
+	// allow for userArg to have either "user" syntax, or optionally "user:group" syntax
+	parseLine(userSpec, &userArg, &groupArg)
+
+	users, err := ParsePasswdFilter(func(u *User) bool {
+		if userArg == "" {
+			return u.Uid == uid
+		}
+		return u.Name == userArg || strconv.Itoa(u.Uid) == userArg
+	})
+	if err != nil && !os.IsNotExist(err) {
+		if userArg == "" {
+			userArg = strconv.Itoa(uid)
+		}
+		return 0, 0, nil, "", fmt.Errorf("Unable to find user %v: %v", userArg, err)
+	}
+
+	haveUser := users != nil && len(users) > 0
+	if haveUser {
+		// if we found any user entries that matched our filter, let's take the first one as "correct"
+		uid = users[0].Uid
+		gid = users[0].Gid
+		home = users[0].Home
+	} else if userArg != "" {
+		// we asked for a user but didn't find them...  let's check to see if we wanted a numeric user
+		uid, err = strconv.Atoi(userArg)
+		if err != nil {
+			// not numeric - we have to bail
+			return 0, 0, nil, "", fmt.Errorf("Unable to find user %v", userArg)
+		}
+		if uid < minId || uid > maxId {
+			return 0, 0, nil, "", ErrRange
+		}
+
+		// if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit
+	}
+
+	if groupArg != "" || (haveUser && users[0].Name != "") {
+		groups, err := ParseGroupFilter(func(g *Group) bool {
+			if groupArg != "" {
+				return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg
+			}
+			for _, u := range g.List {
+				if u == users[0].Name {
+					return true
+				}
+			}
+			return false
+		})
+		if err != nil && !os.IsNotExist(err) {
+			return 0, 0, nil, "", fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
+		}
+
+		haveGroup := groups != nil && len(groups) > 0
+		if groupArg != "" {
+			if haveGroup {
+				// if we found any group entries that matched our filter, let's take the first one as "correct"
+				gid = groups[0].Gid
+			} else {
+				// we asked for a group but didn't find id...  let's check to see if we wanted a numeric group
+				gid, err = strconv.Atoi(groupArg)
+				if err != nil {
+					// not numeric - we have to bail
+					return 0, 0, nil, "", fmt.Errorf("Unable to find group %v", groupArg)
+				}
+				if gid < minId || gid > maxId {
+					return 0, 0, nil, "", ErrRange
+				}
+
+				// if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit
+			}
+		} else if haveGroup {
+			suppGids = make([]int, len(groups))
+			for i, group := range groups {
+				suppGids[i] = group.Gid
+			}
+		}
+	}
+
+	return uid, gid, suppGids, home, nil
+}

+ 94 - 0
vendor/src/github.com/docker/libcontainer/user/user_test.go

@@ -0,0 +1,94 @@
+package user
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestUserParseLine(t *testing.T) {
+	var (
+		a, b string
+		c    []string
+		d    int
+	)
+
+	parseLine("", &a, &b)
+	if a != "" || b != "" {
+		t.Fatalf("a and b should be empty ('%v', '%v')", a, b)
+	}
+
+	parseLine("a", &a, &b)
+	if a != "a" || b != "" {
+		t.Fatalf("a should be 'a' and b should be empty ('%v', '%v')", a, b)
+	}
+
+	parseLine("bad boys:corny cows", &a, &b)
+	if a != "bad boys" || b != "corny cows" {
+		t.Fatalf("a should be 'bad boys' and b should be 'corny cows' ('%v', '%v')", a, b)
+	}
+
+	parseLine("", &c)
+	if len(c) != 0 {
+		t.Fatalf("c should be empty (%#v)", c)
+	}
+
+	parseLine("d,e,f:g:h:i,j,k", &c, &a, &b, &c)
+	if a != "g" || b != "h" || len(c) != 3 || c[0] != "i" || c[1] != "j" || c[2] != "k" {
+		t.Fatalf("a should be 'g', b should be 'h', and c should be ['i','j','k'] ('%v', '%v', '%#v')", a, b, c)
+	}
+
+	parseLine("::::::::::", &a, &b, &c)
+	if a != "" || b != "" || len(c) != 0 {
+		t.Fatalf("a, b, and c should all be empty ('%v', '%v', '%#v')", a, b, c)
+	}
+
+	parseLine("not a number", &d)
+	if d != 0 {
+		t.Fatalf("d should be 0 (%v)", d)
+	}
+
+	parseLine("b:12:c", &a, &d, &b)
+	if a != "b" || b != "c" || d != 12 {
+		t.Fatalf("a should be 'b' and b should be 'c', and d should be 12 ('%v', '%v', %v)", a, b, d)
+	}
+}
+
+func TestUserParsePasswd(t *testing.T) {
+	users, err := parsePasswdFile(strings.NewReader(`
+root:x:0:0:root:/root:/bin/bash
+adm:x:3:4:adm:/var/adm:/bin/false
+this is just some garbage data
+`), nil)
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if len(users) != 3 {
+		t.Fatalf("Expected 3 users, got %v", len(users))
+	}
+	if users[0].Uid != 0 || users[0].Name != "root" {
+		t.Fatalf("Expected users[0] to be 0 - root, got %v - %v", users[0].Uid, users[0].Name)
+	}
+	if users[1].Uid != 3 || users[1].Name != "adm" {
+		t.Fatalf("Expected users[1] to be 3 - adm, got %v - %v", users[1].Uid, users[1].Name)
+	}
+}
+
+func TestUserParseGroup(t *testing.T) {
+	groups, err := parseGroupFile(strings.NewReader(`
+root:x:0:root
+adm:x:4:root,adm,daemon
+this is just some garbage data
+`), nil)
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if len(groups) != 3 {
+		t.Fatalf("Expected 3 groups, got %v", len(groups))
+	}
+	if groups[0].Gid != 0 || groups[0].Name != "root" || len(groups[0].List) != 1 {
+		t.Fatalf("Expected groups[0] to be 0 - root - 1 member, got %v - %v - %v", groups[0].Gid, groups[0].Name, len(groups[0].List))
+	}
+	if groups[1].Gid != 4 || groups[1].Name != "adm" || len(groups[1].List) != 3 {
+		t.Fatalf("Expected groups[1] to be 4 - adm - 3 members, got %v - %v - %v", groups[1].Gid, groups[1].Name, len(groups[1].List))
+	}
+}