diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85b8303..beef0b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,4 +19,4 @@ jobs: go-version: 'stable' - name: Run Go tests - run: go test -v ./cmd/... + run: go test -v ./... diff --git a/README.md b/README.md index 74076d9..8805b9a 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,3 @@ func main() { } } ``` - -### Build Flags - -The nodevfiles build tag removes support for device files. diff --git a/cmd/build.go b/cmd/build.go index 952041f..3249b7e 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -2,14 +2,8 @@ package cmd import ( "fmt" - "io" - "os" - "path/filepath" - "slices" - "strings" "github.com/linux-immutability-tools/EtcBuilder/core" - "github.com/linux-immutability-tools/EtcBuilder/settings" "github.com/spf13/cobra" ) @@ -55,136 +49,11 @@ func buildCommand(_ *cobra.Command, args []string) error { } func ExtBuildCommand(oldSys, newSys, oldUser, newUser string) error { - err := settings.GatherConfigFiles() - if err != nil { - return err - } - - err = clearDirectory(newUser) - if err != nil { - return err - } - - err = filepath.Walk(oldUser, func(userPath string, userInfo os.FileInfo, e error) error { - userPathRel := strings.TrimPrefix(userPath, oldUser) - userPathRel = strings.TrimPrefix(userPathRel, "/") - - if userInfo.IsDir() { - newUserDirAbs := filepath.Join(newUser, userPathRel) - err := os.MkdirAll(newUserDirAbs, 0o755) - if err != nil { - return err - } - err = os.Chmod(newUserDirAbs, userInfo.Mode().Perm()) - return err - } - - err = copyUserFile(userPathRel, oldSys, newSys, oldUser, newUser, userInfo) - if err != nil { - return err - } - - return nil - }) + err := core.BuildNewEtc(oldSys, oldUser, newSys, newUser) if err != nil { return err } return nil } - -func copyUserFile(relUserFile, oldSysDir, newSysDir, oldUserDir, newUserDir string, oldFileInfo os.FileInfo) error { - userFileType := oldFileInfo.Mode().Type().String() - - absUserFileOld := filepath.Join(oldUserDir, relUserFile) - absUserFileNew := filepath.Join(newUserDir, relUserFile) - - os.MkdirAll(filepath.Dir(absUserFileNew), 0o755) - - for _, externalFileHandler := range ExternalFileHandlers { - if !externalFileHandler.IsFileSupported(absUserFileOld) { - continue - } - return externalFileHandler.Handle(relUserFile, oldSysDir, newSysDir, oldUserDir, newUserDir) - } - - if slices.Contains(settings.SpecialFiles, relUserFile) { - fmt.Println("Special merging file", relUserFile) - absSysFileOld := filepath.Join(oldSysDir, relUserFile) - absSysFileNew := filepath.Join(newSysDir, relUserFile) - absUserFileNew := filepath.Join(newUserDir, relUserFile) - err := core.MergeSpecialFile(absUserFileOld, absSysFileOld, absSysFileNew, absUserFileNew) - if err != nil { - return err - } - return nil - } else if oldFileInfo.Mode().IsRegular() { - fmt.Println("Keeping user file", relUserFile) - err := copyRegular(absUserFileOld, absUserFileNew) - return err - } else if strings.HasPrefix(userFileType, "L") { - fmt.Println("Keeping user symlink", relUserFile) - err := copySymlink(absUserFileOld, absUserFileNew) - return err - } else { - return &UnknownFileError{absUserFileOld} - } -} - -func clearDirectory(path string) error { - entries, err := os.ReadDir(path) - if err != nil { - return err - } - for _, entry := range entries { - entryPath := filepath.Join(path, entry.Name()) - if entry.IsDir() { - err = clearDirectory(entryPath) - if err != nil { - return err - } - } - err := os.Remove(entryPath) - if err != nil { - return err - } - } - return nil -} - -func copyRegular(fromPath, toPath string) error { - source, err := os.Open(fromPath) - if err != nil { - return err - } - defer source.Close() - - destination, err := os.Create(toPath) - if err != nil { - return err - } - defer destination.Close() - - fromInfo, err := os.Stat(fromPath) - if err != nil { - return err - } - - err = destination.Chmod(fromInfo.Mode().Perm()) - if err != nil { - return err - } - - _, err = io.Copy(destination, source) - return err -} - -func copySymlink(fromPath, toPath string) error { - sym, err := os.Readlink(fromPath) - if err != nil { - return err - } - err = os.Symlink(sym, toPath) - return err -} diff --git a/cmd/build_test.go b/cmd/build_test.go deleted file mode 100644 index ccc8152..0000000 --- a/cmd/build_test.go +++ /dev/null @@ -1,291 +0,0 @@ -package cmd_test - -import ( - "io/fs" - "os" - "path/filepath" - "strings" - "syscall" - "testing" - - "github.com/linux-immutability-tools/EtcBuilder/cmd" - "github.com/linux-immutability-tools/EtcBuilder/settings" -) - -func prepareTestEnv(t *testing.T) (oldSys, newSys, oldUser, newUser string) { - tmpDir := t.TempDir() - oldSys = filepath.Join(tmpDir, "old Sys") - newSys = filepath.Join(tmpDir, "new Sys") - oldUser = filepath.Join(tmpDir, "old User") - newUser = filepath.Join(tmpDir, "new User") - err := os.Mkdir(oldSys, 0o777) - if err != nil { - t.Log(err) - t.FailNow() - } - err = os.Mkdir(newSys, 0o765) - if err != nil { - t.Log(err) - t.FailNow() - } - err = os.Mkdir(oldUser, 0o701) - if err != nil { - t.Log(err) - t.FailNow() - } - err = os.Mkdir(newUser, 0o700) - if err != nil { - t.Log(err) - t.FailNow() - } - return -} - -func TestEmpty(t *testing.T) { - oldSys, newSys, oldUser, newUser := prepareTestEnv(t) - - err := cmd.ExtBuildCommand(oldSys, newSys, oldUser, newUser) - if err != nil { - t.Error(err) - } -} - -func TestCleanDir(t *testing.T) { - var err error - oldSys, newSys, oldUser, newUser := prepareTestEnv(t) - - fileRel := "some/path to/file/my file.abc" - myFile := filepath.Join(newUser, fileRel) - err = os.MkdirAll(filepath.Dir(myFile), 0o777) - if err != nil { - t.Log(err) - t.FailNow() - } - - err = os.WriteFile(myFile, []byte("Some data"), 0o777) - if err != nil { - t.Log(err) - t.FailNow() - } - - err = cmd.ExtBuildCommand(oldSys, newSys, oldUser, newUser) - if err != nil { - t.Log(err) - t.FailNow() - } - - _, err = os.Stat(filepath.Dir(myFile)) - if !os.IsNotExist(err) { - t.Error("file was not removed successfully") - t.Fail() - } -} - -func TestRegularFile(t *testing.T) { - var err error - oldSys, newSys, oldUser, newUser := prepareTestEnv(t) - - fileRel := "some/path to/file/my file.abc" - myFile := filepath.Join(oldUser, fileRel) - dirPerms := 0o751 - err = os.MkdirAll(filepath.Dir(myFile), fs.FileMode(dirPerms)) - if err != nil { - t.Log(err) - t.FailNow() - } - - filePerm := 0o715 - err = os.WriteFile(myFile, []byte("Some data"), fs.FileMode(filePerm)) - if err != nil { - t.Log(err) - t.FailNow() - } - - err = cmd.ExtBuildCommand(oldSys, newSys, oldUser, newUser) - if err != nil { - t.Log(err) - t.FailNow() - } - - secondParentDir := filepath.Dir(filepath.Dir(filepath.Join(newUser, fileRel))) - secondParentDirInfo, err := os.Stat(secondParentDir) - if err != nil { - t.Log(err) - t.FailNow() - } - if int(secondParentDirInfo.Mode().Perm()) != dirPerms { - t.Logf("Permissions %o instead of %o", secondParentDirInfo.Mode().Perm(), dirPerms) - t.Log("Parent of parent dir doesn't have the right permissions") - t.Fail() - } - - newUserFile := filepath.Join(newUser, fileRel) - contents, err := os.ReadFile(newUserFile) - if err != nil { - t.Log(err) - t.FailNow() - } - if info, err := os.Lstat(newUserFile); err != nil || int(info.Mode().Perm()) != filePerm { - t.Logf("Permissions %o instead of %o", info.Mode().Perm(), filePerm) - t.Log("Permissions don't match") - t.Fail() - } - if string(contents) != "Some data" { - t.Log("Files did not match") - t.FailNow() - } -} - -func TestSymlink(t *testing.T) { - var err error - oldSys, newSys, oldUser, newUser := prepareTestEnv(t) - - fileRel := "some/path to/link/my link" - sym := "../some/ra ndom/path" - myFile := filepath.Join(oldUser, fileRel) - err = os.MkdirAll(filepath.Dir(myFile), 0o753) - if err != nil { - t.Log(err) - t.FailNow() - } - - err = os.Symlink(sym, myFile) - if err != nil { - t.Error(err) - t.FailNow() - } - - err = cmd.ExtBuildCommand(oldSys, newSys, oldUser, newUser) - if err != nil { - t.Log(err) - t.FailNow() - } - - contents, err := os.Readlink(filepath.Join(newUser, fileRel)) - if err != nil { - t.Log(err) - t.FailNow() - } - if string(contents) != sym { - t.Log("Files did not match") - t.FailNow() - } -} - -func TestSpecialFile(t *testing.T) { - var err error - oldSys, newSys, oldUser, newUser := prepareTestEnv(t) - - fileRel := "test/äu/path/special file" - - oldUserFile := filepath.Join(oldUser, fileRel) - err = os.MkdirAll(filepath.Dir(oldUserFile), 0o777) - if err != nil { - t.Log(err) - t.FailNow() - } - oldSysFile := filepath.Join(oldSys, fileRel) - err = os.MkdirAll(filepath.Dir(oldSysFile), 0o777) - if err != nil { - t.Log(err) - t.FailNow() - } - newSysFile := filepath.Join(newSys, fileRel) - err = os.MkdirAll(filepath.Dir(newSysFile), 0o777) - if err != nil { - t.Log(err) - t.FailNow() - } - - settings.SpecialFiles = append(settings.SpecialFiles, fileRel) - - part1 := `dnsmasq:x:993:991:Dnsmasq DHCP and DNS server:/var/lib/dnsmasq:/usr/sbin/nologin` - part2 := `colord:x:992:990:User for colord:/var/lib/colord:/sbin/nologin` - part3 := `abrt:x:173:173::/etc/abrt:/sbin/nologin` - part4 := `bin:x:1:1:bin:/bin:/sbin/nologin` - part5 := `flatpak:x:986:984:Flatpak system helper:/:/usr/sbin/nologin` - part6 := `rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin` - part7 := `mail:x:8:12:mail:/var/spool/mail:/sbin/nologin` - - oldUserContents := part1 + "\n" + part2 + "\n" + part3 + "\n" + part4 + "\n" + part5 + "\n" - - oldSysContents := part1 + "\n" + part6 + "\n" + part2 + "\n" + part3 + "\n" + part5 - - newSysContents := part1 + "\n" + part7 + "\n" + part6 + "\n" + part3 + "\n" + part5 + "\n" - - mergedContent := part1 + "\n" + part7 + "\n" + part3 + "\n" + part5 + "\n" + part4 - - err = os.WriteFile(oldUserFile, []byte(oldUserContents), 0o777) - if err != nil { - t.Log(err) - t.FailNow() - } - - err = os.WriteFile(oldSysFile, []byte(oldSysContents), 0o777) - if err != nil { - t.Log(err) - t.FailNow() - } - - err = os.WriteFile(newSysFile, []byte(newSysContents), 0o777) - if err != nil { - t.Log(err) - t.FailNow() - } - - err = cmd.ExtBuildCommand(oldSys, newSys, oldUser, newUser) - if err != nil { - t.Log(err) - t.FailNow() - } - - contents, err := os.ReadFile(filepath.Join(newUser, fileRel)) - if err != nil { - t.Log(err) - t.FailNow() - } - if strings.TrimSpace(string(contents)) != strings.TrimSpace(mergedContent) { - t.Log("Files did not get merged correctly") - t.FailNow() - } -} - -func TestCharSpecial(t *testing.T) { - var err error - oldSys, newSys, oldUser, newUser := prepareTestEnv(t) - - fileRel := "some/path to/link/my char special" - myFile := filepath.Join(oldUser, fileRel) - err = os.MkdirAll(filepath.Dir(myFile), 0o753) - if err != nil { - t.Log(err) - t.FailNow() - } - - err = syscall.Mknod(myFile, 0x2000, 0) - if err != nil { - t.Error(err) - t.FailNow() - } - - err = cmd.ExtBuildCommand(oldSys, newSys, oldUser, newUser) - if err != nil { - t.Log(err) - t.FailNow() - } - - var info syscall.Stat_t - err = syscall.Lstat(filepath.Join(newUser, fileRel), &info) - if err != nil { - t.Log(err) - t.FailNow() - } - if info.Mode != 0x2000 { - t.Log("Character special was not created correctly") - t.Fail() - } - if info.Rdev != 0 { - t.Log("Device was not set correctly") - t.Fail() - } -} diff --git a/cmd/deviceFileHandler.go b/cmd/deviceFileHandler.go deleted file mode 100644 index bed0402..0000000 --- a/cmd/deviceFileHandler.go +++ /dev/null @@ -1,41 +0,0 @@ -//go:build !nodevfiles - -package cmd - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "syscall" -) - -func init() { - ExternalFileHandlers = append(ExternalFileHandlers, FileHandler{checkIfDeviceFile, copyDeviceFile}) -} - -func checkIfDeviceFile(filePath string) bool { - info, err := os.Lstat(filePath) - if err != nil { - return false - } - if strings.HasPrefix(info.Mode().Type().String(), "D") { - return true - } - return false -} - -func copyDeviceFile(relFromPath, oldSysDir, newSysDir, oldUserDir, newUserDir string) error { - fmt.Println("Keeping user device file", relFromPath) - - fromPath := filepath.Join(oldUserDir, relFromPath) - toPath := filepath.Join(newUserDir, relFromPath) - - var stat syscall.Stat_t - err := syscall.Lstat(fromPath, &stat) - if err != nil { - return err - } - err = syscall.Mknod(toPath, stat.Mode, int(stat.Rdev)) - return err -} diff --git a/core/MergeNormFile.go b/core/MergeNormFile.go deleted file mode 100644 index bf8fb4b..0000000 --- a/core/MergeNormFile.go +++ /dev/null @@ -1,45 +0,0 @@ -package core - -import ( - "crypto/sha1" - "fmt" - "io" - "os" - "strings" -) - -func KeepUserFile(user string, new string) (bool, error) { - // Decide wether to keep the user file or use the new file - // Returns true if the user file should be kept - // False if the new file should be used - userFileHash, err := calculateHash(user) - if err != nil { - fmt.Printf("err: %v\n", err) - return true, fmt.Errorf("failed to calculate hash of user file") - } - - newFilehash, err := calculateHash(new) - if err != nil { - fmt.Printf("err: %v\n", err) - return true, fmt.Errorf("failed to calculate hash of new file") - } - - if strings.Compare(strings.TrimSpace(userFileHash), strings.TrimSpace(newFilehash)) != 0 { - return true, nil - } - - return false, nil -} - -func calculateHash(file string) (string, error) { - hash := sha1.New() - osFile, err := os.Open(file) - if err != nil { - return "", err - } - if _, err := io.Copy(hash, osFile); err != nil { - return "", err - } - hashInBytes := hash.Sum(nil)[:20] - return strings.TrimSpace(fmt.Sprintf("%x", hashInBytes)), nil -} diff --git a/core/MergeSpecialFile.go b/core/MergeSpecialFile.go deleted file mode 100644 index ba6c18f..0000000 --- a/core/MergeSpecialFile.go +++ /dev/null @@ -1,88 +0,0 @@ -package core - -import ( - "os" - "slices" - "strings" -) - -func removeEmptyLines(lines []string) []string { - newLines := []string{} - for _, line := range lines { - if strings.TrimSpace(line) != "" { - newLines = append(newLines, line) - } - } - return newLines -} - -func createLineDiff(baseData, modifiedData string) (added, removed []string) { - oldLines := strings.Split(baseData, "\n") - newLines := strings.Split(modifiedData, "\n") - oldLines = removeEmptyLines(oldLines) - newLines = removeEmptyLines(newLines) - - for _, line := range oldLines { - if !slices.Contains(newLines, line) { - removed = append(removed, line) - } - } - for _, line := range newLines { - if !slices.Contains(oldLines, line) { - added = append(added, line) - } - } - - return -} - -func applyLineDiff(added, removed []string, data string) string { - dataLines := strings.Split(data, "\n") - dataLines = removeEmptyLines(dataLines) - - for _, line := range removed { - pos := slices.Index(dataLines, line) - if pos >= 0 { - dataLines = slices.Delete(dataLines, pos, pos+1) - } - } - for _, line := range added { - if !slices.Contains(dataLines, line) { - dataLines = append(dataLines, line) - } - } - - return strings.Join(dataLines, "\n") + "\n" -} - -func MergeSpecialFile(user string, old string, new string, out string) error { - // Merges special files - // Files get merged by first forming a diff between the old file and the user file - // Then applying the generated patch to the new file - // The new file then gets written to the given destination - userData, err := os.ReadFile(user) - if err != nil { - return err - } - oldData, err := os.ReadFile(old) - if err != nil { - return err - } - newData, err := os.ReadFile(new) - if err != nil { - return err - } - - added, removed := createLineDiff(string(oldData), string(userData)) - - result := applyLineDiff(added, removed, string(newData)) - filePerms, err := os.Stat(new) - if err != nil { - return err - } - err = os.WriteFile(out, []byte(result), filePerms.Mode()) - if err != nil { - return err - } - return nil -} diff --git a/core/cleanup.go b/core/cleanup.go new file mode 100644 index 0000000..815eb4a --- /dev/null +++ b/core/cleanup.go @@ -0,0 +1,71 @@ +package core + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" +) + +type Comparable interface { + SupportsFile(info os.FileInfo) bool + IsIdentical(a, b os.FileInfo, aPath, bPath string) (bool, error) +} + +// no folders since checking if they are empty complicates things +var comparables = [...]Comparable{&RegularFile{}, &Symlink{}, &CharDeviceFile{}} + +// RemoveIdenticalFiles removes files from target if an identical +// version exists in the same location in base. +func RemoveIdenticalFiles(target string, base string) { + filesToRemove := []string{} + + err := fs.WalkDir(os.DirFS(target), ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: can't search path \"%s\" for cleanup: %s", path, err) + } + + baseFile := filepath.Join(base, path) + targetFile := filepath.Join(target, path) + + targetInfo, err := os.Lstat(targetFile) + if err != nil { + fmt.Fprintln(os.Stderr, "Warning:", err) + return nil + } + + baseInfo, err := os.Lstat(baseFile) + if err != nil { + // no base file, so keep target + return nil + } + + for _, comparable := range comparables { + if comparable.SupportsFile(targetInfo) { + isIdentical, err := comparable.IsIdentical(targetInfo, baseInfo, targetFile, baseFile) + if err != nil { + fmt.Fprintln(os.Stderr, "Warning:", err) + return nil + } + if isIdentical { + filesToRemove = append(filesToRemove, targetFile) + } + return nil + } + } + + return nil + }) + + if err != nil { + fmt.Fprintln(os.Stderr, "Warning:", err) + return + } + + for _, toRemove := range filesToRemove { + err := os.Remove(toRemove) + if err != nil { + fmt.Fprintln(os.Stderr, "Warning: can not remove unnecessary file"+toRemove+":", err) + } + } +} diff --git a/core/copy.go b/core/copy.go new file mode 100644 index 0000000..82a4f6e --- /dev/null +++ b/core/copy.go @@ -0,0 +1,69 @@ +package core + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" +) + +func CarbonCopyRecursive(from, to string) error { + + err := fs.WalkDir(os.DirFS(from), ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't search path \"%s\": %w", path, err) + } + + from := filepath.Join(from, path) + to := filepath.Join(to, path) + + err = CarbonCopy(from, to) + if err != nil { + return fmt.Errorf("can't copy \"%s\": %w", path, err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't copy all files: %w", err) + } + + return nil +} + +type Copyable interface { + SupportsFile(info os.FileInfo) bool + Copy(fromInfo os.FileInfo, from, to string) error + CopyAttributes(fromInfo os.FileInfo, to string) error +} + +var copyables = [...]Copyable{&Folder{}, &RegularFile{}, &Symlink{}, &CharDeviceFile{}} + +var ErrUnsupportedFiletype = errors.New("unsupported file type") + +func CarbonCopy(from, to string) error { + fromInfo, err := os.Lstat(from) + if err != nil { + return fmt.Errorf("can't find information about file: %w", err) + } + + for _, copyable := range copyables { + if copyable.SupportsFile(fromInfo) { + err = copyable.Copy(fromInfo, from, to) + if err != nil { + return fmt.Errorf("can't copy node: %w", err) + } + + err = copyable.CopyAttributes(fromInfo, to) + if err != nil { + return fmt.Errorf("can't copy attributes: %w", err) + } + + return nil + } + } + + return ErrUnsupportedFiletype +} diff --git a/core/etcmerge.go b/core/etcmerge.go new file mode 100644 index 0000000..78d3650 --- /dev/null +++ b/core/etcmerge.go @@ -0,0 +1,190 @@ +package core + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strings" +) + +type ErrMergeFiles struct { + msg string + errs []error +} + +func (e *ErrMergeFiles) Error() string { + return e.msg + ": " + fmt.Sprint(e.errs) +} + +func (e *ErrMergeFiles) Unwrap() []error { + return e.errs +} + +// BuildNewEtc fixes the owner of the new lower etc folder and create the new upper etc folder +func BuildNewEtc(lowerOld, upperOld, lowerNew, upperNew string) error { + + os.RemoveAll(upperNew) + os.MkdirAll(lowerOld, 0x755) + os.MkdirAll(upperOld, 0x755) + os.MkdirAll(lowerNew, 0x755) + + err := CarbonCopyRecursive(upperOld, upperNew) + if err != nil { + return fmt.Errorf("can't create new upper etc: %w", err) + } + + groupFile, groupMapping, err := handleGroupFiles(upperOld, lowerNew, upperNew) + if err != nil { + return err + } + + _, err = MergeInGshadow(upperNew, lowerNew) + if err != nil { + return fmt.Errorf("can't merge lower gshadow file into upper: %w", err) + } + + _, userMapping, err := handlePasswdFiles(upperOld, lowerNew, upperNew, groupFile, groupMapping) + if err != nil { + return err + } + + _, err = MergeInShadow(upperNew, lowerNew) + if err != nil { + return fmt.Errorf("can't merge lower shadow file into upper: %w", err) + } + + _, err = MergeInShells(upperNew, lowerNew) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("can't merge lower shells file into upper: %w", err) + } + + err = ApplyOwnerMappingRecursive(lowerNew, userMapping, groupMapping) + if err != nil { + return fmt.Errorf("can't apply owner mapping: %w", err) + } + + RemoveIdenticalFiles(upperNew, lowerNew) + + return nil +} + +func handleGroupFiles(upperOld, lowerNew, upperNew string) (*GroupFile, map[int]int, error) { + groupFile, err := NewGroupFile(filepath.Join(upperOld, "group")) + if err != nil { + return nil, nil, fmt.Errorf("can't open current group file: %w", err) + } + + newLowerGroupFile, err := NewGroupFile(filepath.Join(lowerNew, "group")) + if err != nil { + return nil, nil, fmt.Errorf("can't open new lower group file: %w", err) + } + + errs := groupFile.MergeWithOther(*newLowerGroupFile) + if len(errs) != 0 { + return nil, nil, &ErrMergeFiles{msg: "can't merge groups", errs: errs} + } + + err = groupFile.WriteToFile(filepath.Join(upperNew, "group")) + if err != nil { + return nil, nil, fmt.Errorf("can't write merged group file: %w", err) + } + + groupMapping, err := CreateGroupMapping(*newLowerGroupFile, *groupFile) + if err != nil { + return nil, nil, fmt.Errorf("can't create group mapping: %w", err) + } + + return groupFile, groupMapping, nil +} + +func handlePasswdFiles(upperOld, lowerNew, upperNew string, groupFile *GroupFile, groupMapping map[int]int) (*PasswdFile, map[int]int, error) { + passwdFile, err := NewPasswdFile(filepath.Join(upperOld, "passwd")) + if err != nil { + return nil, nil, fmt.Errorf("can't open current passwd file: %w", err) + } + + newLowerPasswdFile, err := NewPasswdFile(filepath.Join(lowerNew, "passwd")) + if err != nil { + return nil, nil, fmt.Errorf("can't open new lower passwd file: %w", err) + } + + var nogroupGid int + if nogroup, ok := groupFile.Contents["nogroup"]; ok { + nogroupGid = nogroup.Gid + } else { + nogroupGid = 65534 + } + + errs := passwdFile.MergeWithOther(*newLowerPasswdFile, groupMapping, nogroupGid) + if len(errs) != 0 { + return nil, nil, &ErrMergeFiles{msg: "can't merge users", errs: errs} + } + + err = passwdFile.WriteToFile(filepath.Join(upperNew, "passwd")) + if err != nil { + return nil, nil, fmt.Errorf("can't write merged passwd file: %w", err) + } + + userMapping, err := CreateUserMapping(*newLowerPasswdFile, *passwdFile) + if err != nil { + return nil, nil, fmt.Errorf("can't create user mapping: %w", err) + } + + return passwdFile, userMapping, nil +} + +// MergeInShells merges extra entries from the shells file in extraShellsDir into the shells file in shellsDir +func MergeInShells(shellsDir, extraShellsDir string) (int, error) { + shellsFilePath := filepath.Join(shellsDir, "shells") + extraShellsFilePath := filepath.Join(extraShellsDir, "shells") + + shellsFileContents, err := os.ReadFile(shellsFilePath) + if err != nil { + return 0, fmt.Errorf("can't open shells file: %w", err) + } + extraShellsFileContents, err := os.ReadFile(extraShellsFilePath) + if err != nil { + return 0, fmt.Errorf("can't open extra shells file: %w", err) + } + + shellsList := []string{} + + for line := range strings.SplitSeq(string(shellsFileContents), "\n") { + line := strings.TrimSpace(line) + if line == "" { + continue + } + shellsList = append(shellsList, line) + } + + for index, line := range shellsList { + line = strings.TrimSpace(line) + shellsList[index] = line + } + + addedCount := 0 + + for extraLine := range strings.SplitSeq(string(extraShellsFileContents), "\n") { + extraLine := strings.TrimSpace(extraLine) + if extraLine == "" { + continue + } + if strings.HasPrefix(extraLine, "#") { + continue + } + if slices.Contains(shellsList, extraLine) { + continue + } + + shellsList = append(shellsList, extraLine) + addedCount++ + } + + mergedFileContents := strings.Join(shellsList, "\n") + "\n" + + os.WriteFile(shellsFilePath, []byte(mergedFileContents), 0o644) + + return addedCount, nil +} diff --git a/core/etcmerge_test.go b/core/etcmerge_test.go new file mode 100644 index 0000000..460f928 --- /dev/null +++ b/core/etcmerge_test.go @@ -0,0 +1,680 @@ +package core + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "slices" + "strings" + "syscall" + "testing" +) + +const passwdLowerOld = ` +root:x:0:0:root:/root:/bin/bash +irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin +_apt:x:42:65534::/nonexistent:/usr/sbin/nologin +nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin +` + +const passwdUpperOld = ` +root:x:0:0:root:/root:/bin/bash +irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin +_apt:x:42:65534::/nonexistent:/usr/sbin/nologin +nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin +test::1000:1000:Tau:/home/test:/usr/bin/bash +` + +const passwdLowerNew = ` +root:x:0:0:root:/root:/bin/bash +irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin +uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin +_apt:x:42:65534::/nonexistent:/usr/sbin/nologin +nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin +` + +const passwdExpect = ` +root:x:0:0:root:/root:/bin/bash +uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin +irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin +_apt:x:42:65534::/nonexistent:/usr/sbin/nologin +test::1000:1000:Tau:/home/test:/usr/bin/bash +nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin +` + +const groupLowerOld = ` +root:x:0: +irc:x:39: +nogroup:x:65534: +` + +const groupUpperOld = ` +root:x:0: +irc:x:39: +nogroup:x:65534: +test:x:1000: +` + +const groupLowerNew = ` +root:x:0: +irc:x:39: +uucp:x:10: +nogroup:x:65534: +` + +const groupExpect = ` +root:x:0: +uucp:x:10: +irc:x:39: +test:x:1000: +nogroup:x:65534: +` + +const gshadowLowerOld = ` +root:*:: +irc:*:: +nogroup:*:: +` + +const gshadowUpperOld = ` +root:*:: +irc:*:: +nogroup:*:: +test:!:: +` + +const gshadowLowerNew = ` +root:*:: +uucp:*:: +irc:*:: +nogroup:*:: +` + +const gshadowExpect = ` +uucp:*:: +root:*:: +irc:*:: +nogroup:*:: +test:!:: +` + +const shadowLowerOld = ` +root::20248:0:99999:7::: +irc:*:20228:0:99999:7::: +nobody:*:20228:0:99999:7::: +` +const shadowUpperOld = ` +root::20248:0:99999:7::: +irc:*:20228:0:99999:7::: +nobody:*:20228:0:99999:7::: +test:$j$jjT$huf789w.$iojfw3897:20191:0:99999:7::: +` + +const shadowLowerNew = ` +root::20248:0:99999:7::: +uucp:*:20228:0:99999:7::: +irc:*:20228:0:99999:7::: +nobody:*:20228:0:99999:7::: +` + +const shadowExpect = ` +nobody:*:20228:0:99999:7::: +test:$j$jjT$huf789w.$iojfw3897:20191:0:99999:7::: +uucp:*:20228:0:99999:7::: +root::20248:0:99999:7::: +irc:*:20228:0:99999:7::: +` + +const shellsLowerOld = ` +# /etc/shells: valid login shells +/bin/sh +/usr/bin/sh +/bin/bash +` +const shellsUpperOld = ` +# /etc/shells: valid login shells +/bin/sh +/usr/bin/sh +/bin/bash +/usr/bin/fish +` + +const shellsLowerNew = ` +# /etc/shells: valid login shells +/bin/sh +/usr/bin/sh +/bin/bash +/usr/bin/vso-os-shell +` + +const shellsExpect = ` +# /etc/shells: valid login shells +/bin/sh +/usr/bin/sh +/bin/bash +/usr/bin/fish +/usr/bin/vso-os-shell +` + +func setupEnvironment(t *testing.T) (string, string, string, string) { + testEtcPath := t.TempDir() + + lowerOld := filepath.Join(testEtcPath, "lowerOld") + upperOld := filepath.Join(testEtcPath, "upperOld") + lowerNew := filepath.Join(testEtcPath, "lowerNew") + upperNew := filepath.Join(testEtcPath, "upperNew") + + os.RemoveAll(testEtcPath) + + err := os.MkdirAll(lowerOld, 0o755) + if err != nil { + t.Error(err) + } + err = os.MkdirAll(upperOld, 0o755) + if err != nil { + t.Error(err) + } + err = os.MkdirAll(lowerNew, 0o755) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(lowerOld, "passwd"), []byte(passwdLowerOld), 0o644) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(upperOld, "passwd"), []byte(passwdUpperOld), 0o644) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(lowerNew, "passwd"), []byte(passwdLowerNew), 0o644) + if err != nil { + t.Error(err) + } + + err = os.WriteFile(filepath.Join(lowerOld, "group"), []byte(groupLowerOld), 0o644) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(upperOld, "group"), []byte(groupUpperOld), 0o644) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(lowerNew, "group"), []byte(groupLowerNew), 0o644) + if err != nil { + t.Error(err) + } + + err = os.WriteFile(filepath.Join(lowerOld, "gshadow"), []byte(gshadowLowerOld), 0o640) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(upperOld, "gshadow"), []byte(gshadowUpperOld), 0o640) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(lowerNew, "gshadow"), []byte(gshadowLowerNew), 0o640) + if err != nil { + t.Error(err) + } + + err = os.WriteFile(filepath.Join(lowerOld, "shadow"), []byte(shadowLowerOld), 0o640) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(upperOld, "shadow"), []byte(shadowUpperOld), 0o640) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(lowerNew, "shadow"), []byte(shadowLowerNew), 0o640) + if err != nil { + t.Error(err) + } + + err = os.WriteFile(filepath.Join(lowerOld, "shells"), []byte(shellsLowerOld), 0o640) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(upperOld, "shells"), []byte(shellsUpperOld), 0o640) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(lowerNew, "shells"), []byte(shellsLowerNew), 0o640) + if err != nil { + t.Error(err) + } + + return lowerOld, lowerNew, upperOld, upperNew +} + +func TestEmpty(t *testing.T) { + oldSys, newSys, oldUser, newUser := setupEnvironment(t) + + err := BuildNewEtc(oldSys, oldUser, newSys, newUser) + if err != nil { + t.Error(err) + } +} + +func TestCleanDir(t *testing.T) { + var err error + oldSys, newSys, oldUser, newUser := setupEnvironment(t) + + fileRel := "some/path to/file/my file.abc" + myFile := filepath.Join(newUser, fileRel) + err = os.MkdirAll(filepath.Dir(myFile), 0o777) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = os.WriteFile(myFile, []byte("Some data"), 0o777) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = BuildNewEtc(oldSys, oldUser, newSys, newUser) + if err != nil { + t.Log(err) + t.FailNow() + } + + _, err = os.Stat(filepath.Dir(myFile)) + if !os.IsNotExist(err) { + t.Error("file was not removed successfully") + t.Fail() + } +} + +func TestRegularFile(t *testing.T) { + var err error + oldSys, newSys, oldUser, newUser := setupEnvironment(t) + + fileRel := "some/path to/file/my file.abc" + myFile := filepath.Join(oldUser, fileRel) + dirPerms := 0o751 + err = os.MkdirAll(filepath.Dir(myFile), fs.FileMode(dirPerms)) + if err != nil { + t.Log(err) + t.FailNow() + } + + filePerm := 0o715 + err = os.WriteFile(myFile, []byte("Some data"), fs.FileMode(filePerm)) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = BuildNewEtc(oldSys, oldUser, newSys, newUser) + if err != nil { + t.Log(err) + t.FailNow() + } + + secondParentDir := filepath.Dir(filepath.Dir(filepath.Join(newUser, fileRel))) + secondParentDirInfo, err := os.Stat(secondParentDir) + if err != nil { + t.Log(err) + t.FailNow() + } + if int(secondParentDirInfo.Mode().Perm()) != dirPerms { + t.Logf("Permissions %o instead of %o", secondParentDirInfo.Mode().Perm(), dirPerms) + t.Log("Parent of parent dir doesn't have the right permissions") + t.Fail() + } + + newUserFile := filepath.Join(newUser, fileRel) + contents, err := os.ReadFile(newUserFile) + if err != nil { + t.Log(err) + t.FailNow() + } + if info, err := os.Lstat(newUserFile); err != nil || int(info.Mode().Perm()) != filePerm { + t.Logf("Permissions %o instead of %o", info.Mode().Perm(), filePerm) + t.Log("Permissions don't match") + t.Fail() + } + if string(contents) != "Some data" { + t.Log("Files did not match") + t.FailNow() + } +} + +func TestSymlink(t *testing.T) { + var err error + oldSys, newSys, oldUser, newUser := setupEnvironment(t) + + fileRel := "some/path to/link/my link" + sym := "../some/ra ndom/path" + myFile := filepath.Join(oldUser, fileRel) + err = os.MkdirAll(filepath.Dir(myFile), 0o753) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = os.Symlink(sym, myFile) + if err != nil { + t.Error(err) + t.FailNow() + } + + err = BuildNewEtc(oldSys, oldUser, newSys, newUser) + if err != nil { + t.Log(err) + t.FailNow() + } + + contents, err := os.Readlink(filepath.Join(newUser, fileRel)) + if err != nil { + t.Log(err) + t.FailNow() + } + if string(contents) != sym { + t.Log("Files did not match") + t.FailNow() + } +} + +func TestSpecialFiles(t *testing.T) { + var err error + oldSys, newSys, oldUser, newUser := setupEnvironment(t) + + err = BuildNewEtc(oldSys, oldUser, newSys, newUser) + if err != nil { + t.Log(err) + t.FailNow() + } + + allSpecials := map[string]string{"passwd": passwdExpect, "shadow": shadowExpect, "group": groupExpect, "gshadow": gshadowExpect, "shells": shellsExpect} + + for special, expect := range allSpecials { + contents, err := os.ReadFile(filepath.Join(newUser, special)) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = compareSpecialContents(string(contents), expect) + if err != nil { + t.Fatal(special, "did not get merged correctly:", err) + } + } + +} + +func compareSpecialContents(a, b string) error { + + aParts := strings.Split(strings.TrimSpace(a), "\n") + bParts := strings.Split(strings.TrimSpace(b), "\n") + + if len(aParts) != len(bParts) { + return errors.New("number of entries doesn't match") + } + + for _, aPart := range aParts { + if !slices.Contains(bParts, aPart) { + return errors.New("b is missing line" + aPart) + } + } + + for _, bPart := range bParts { + if !slices.Contains(aParts, bPart) { + return errors.New("a is missing line" + bPart) + } + } + + return nil +} + +func TestCharSpecial(t *testing.T) { + var err error + oldSys, newSys, oldUser, newUser := setupEnvironment(t) + + fileRel := "some/path to/link/my char special" + myFile := filepath.Join(oldUser, fileRel) + err = os.MkdirAll(filepath.Dir(myFile), 0o753) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = syscall.Mknod(myFile, 0x2000, 0) + if err != nil { + t.Error(err) + t.FailNow() + } + + err = BuildNewEtc(oldSys, oldUser, newSys, newUser) + if err != nil { + t.Log(err) + t.FailNow() + } + + var info syscall.Stat_t + err = syscall.Lstat(filepath.Join(newUser, fileRel), &info) + if err != nil { + t.Log(err) + t.FailNow() + } + if info.Mode != 0x2000 { + t.Log("Character special was not created correctly") + t.Fail() + } + if info.Rdev != 0 { + t.Log("Device was not set correctly") + t.Fail() + } +} + +func TestCleanup(t *testing.T) { + var err error + oldSys, newSys, oldUser, newUser := setupEnvironment(t) + + fileRel := "some/path to/file/my file.abc" + myFile := filepath.Join(oldUser, fileRel) + dirPerms := 0o751 + err = os.MkdirAll(filepath.Dir(myFile), fs.FileMode(dirPerms)) + if err != nil { + t.Log(err) + t.FailNow() + } + + filePerm := 0o715 + err = os.WriteFile(myFile, []byte("Some data"), fs.FileMode(filePerm)) + if err != nil { + t.Log(err) + t.FailNow() + } + + myFile2 := filepath.Join(newSys, fileRel) + err = os.MkdirAll(filepath.Dir(myFile2), fs.FileMode(dirPerms)) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = os.WriteFile(myFile2, []byte("Some data"), fs.FileMode(filePerm)) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = BuildNewEtc(oldSys, oldUser, newSys, newUser) + if err != nil { + t.Log(err) + t.FailNow() + } + + newUserFile := filepath.Join(newUser, fileRel) + + _, err = os.Lstat(newUserFile) + + if err == nil { + t.Fatal("identical file was not cleaned up") + } +} + +func TestCleanup2(t *testing.T) { + var err error + oldSys, newSys, oldUser, newUser := setupEnvironment(t) + + fileRel := "some/path to/file/my file.abc" + myFile := filepath.Join(oldUser, fileRel) + dirPerms := 0o751 + err = os.MkdirAll(filepath.Dir(myFile), fs.FileMode(dirPerms)) + if err != nil { + t.Log(err) + t.FailNow() + } + + filePerm := 0o715 + err = os.WriteFile(myFile, []byte("Some data"), fs.FileMode(filePerm)) + if err != nil { + t.Log(err) + t.FailNow() + } + + myFile2 := filepath.Join(newSys, fileRel) + err = os.MkdirAll(filepath.Dir(myFile2), fs.FileMode(dirPerms)) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = os.WriteFile(myFile2, []byte("Some other data"), fs.FileMode(filePerm)) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = BuildNewEtc(oldSys, oldUser, newSys, newUser) + if err != nil { + t.Log(err) + t.FailNow() + } + + newUserFile := filepath.Join(newUser, fileRel) + + _, err = os.Lstat(newUserFile) + + if err != nil { + t.Fatal("file was cleaned up even though it's not identical") + } +} + +func TestCleanup3(t *testing.T) { + var err error + oldSys, newSys, oldUser, newUser := setupEnvironment(t) + + fileRel := "some/path to/file/my file.abc" + myFile := filepath.Join(oldUser, fileRel) + dirPerms := 0o751 + err = os.MkdirAll(filepath.Dir(myFile), fs.FileMode(dirPerms)) + if err != nil { + t.Log(err) + t.FailNow() + } + + filePerm := 0o715 + err = os.WriteFile(myFile, []byte("Some data"), fs.FileMode(filePerm)) + if err != nil { + t.Log(err) + t.FailNow() + } + + myFile2 := filepath.Join(newSys, fileRel) + err = os.MkdirAll(filepath.Dir(myFile2), fs.FileMode(dirPerms)) + if err != nil { + t.Log(err) + t.FailNow() + } + + filePerm2 := 0o777 + err = os.WriteFile(myFile2, []byte("Some data"), fs.FileMode(filePerm2)) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = BuildNewEtc(oldSys, oldUser, newSys, newUser) + if err != nil { + t.Log(err) + t.FailNow() + } + + newUserFile := filepath.Join(newUser, fileRel) + + _, err = os.Lstat(newUserFile) + + if err != nil { + t.Fatal("file was cleaned up even though the attributes were not identical") + } +} + +func TestCleanupSymlink(t *testing.T) { + var err error + oldSys, newSys, oldUser, newUser := setupEnvironment(t) + + fileRel := "some/path to/file/my file.abc" + myFile := filepath.Join(oldUser, fileRel) + dirPerms := 0o751 + err = os.MkdirAll(filepath.Dir(myFile), fs.FileMode(dirPerms)) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = os.Symlink("some/link", myFile) + if err != nil { + t.Log(err) + t.FailNow() + } + err = os.Symlink("some/link", myFile+"different") + if err != nil { + t.Log(err) + t.FailNow() + } + + myFile2 := filepath.Join(newSys, fileRel) + err = os.MkdirAll(filepath.Dir(myFile2), fs.FileMode(dirPerms)) + if err != nil { + t.Log(err) + t.FailNow() + } + + err = os.Symlink("some/link", myFile2) + if err != nil { + t.Log(err) + t.FailNow() + } + err = os.Symlink("some/other/link", myFile2+"different") + if err != nil { + t.Log(err) + t.FailNow() + } + + err = BuildNewEtc(oldSys, oldUser, newSys, newUser) + if err != nil { + t.Log(err) + t.FailNow() + } + + newUserFile := filepath.Join(newUser, fileRel) + + _, err = os.Lstat(newUserFile) + + if err == nil { + t.Fatal("symlink was not cleaned up even though it was identical") + } + + newUserFileDifferent := filepath.Join(newUser, fileRel+"different") + + _, err = os.Lstat(newUserFileDifferent) + + if err != nil { + t.Fatal("symlink was cleaned up even though it was different") + } +} diff --git a/core/filetypes.go b/core/filetypes.go new file mode 100644 index 0000000..f1dbc85 --- /dev/null +++ b/core/filetypes.go @@ -0,0 +1,211 @@ +package core + +import ( + "fmt" + "hash/crc32" + "io" + "os" + "syscall" + "time" +) + +type Symlink struct{} + +func (s *Symlink) SupportsFile(info os.FileInfo) bool { + return info.Mode()&os.ModeSymlink != 0 +} + +func (s *Symlink) Copy(fromInfo os.FileInfo, from, to string) error { + target, err := os.Readlink(from) + if err != nil { + return fmt.Errorf("can't read symlink: %w", err) + } + + err = os.Symlink(target, to) + if err != nil { + return fmt.Errorf("can't create symlink: %w", err) + } + + return nil +} + +func (s *Symlink) CopyAttributes(fromInfo os.FileInfo, to string) error { + return nil +} + +func (s *Symlink) IsIdentical(a, b os.FileInfo, aPath, bPath string) (bool, error) { + aTarget, err := os.Readlink(aPath) + if err != nil { + return false, err + } + + bTarget, err := os.Readlink(bPath) + if err != nil { + return false, err + } + + return aTarget == bTarget, nil +} + +type Folder struct{} + +func (f *Folder) SupportsFile(info os.FileInfo) bool { + return info.Mode().IsDir() +} + +func (f *Folder) Copy(fromInfo os.FileInfo, from, to string) error { + err := os.Mkdir(to, fromInfo.Mode()) + if err != nil { + return fmt.Errorf("can't make directory: %w", err) + } + + return nil +} + +func (f *Folder) CopyAttributes(fromInfo os.FileInfo, to string) error { + return copyAttributes(fromInfo, to) +} + +func (f *Folder) IsIdentical(a, b os.FileInfo, aPath, bPath string) (bool, error) { + if a.Name() != b.Name() { + return false, nil + } + + return compareAttributes(a, b), nil +} + +type RegularFile struct{} + +func (f *RegularFile) SupportsFile(info os.FileInfo) bool { + return info.Mode().IsRegular() +} + +func (f *RegularFile) Copy(fromInfo os.FileInfo, from, to string) error { + fromFile, err := os.OpenFile(from, os.O_RDONLY, 0) + if err != nil { + return fmt.Errorf("can't open file for reading: %w", err) + } + defer fromFile.Close() + + toFile, err := os.OpenFile(to, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fromInfo.Mode()) + if err != nil { + return fmt.Errorf("can't create file: %w", err) + } + defer toFile.Close() + + _, err = io.Copy(toFile, fromFile) + if err != nil { + return fmt.Errorf("can't copy data: %w", err) + } + + return nil +} + +func (f *RegularFile) CopyAttributes(fromInfo os.FileInfo, to string) error { + return copyAttributes(fromInfo, to) +} + +func (f *RegularFile) IsIdentical(a, b os.FileInfo, aPath, bPath string) (bool, error) { + if a.Name() != b.Name() { + return false, nil + } + + if !compareAttributes(a, b) { + return false, nil + } + + checkA, err := calculateChecksum(aPath) + if err != nil { + return false, err + } + checkB, err := calculateChecksum(bPath) + if err != nil { + return false, err + } + + return checkA == checkB, nil +} + +type CharDeviceFile struct{} + +func (f *CharDeviceFile) SupportsFile(info os.FileInfo) bool { + return info.Mode()&os.ModeCharDevice != 0 +} + +func (f *CharDeviceFile) Copy(fromInfo os.FileInfo, from, to string) error { + fromInfoUnix := fromInfo.Sys().(*syscall.Stat_t) + + err := syscall.Mknod(to, fromInfoUnix.Mode, int(fromInfoUnix.Rdev)) + if err != nil { + return fmt.Errorf("can't create character special file: %w", err) + } + + return nil +} + +func (f *CharDeviceFile) CopyAttributes(fromInfo os.FileInfo, to string) error { + return copyAttributes(fromInfo, to) +} + +func (f *CharDeviceFile) IsIdentical(a, b os.FileInfo, aPath, bPath string) (bool, error) { + if a.Name() != b.Name() { + return false, nil + } + + if !compareAttributes(a, b) { + return false, nil + } + + aInfoUnix := a.Sys().(*syscall.Stat_t) + bInfoUnix := b.Sys().(*syscall.Stat_t) + + return aInfoUnix.Rdev == bInfoUnix.Rdev, nil +} + +var allATime = time.Now() + +func copyAttributes(fromInfo os.FileInfo, to string) error { + fromInfoUnix := fromInfo.Sys().(*syscall.Stat_t) + + err := syscall.Chown(to, int(fromInfoUnix.Uid), int(fromInfoUnix.Gid)) + if err != nil { + return fmt.Errorf("can't change owner: %w", err) + } + + err = syscall.Chmod(to, fromInfoUnix.Mode) + if err != nil { + return fmt.Errorf("can't change permissions: %w", err) + } + + err = os.Chtimes(to, allATime, fromInfo.ModTime()) + if err != nil { + return fmt.Errorf("can't change mod time: %w", err) + } + + return nil +} + +func compareAttributes(a, b os.FileInfo) bool { + aPerm := a.Mode().Perm() + bPerm := b.Mode().Perm() + aSysMode := a.Sys().(*syscall.Stat_t) + bSysMode := b.Sys().(*syscall.Stat_t) + aUid := aSysMode.Uid + bUid := bSysMode.Uid + aGid := aSysMode.Gid + bGid := bSysMode.Gid + + return aPerm == bPerm && aUid == bUid && aGid == bGid +} + +func calculateChecksum(file string) (uint32, error) { + calculator := crc32.NewIEEE() + osFile, err := os.Open(file) + if err != nil { + return 0, err + } + if _, err := io.Copy(calculator, osFile); err != nil { + return 0, err + } + return calculator.Sum32(), nil +} diff --git a/core/fixperms.go b/core/fixperms.go new file mode 100644 index 0000000..927c308 --- /dev/null +++ b/core/fixperms.go @@ -0,0 +1,80 @@ +package core + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "syscall" +) + +func ApplyOwnerMappingRecursive(dir string, uidMapping map[int]int, gidMapping map[int]int) error { + return applyOwnerMappingRecursive(dir, uidMapping, gidMapping, syscall.Chown) +} + +func applyOwnerMappingRecursive(dir string, uidMapping map[int]int, gidMapping map[int]int, chownFn func(string, int, int) error) error { + err := fs.WalkDir(os.DirFS(dir), ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't search path \"%s\": %w", path, err) + } + + err = applyOwnerMapping(filepath.Join(dir, path), uidMapping, gidMapping, chownFn) + if err != nil { + return fmt.Errorf("can't apply ownership of %s: %w", path, err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't copy all files: %w", err) + } + + return nil +} + +func ApplyOwnerMapping(path string, uidMapping map[int]int, gidMapping map[int]int) error { + return applyOwnerMapping(path, uidMapping, gidMapping, syscall.Chown) +} + +func applyOwnerMapping(path string, uidMapping map[int]int, gidMapping map[int]int, chownFn func(string, int, int) error) error { + info, err := os.Lstat(path) + if err != nil { + return fmt.Errorf("can't get info about file: %w", err) + } + + if isSymlink(info) { + return nil + } + + infoUnix := info.Sys().(*syscall.Stat_t) + + oldUid := int(infoUnix.Uid) + newUid, ok := uidMapping[oldUid] + if !ok { + oldUid = newUid + } + + oldGid := int(infoUnix.Gid) + newGid, ok := gidMapping[oldGid] + if !ok { + newGid = oldGid + } + + if newUid == oldUid && newGid == oldGid { + return nil + } + + fmt.Println("changing ownership of:", path) + + err = chownFn(path, newUid, newGid) + if err != nil { + return fmt.Errorf("can't change owner: %w", err) + } + + return nil +} + +func isSymlink(info os.FileInfo) bool { + return info.Mode()&os.ModeSymlink != 0 +} diff --git a/core/fixperms_test.go b/core/fixperms_test.go new file mode 100644 index 0000000..4ca315e --- /dev/null +++ b/core/fixperms_test.go @@ -0,0 +1,26 @@ +package core + +import ( + "os" + "path/filepath" + "testing" +) + +func TestNoop(t *testing.T) { + dir := t.TempDir() + + testfile := filepath.Join(dir, "some weird testfile.x") + + err := os.WriteFile(testfile, []byte("test content"), 0o743) + if err != nil { + t.Fatal(err) + } + + err = applyOwnerMappingRecursive(dir, map[int]int{}, map[int]int{}, func(path string, uid, gid int) error { + t.Fatal("changed file", path, "even though no mapping was set") + return nil + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/core/groups.go b/core/groups.go new file mode 100644 index 0000000..92dfbdc --- /dev/null +++ b/core/groups.go @@ -0,0 +1,234 @@ +package core + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strconv" + "strings" +) + +func CreateGroupMapping(from, to GroupFile) (map[int]int, error) { + mapping := make(map[int]int) + + for key, val := range from.Contents { + to, ok := to.Contents[key] + if !ok { + return nil, errors.New("can't find " + key + " in other map") + } + mapping[val.Gid] = to.Gid + } + + return mapping, nil +} + +type GroupEntry struct { + Name string + Password string + Gid int + Users []string +} + +func NewGroupFile(path string) (*GroupFile, error) { + groupFile := GroupFile{Filepath: path} + groupFile.Contents = make(map[string]GroupEntry) + err := groupFile.parse() + if err != nil { + return nil, fmt.Errorf("can't parse group file: %w", err) + } + return &groupFile, nil +} + +type GroupFile struct { + Filepath string + Contents map[string]GroupEntry +} + +func (e *GroupFile) WriteToFile(path string) error { + lines := make(map[int]string) + gids := []int{} + + for _, entry := range e.Contents { + line := entry.Name + ":" + + entry.Password + ":" + + strconv.Itoa(entry.Gid) + ":" + + strings.Join(entry.Users, ",") + "\n" + + lines[entry.Gid] = line + gids = append(gids, entry.Gid) + } + + slices.Sort(gids) + + fileContent := "" + + for _, gid := range gids { + fileContent += lines[gid] + } + + err := os.WriteFile(path, []byte(fileContent), 0o644) + if err != nil { + return fmt.Errorf("can't write file: %w", err) + } + + return nil +} + +func (e *GroupFile) parse() error { + groupContents, err := os.ReadFile(e.Filepath) + if err != nil { + return fmt.Errorf("can't read group file: %w", err) + } + + for line := range strings.SplitAfterSeq(string(groupContents), "\n") { + line := strings.TrimSpace(line) + + if len(line) == 0 { + continue + } + + fields := strings.Split(line, ":") + if len(fields) != 4 { + continue + } + + if fields[0] == "" { + continue + } + + gid, err := strconv.Atoi(fields[2]) + if err != nil { + continue + } + users := strings.Split(fields[3], ",") + + if len(users) == 1 && users[0] == "" { + users = []string{} + } + + entry := GroupEntry{Name: fields[0], Password: fields[1], Gid: gid, Users: users} + + e.Contents[entry.Name] = entry + } + + return nil +} + +const LowestSystemGid = 101 +const HighestSystemGid = 999 + +var ErrNoGidsLeft = errors.New("All available GIDs are taken") + +func (e *GroupFile) AddSystemGroup(name string, requestGid int, password string, users []string) (int, error) { + if existing, alreadyExists := e.Contents[name]; alreadyExists { + return existing.Gid, nil + } + + gidExists := make(map[int]bool) + + for _, value := range e.Contents { + gidExists[value.Gid] = true + } + + entry := GroupEntry{Name: name, Gid: requestGid, Password: password, Users: users} + + if !gidExists[requestGid] { + e.Contents[name] = entry + + return requestGid, nil + } + + for gid := HighestSystemGid; gid >= LowestSystemGid; gid-- { + if gidExists[gid] { + continue + } + + entry.Gid = gid + e.Contents[name] = entry + + return gid, nil + } + + return -1, ErrNoGidsLeft +} + +func (e *GroupFile) MergeWithOther(other GroupFile) []error { + errList := []error{} + + for name, entry := range other.Contents { + if _, exists := e.Contents[name]; exists { + continue + } + + _, err := e.AddSystemGroup(entry.Name, entry.Gid, entry.Password, entry.Users) + if err != nil { + errList = append(errList, err) + continue + } + } + + return errList +} + +// MergeInGshadow merges extra entries from the gshadow file in extraGshadowDir into the gshadow file in gshadowDir +// +// returns the number of added lines and and errors for reading or writing files +func MergeInGshadow(gshadowDir string, extraGshadowDir string) (int, error) { + gshadowFilePath := filepath.Join(gshadowDir, "gshadow") + extraGshadowFilePath := filepath.Join(extraGshadowDir, "gshadow") + + gshadowFileContents, err := os.ReadFile(gshadowFilePath) + if err != nil { + return 0, fmt.Errorf("can't open gshadow file: %w", err) + } + extraGshadowFileContents, err := os.ReadFile(extraGshadowFilePath) + if err != nil { + return 0, fmt.Errorf("can't open extra gshadow file: %w", err) + } + + gshadowEntries := make(map[string]string) + for line := range strings.SplitAfterSeq(string(gshadowFileContents), "\n") { + line = strings.TrimSpace(line) + name, info, _ := strings.Cut(line, ":") + if name == "" { + continue + } + + gshadowEntries[name] = info + } + + newLines := 0 + + for line := range strings.SplitAfterSeq(string(extraGshadowFileContents), "\n") { + line = strings.TrimSpace(line) + name, info, _ := strings.Cut(line, ":") + if name == "" { + continue + } + + _, ok := gshadowEntries[name] + if !ok { + gshadowEntries[name] = info + newLines += 1 + } + } + + if newLines == 0 { + return 0, nil + } + + newFileContents := "" + + for name, info := range gshadowEntries { + newFileContents += name + ":" + info + "\n" + } + + err = os.WriteFile(gshadowFilePath, []byte(newFileContents), 0o640) + if err != nil { + return 0, fmt.Errorf("can't write gshadow file: %w", err) + } + + return newLines, nil +} diff --git a/core/users.go b/core/users.go new file mode 100644 index 0000000..0f1a6d9 --- /dev/null +++ b/core/users.go @@ -0,0 +1,242 @@ +package core + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strconv" + "strings" +) + +type PasswdEntry struct { + Name string + Password string + Uid int + Gid int + Gecos string + Directory string + Shell string +} + +func NewPasswdFile(path string) (*PasswdFile, error) { + passFile := PasswdFile{Filepath: path} + passFile.Contents = make(map[string]PasswdEntry) + err := passFile.parse() + if err != nil { + return nil, fmt.Errorf("can't parse file: %w", err) + } + return &passFile, nil +} + +type PasswdFile struct { + Filepath string + Contents map[string]PasswdEntry +} + +func (e *PasswdFile) WriteToFile(path string) error { + lines := make(map[int]string) + uids := []int{} + + for _, entry := range e.Contents { + line := entry.Name + ":" + + entry.Password + ":" + + strconv.Itoa(entry.Uid) + ":" + + strconv.Itoa(entry.Gid) + ":" + + entry.Gecos + ":" + + entry.Directory + ":" + + entry.Shell + "\n" + + lines[entry.Uid] = line + uids = append(uids, entry.Uid) + } + + slices.Sort(uids) + + fileContent := "" + + for _, uid := range uids { + fileContent += lines[uid] + } + + err := os.WriteFile(path, []byte(fileContent), 0o644) + if err != nil { + return fmt.Errorf("can't write file: %w", err) + } + + return nil +} + +func (e *PasswdFile) parse() error { + passwdContents, err := os.ReadFile(e.Filepath) + if err != nil { + return fmt.Errorf("can't read file: %w", err) + } + + for line := range strings.SplitAfterSeq(string(passwdContents), "\n") { + line := strings.TrimSpace(line) + + if len(line) == 0 { + continue + } + + fields := strings.Split(line, ":") + if len(fields) != 7 { + continue + } + + uid, err := strconv.Atoi(fields[2]) + if err != nil { + continue + } + gid, err := strconv.Atoi(fields[3]) + if err != nil { + continue + } + + entry := PasswdEntry{Name: fields[0], Password: fields[1], Uid: uid, Gid: gid, Gecos: fields[4], Directory: fields[5], Shell: fields[6]} + + e.Contents[entry.Name] = entry + } + + return nil +} + +const LowestSystemUid = 101 +const HighestSystemUid = 999 + +var ErrNoUidsLeft = errors.New("All available UIDs are taken") + +func (e *PasswdFile) AddSystemUser(name string, gid int, requestUid int, password string, gecos string, directory string, shell string) (int, error) { + if existing, alreadyExists := e.Contents[name]; alreadyExists { + return existing.Uid, nil + } + + uidExists := make(map[int]bool) + + for _, value := range e.Contents { + uidExists[value.Uid] = true + } + + entry := PasswdEntry{Name: name, Uid: requestUid, Gid: gid, Password: password, Gecos: gecos, Directory: directory, Shell: shell} + + if !uidExists[requestUid] { + e.Contents[name] = entry + + return requestUid, nil + } + + for uid := HighestSystemUid; uid >= LowestSystemUid; uid-- { + if uidExists[uid] { + continue + } + + entry.Uid = uid + + e.Contents[name] = entry + + return uid, nil + } + + return -1, ErrNoUidsLeft +} + +func (e *PasswdFile) MergeWithOther(other PasswdFile, groupMapping map[int]int, nogroupID int) []error { + errList := []error{} + + for name, entry := range other.Contents { + if _, exists := e.Contents[name]; exists { + continue + } + + gid, ok := groupMapping[entry.Gid] + if !ok { + gid = nogroupID + } + + _, err := e.AddSystemUser(name, gid, entry.Uid, entry.Password, entry.Gecos, entry.Directory, entry.Shell) + if err != nil { + errList = append(errList, err) + continue + } + } + + return errList + +} + +func CreateUserMapping(from, to PasswdFile) (map[int]int, error) { + mapping := make(map[int]int) + + for key, val := range from.Contents { + to, ok := to.Contents[key] + if !ok { + return nil, errors.New("can't find " + key + " in other map") + } + mapping[val.Uid] = to.Uid + } + + return mapping, nil +} + +// MergeInShadow merges extra entries from the shadow file in extraShadowDir into the shadow file in shadowDir +// +// returns the number of added lines and and errors for reading or writing files +func MergeInShadow(shadowDir string, extraShadowDir string) (int, error) { + shadowFilePath := filepath.Join(shadowDir, "shadow") + extraShadowFilePath := filepath.Join(extraShadowDir, "shadow") + + shadowFileContents, err := os.ReadFile(shadowFilePath) + if err != nil { + return 0, fmt.Errorf("can't open shadow file: %w", err) + } + extraShadowFileContents, err := os.ReadFile(extraShadowFilePath) + if err != nil { + return 0, fmt.Errorf("can't open extra shadow file: %w", err) + } + + shadowEntries := make(map[string]string) + for line := range strings.SplitAfterSeq(string(shadowFileContents), "\n") { + line = strings.TrimSpace(line) + name, info, _ := strings.Cut(line, ":") + if name == "" { + continue + } + + shadowEntries[name] = info + } + + newLines := 0 + + for line := range strings.SplitAfterSeq(string(extraShadowFileContents), "\n") { + line = strings.TrimSpace(line) + name, info, _ := strings.Cut(line, ":") + if name == "" { + continue + } + + _, ok := shadowEntries[name] + if !ok { + shadowEntries[name] = info + newLines += 1 + } + } + + if newLines == 0 { + return 0, nil + } + + newFileContents := "" + + for name, info := range shadowEntries { + newFileContents += name + ":" + info + "\n" + } + + err = os.WriteFile(shadowFilePath, []byte(newFileContents), 0o640) + if err != nil { + return 0, fmt.Errorf("can't write shadow file: %w", err) + } + + return newLines, nil +} diff --git a/go.mod b/go.mod index 336d79f..63f3892 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,10 @@ module github.com/linux-immutability-tools/EtcBuilder -go 1.20 +go 1.24.4 -require ( - github.com/spf13/cobra v1.7.0 -) +require github.com/spf13/cobra v1.10.1 require ( - github.com/BurntSushi/toml v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/spf13/pflag v1.0.9 // indirect ) diff --git a/go.sum b/go.sum index 69d73e7..e613680 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,10 @@ -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/passwd.user b/passwd.user deleted file mode 100644 index a664b81..0000000 --- a/passwd.user +++ /dev/null @@ -1,4 +0,0 @@ -pulse:*:563:563:PulseAudio System User:/nonexistent:/usr/sbin/nologin -_sndio:*:702:702:sndio privsep:/var/empty:/usr/sbin/nologin -cyrus:*:60:60:the cyrus mail server:/nonexistent:/usr/sbin/nologin -webcamd:*:145:145:Webcamd user:/var/empty:/usr/sbin/nologin \ No newline at end of file diff --git a/settings/config.go b/settings/config.go deleted file mode 100644 index ac404e9..0000000 --- a/settings/config.go +++ /dev/null @@ -1,52 +0,0 @@ -package settings - -import ( - "fmt" - "os" - "strings" - - "github.com/BurntSushi/toml" -) - -var OverwriteFiles []string -var SpecialFiles []string - -type Config struct { - OverwriteFiles []string - SpecialFiles []string -} - -func GatherConfigFiles() error { - configFiles, err := os.ReadDir("/usr/share/etcbuilder/") - if os.IsNotExist(err) { - return nil - } - if err != nil { - return err - } - - for _, config := range configFiles { - if !strings.Contains(config.Name(), ".toml") { - continue - } - parseConfigFile("/usr/share/etcbuilder/" + config.Name()) - } - - return nil -} - -func parseConfigFile(file string) { - var conf Config - configData, err := os.ReadFile(file) - if err != nil { - fmt.Printf("err: %v\n", err) - return - } - _, err = toml.Decode(string(configData), &conf) - if err != nil { - _ = fmt.Errorf("ERROR: Failed to parse configuration file %s", file) - fmt.Printf("err: %v\n", err) - } - OverwriteFiles = append(OverwriteFiles, conf.OverwriteFiles...) - SpecialFiles = append(SpecialFiles, conf.SpecialFiles...) -}