diff --git a/.gitignore b/.gitignore index 3a30eb64..415f6b6e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ # IDEs .idea/ + +# Build timestamp +tools/bcdhive_generator/.build diff --git a/Makefile b/Makefile index 880c4db0..f27f0d16 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ # Go parameters GOCMD?=go +GO_VERSION=$(shell go list -m -f "{{.GoVersion}}") PACKAGE_BASE=github.com/buildpacks/imgutil all: test @@ -20,5 +21,22 @@ lint: install-golangci-lint @echo "> Linting code..." @golangci-lint run -c golangci.yaml -test: format lint +tools/bcdhive_generator/.build: $(wildcard tools/bcdhive_generator/*) +ifneq ($(OS),Windows_NT) + @echo "> Building bcdhive-generator in Docker using current golang version" + docker build tools/bcdhive_generator --tag bcdhive-generator --build-arg go_version=$(GO_VERSION) + + @touch tools/bcdhive_generator/.build +else + @echo "> Cannot generate on Windows" +endif + +layer/bcdhive_generated.go: layer/windows_baselayer.go tools/bcdhive_generator/.build +ifneq ($(OS),Windows_NT) + $(GOCMD) generate ./... +else + @echo "> Cannot generate on Windows" +endif + +test: layer/bcdhive_generated.go format lint $(GOCMD) test -parallel=1 -count=1 -v ./... diff --git a/go.sum b/go.sum index 6fef1705..affec3fb 100644 --- a/go.sum +++ b/go.sum @@ -122,14 +122,6 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-containerregistry v0.0.0-20200311163244-4b1985e5ea21 h1:rz5VzU1xKHR8HDtifeAJ+SPwE0v3YW0AEien/Lobgww= diff --git a/layer/bcdhive_generated.go b/layer/bcdhive_generated.go new file mode 100644 index 00000000..65751772 --- /dev/null +++ b/layer/bcdhive_generated.go @@ -0,0 +1,31 @@ +package layer + +// Code generated by github.com/buildpacks/imgutil/tools/bcdhive_generator DO NOT EDIT + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "io/ioutil" +) + +const encodedBytes = "H4sIAAAAAAAC/+yaPWwbZRjH/5cmJAQoF/tcBanDoUSwcJXPvri+KaJN2gAlQZBWDBm4j9fkajuxbAOtqqCMGTuwMCB5YOhCxcYICxJiQB4Zu9EBoUgsgYEXPfeR88dZbRFDpT6/6HyP3/d5/T73PH/Feh+5LT6uKQpA14Oj/lcvff0zmZhBBNk6oheyd7CKVezgOjZxGTvooo0AHuqh3UQLO1jDOq7gTVzHNWyDeVr5Pqj+zVlgGIZhGIZhGIZ5Nth1g73QUNOxpA9Adk9KuVd/A/rrl75Mxn6YBaaS9QuAlFKSTffD+H4uY68XASwvL7/3/tb21oWNt27Q2E//SNmpA7SOrhcAnFcABdNfQAFUJVqrgfb8HfPhu0W8gikoURAzOoBXyZ5fpPeHY+MZ/ksT/Jdi/1W8POSvp/7q2Di0yD+KdSZHr/Na5Ds4NvFzh3zPhH2XEZ/hzwE2zrJ2GYZhGIZhGIZhmCc8/6vD5/9BovO/PnT+1wfmZQwdV++q0Rk+6QckvouxTWf7NdHx2kGrG+ynbYfGrgJdBfzyj78cSyl7KtCP4+lLKT+tq5imczpA9/B3CFeCdvMzpy3e3feDWiD8095F6D8b7nmsRr50vSNubzpNEc5fwmWsoTjyF3FvwvMm/Y4NLbvfcS0jd7MAttybwut28FBK2didOn1OdQH4Y/3+20cZ+9Fcsh/yj99feQ3AnYpT8cya6Rortm8Zpikcw7bKVcO2iiVRcVzftqsHad4PF4AP/lz9K6vONDdaZ5q4lRuv81xcm0fVeS4XPf8JPUsuqdd0WN8z0NWkvtu3W2KS/iiuuC2EWxPykyVninG9IZpir9tJxpK6JHH1coB9/7d7Wfv2ctn5OMqP5+MsgPMD+5qlImEl88fhvgoW88DSN899R/m4m0/1S2u/zaf6jeMO5118ggAN+GjBCX9708EFBNinWB6hpw8LT6gn2xdOtVLyjBXP9w1LXCwajueZRq1ccsuW5VsX7ZWDNI+Jnvp54Orn3atZeaS5rDz2tf+uq4+0VFe/asO6moL6WLrq51Nd9Qv/j65O49Im6+pEy87Hg0K2rjYH9i2VQ12VR3XVKgAn8zduUj4eFlJd0dq5c+O6ov97d1CBgwo8mKjBhAsDK7Dhw4IBEyYEHBiwYaGMamwVUYIIV7rwYcNGFQfp98fz/B3LMAzDMAzzNPFvAAAA///Odx8+ADAAAA==" + +func BaseLayerBCD() ([]byte, error) { + gzipBytes, err := base64.StdEncoding.DecodeString(encodedBytes) + if err != nil { + return nil, err + } + + gzipReader, err := gzip.NewReader(bytes.NewBuffer(gzipBytes)) + if err != nil { + return nil, err + } + + decodedBytes, err := ioutil.ReadAll(gzipReader) + if err != nil { + return nil, err + } + + return decodedBytes, nil +} diff --git a/layer/windows_baselayer.go b/layer/windows_baselayer.go new file mode 100644 index 00000000..d973bb74 --- /dev/null +++ b/layer/windows_baselayer.go @@ -0,0 +1,84 @@ +package layer + +import ( + "archive/tar" + "bytes" + "io" +) + +// Generate using `make generate` +//go:generate docker run --rm -v $PWD:/out/ bcdhive-generator -file=/out/layer/bcdhive_generated.go -package=layer -func=BaseLayerBCD + +// Windows base layers must follow this pattern: +// \-> UtilityVM/Files/EFI/Microsoft/Boot/BCD (file must exist and a valid BCD format - from bcdhive_gen) +// \-> Files/Windows/System32/config/DEFAULT (file and must exist but can be empty) +// \-> Files/Windows/System32/config/SAM (file must exist but can be empty) +// \-> Files/Windows/System32/config/SECURITY (file must exist but can be empty) +// \-> Files/Windows/System32/config/SOFTWARE (file must exist but can be empty) +// \-> Files/Windows/System32/config/SYSTEM (file must exist but can be empty) +// Refs: +// https://github.com/microsoft/hcsshim/blob/master/internal/wclayer/legacy.go +// https://github.com/moby/moby/blob/master/daemon/graphdriver/windows/windows.go#L48 +func WindowsBaseLayer() (io.Reader, error) { + bcdBytes, err := BaseLayerBCD() + if err != nil { + return nil, err + } + + layerBuffer := &bytes.Buffer{} + tw := tar.NewWriter(layerBuffer) + + if err := tw.WriteHeader(&tar.Header{Name: "UtilityVM", Typeflag: tar.TypeDir}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "UtilityVM/Files", Typeflag: tar.TypeDir}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "UtilityVM/Files/EFI", Typeflag: tar.TypeDir}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "UtilityVM/Files/EFI/Microsoft", Typeflag: tar.TypeDir}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "UtilityVM/Files/EFI/Microsoft/Boot", Typeflag: tar.TypeDir}); err != nil { + return nil, err + } + + if err := tw.WriteHeader(&tar.Header{Name: "UtilityVM/Files/EFI/Microsoft/Boot/BCD", Size: int64(len(bcdBytes)), Mode: 0644}); err != nil { + return nil, err + } + if _, err := tw.Write(bcdBytes); err != nil { + return nil, err + } + + if err := tw.WriteHeader(&tar.Header{Name: "Files", Typeflag: tar.TypeDir}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "Files/Windows", Typeflag: tar.TypeDir}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32", Typeflag: tar.TypeDir}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config", Typeflag: tar.TypeDir}); err != nil { + return nil, err + } + + if err := tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config/DEFAULT", Size: 0, Mode: 0644}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config/SAM", Size: 0, Mode: 0644}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config/SECURITY", Size: 0, Mode: 0644}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config/SOFTWARE", Size: 0, Mode: 0644}); err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config/SYSTEM", Size: 0, Mode: 0644}); err != nil { + return nil, err + } + + return layerBuffer, nil +} diff --git a/local/local_test.go b/local/local_test.go index 1c410f61..a015a327 100644 --- a/local/local_test.go +++ b/local/local_test.go @@ -1080,7 +1080,6 @@ func testImage(t *testing.T, when spec.G, it spec.S) { repoName, dockerClient, local.FromBaseImage(repoName), - local.WithPreviousImage(repoName), ) h.AssertNil(t, err) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index c983e4ce..66ffeea7 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -2,11 +2,8 @@ package testhelpers import ( "archive/tar" - "bytes" - "compress/gzip" "context" "crypto/sha256" - "encoding/base64" "encoding/hex" "fmt" "io" @@ -14,13 +11,14 @@ import ( "math/rand" "net/http" "os" - "path" "regexp" "strings" "sync" "testing" "time" + "github.com/buildpacks/imgutil/layer" + dockertypes "github.com/docker/docker/api/types" dockercli "github.com/docker/docker/client" "github.com/google/go-cmp/cmp" @@ -222,137 +220,78 @@ func CreateSingleFileTarReader(path, txt string) io.ReadCloser { pr, pw := io.Pipe() go func() { - var err error - defer func() { + // Use the regular tar.Writer, as this isn't a layer tar. + tw := tar.NewWriter(pw) + + if err := tw.WriteHeader(&tar.Header{Name: path, Size: int64(len(txt)), Mode: 0644}); err != nil { pw.CloseWithError(err) - }() + } - tw := tar.NewWriter(pw) - defer tw.Close() + if _, err := tw.Write([]byte(txt)); err != nil { + pw.CloseWithError(err) + } - err = writeTarSingleFileLinux(tw, path, txt) // Use the Linux writer, as this isn't a layer tar. + if err := tw.Close(); err != nil { + pw.CloseWithError(err) + } + + if err := pw.Close(); err != nil { + pw.CloseWithError(err) + } }() return pr } -func CreateSingleFileLayerTar(layerPath, txt, osType string) (string, error) { - tarFile, err := ioutil.TempFile("", "create-single-file-layer-tar-path") - if err != nil { - return "", err - } - defer tarFile.Close() - - tw := tar.NewWriter(tarFile) - defer tw.Close() +type layerWriter interface { + WriteHeader(*tar.Header) error + Write([]byte) (int, error) + Close() error +} - // regular Linux layer - writeFunc := writeTarSingleFileLinux +func getLayerWriter(osType string, file *os.File) layerWriter { if osType == "windows" { - // regular Windows layer - writeFunc = writeTarSingleFileWindows + return layer.NewWindowsWriter(file) } + return tar.NewWriter(file) +} - err = writeFunc(tw, layerPath, txt) +func CreateSingleFileLayerTar(layerPath, txt, osType string) (string, error) { + tarFile, err := ioutil.TempFile("", "create-single-file-layer-tar-path") if err != nil { return "", err } + defer tarFile.Close() - return tarFile.Name(), nil -} + tw := getLayerWriter(osType, tarFile) -func writeTarSingleFileLinux(tw *tar.Writer, layerPath, txt string) error { if err := tw.WriteHeader(&tar.Header{Name: layerPath, Size: int64(len(txt)), Mode: 0644}); err != nil { - return err + return "", err } if _, err := tw.Write([]byte(txt)); err != nil { - return err + return "", err + } + + if err := tw.Close(); err != nil { + return "", err } - return nil + return tarFile.Name(), nil } -// WindowsBaseLayer returns a minimal windows base layer. -// This base layer cannot use for running but can be used for saving to a Windows daemon and container creation. -// Windows image layers must follow this pattern¹: -// - base layer² (always required; tar file with relative paths without "/" prefix; all parent directories require own tar entries) -// \-> Files/Windows/System32/config/DEFAULT (file must exist but can be empty) -// \-> Files/Windows/System32/config/SAM (file must exist but can be empty) -// \-> Files/Windows/System32/config/SECURITY (file must exist but can be empty) -// \-> Files/Windows/System32/config/SOFTWARE (file must exist but can be empty) -// \-> Files/Windows/System32/config/SYSTEM (file must exist but can be empty) -// \-> UtilityVM/Files/EFI/Microsoft/Boot/BCD (file must exist and a valid BCD format - via `bcdedit` tool as below) -// - normal or top layer (optional; tar file with relative paths without "/" prefix; all parent directories require own tar entries) -// \-> Files/ (required directory entry) -// \-> Files/mystuff.exe (optional container filesystem files - C:\mystuff.exe) -// \-> Hives/ (required directory entry) -// \-> Hives/DefaultUser_Delta (optional Windows reg hive delta; BCD format - HKEY_USERS\.DEFAULT additional content) -// \-> Hives/Sam_Delta (optional Windows reg hive delta; BCD format - HKEY_LOCAL_MACHINE\SAM additional content) -// \-> Hives/Security_Delta (optional Windows reg hive delta; BCD format - HKEY_LOCAL_MACHINE\SECURITY additional content) -// \-> Hives/Software_Delta (optional Windows reg hive delta; BCD format - HKEY_LOCAL_MACHINE\SOFTWARE additional content) -// \-> Hives/System_Delta (optional Windows reg hive delta; BCD format - HKEY_LOCAL_MACHINE\SYSTEM additional content) -// 1. This was all discovered experimentally and should be considered an undocumented API, subject to change when the Windows Daemon internals change -// 2. There are many other files in an "real" base layer but this is the minimum set which a Daemon can store and use to create an container func WindowsBaseLayer(t *testing.T) string { tarFile, err := ioutil.TempFile("", "windows-base-layer.tar") AssertNil(t, err) + defer tarFile.Close() - tw := tar.NewWriter(tarFile) - - //Valid BCD file required, containing Windows Boot Manager and Windows Boot Loader sections - //Note: Gzip/Base64 encoded only to inline the binary BCD file here - //CMD: `bcdedit /createstore c:\output-bcd & bcdedit /create {6a6c1f1b-59d4-11ea-9438-9402e6abd998} /d buildpacks.io /application osloader /store c:\output-bcd & bcdedit /create {bootmgr} /store c:\output-bcd & bcdedit /set {bootmgr} default {6a6c1f1b-59d4-11ea-9438-9402e6abd998} /store c:\output-bcd & bcdedit /enum all /store c:\output-bcd` - //BASH: `gzip --stdout --best output-bcd | base64` - bcdGzipBase64 := "H4sIABeDWF4CA+1YTWgTQRR+m2zSFItGkaJQcKXgyZX8bNKklxatUvyJoh4qKjS7O7GxzQ9Jai210JtFQXrUW4/e2ostCF4EoSCCF6HHQi9FWsxJepH43s5us02XYlFBcL7l7U7evHnzzZtvoJ0Ke5A7BABkoU/f3qxvfZEkbPuBg9oKNcK8fQ/68LkHBvTiuwTjUIOy9VZBR68J++NZ/sXU/J2vR99d/thxdzOz0PqbYp63+CqFWuH7wg+LGwj8MfRuvnovqiAgICAgICAgICAgIPB/YETPF8H+/96Bcw9A7flGo1EcPQvrRVginw99Q6cAfHbsEDYwpEFt+s7Y306PuTrQMmziVq1UYTdLpRr5HmNsdRSg38+N8o5Z9w7yzCDlt8ceB+rT7HlPQB8cAYn/CND9hOLjUZaf3xIEjlGelhiJX2wE7gOfy7lQeG2tU4Gt7vZlZ50KNNd5I7B7ncSVvlc91tmGdl1/yIxaFbYxph4EmLXzq/2nd/KHpGZ+RfbO71XHM2hTyWzSiOaiuppIm5oajbKsmtbiKXxFYiyZ1c10OjUNUMccZZxkGDkMsKpBfBS/s68KPHlbX3Kv10HDBvnXkKezr06/Yu0CB90dUe5KvlzLl7icNjB2LOeDbYn30VqpJmvofzTaZo2d8/H6k11hk5lsgQH1n4cLMACRlofDi/eItJc3ueoS15SbdwhN3od33eItQQqDorFIhPOVacyMH5SwbPO9PVlmgy79Un3I2n9Rvych+Ff0+6Grc9ldF6c/7PfWV9hDX1Sji2OswIq1qrOPiz5eqxU/7/Oab8XvvQ+f5b37cBityzUf1RqhOfqgvkW5qQ+bj6UPHcYhj1U2oQxZMGAUqnAOPSWMI33Pyc3z5j7P7vM2GDzgeUubLJtKxgw1YZimqrGeiJo1jKiai8f0uKaZWk86Md3UPdWezuiqzMd66XZV9q7XRuDgunXr1AfhXToFuy4rgaZOup82dvFwdLIW/D2djAQ4t3qA961avMIWL8leA30v5SuFiWyFXSuZ+VyemV68KIdXfYYlbz1lXLxicUtPcec8zwa5z9EXxYbb9uqLeExBEnWVRGVFIYemgwoJSKPeNGxF8WHYr6JHgzik7FYEYuinkTpGpvFJwfQO/5ch8beGgICAgICAwL+BnwAgqcMAIAAA" - bcdGzip, _ := base64.StdEncoding.DecodeString(bcdGzipBase64) - bcdReader, _ := gzip.NewReader(bytes.NewBuffer(bcdGzip)) - bcdBytes, _ := ioutil.ReadAll(bcdReader) - - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "Files", Typeflag: tar.TypeDir})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "Files/Windows", Typeflag: tar.TypeDir})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32", Typeflag: tar.TypeDir})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config", Typeflag: tar.TypeDir})) - - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "UtilityVM", Typeflag: tar.TypeDir})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "UtilityVM/Files", Typeflag: tar.TypeDir})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "UtilityVM/Files/EFI", Typeflag: tar.TypeDir})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "UtilityVM/Files/EFI/Microsoft", Typeflag: tar.TypeDir})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "UtilityVM/Files/EFI/Microsoft/Boot", Typeflag: tar.TypeDir})) - - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config/DEFAULT", Size: 0, Mode: 0644})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config/SAM", Size: 0, Mode: 0644})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config/SECURITY", Size: 0, Mode: 0644})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config/SOFTWARE", Size: 0, Mode: 0644})) - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "Files/Windows/System32/config/SYSTEM", Size: 0, Mode: 0644})) - - AssertNil(t, tw.WriteHeader(&tar.Header{Name: "UtilityVM/Files/EFI/Microsoft/Boot/BCD", Size: int64(len(bcdBytes)), Mode: 0644})) - _, err = tw.Write(bcdBytes) + baseLayer, err := layer.WindowsBaseLayer() AssertNil(t, err) - return tarFile.Name() -} - -func writeTarSingleFileWindows(tw *tar.Writer, containerPath, txt string) error { - // root Windows layer directories - if err := tw.WriteHeader(&tar.Header{Name: "Files", Typeflag: tar.TypeDir}); err != nil { - return err - } - if err := tw.WriteHeader(&tar.Header{Name: "Hives", Typeflag: tar.TypeDir}); err != nil { - return err - } - - // prepend file entries with "Files" - layerPath := path.Join("Files", containerPath) - if err := tw.WriteHeader(&tar.Header{Name: layerPath, Size: int64(len(txt)), Mode: 0644}); err != nil { - return err - } - - if _, err := tw.Write([]byte(txt)); err != nil { - return err - } + _, err = io.Copy(tarFile, baseLayer) + AssertNil(t, err) - return nil + return tarFile.Name() } func FetchManifestLayers(t *testing.T, repoName string) []string { diff --git a/tools/bcdhive_generator/Dockerfile b/tools/bcdhive_generator/Dockerfile new file mode 100644 index 00000000..cb51ca8e --- /dev/null +++ b/tools/bcdhive_generator/Dockerfile @@ -0,0 +1,15 @@ +ARG go_version +FROM golang:${go_version}-buster + +RUN apt update \ + && apt install -y libhivex-dev libhivex-bin libwin-hivex-perl + +COPY . /src + +WORKDIR /src + +RUN go generate ./ \ + && go test -parallel=1 -count=1 -v . \ + && go install . + +ENTRYPOINT ["/go/bin/bcdhive_gen"] diff --git a/tools/bcdhive_generator/bcdhive_encoded.go.tmpl b/tools/bcdhive_generator/bcdhive_encoded.go.tmpl new file mode 100644 index 00000000..e3ee7963 --- /dev/null +++ b/tools/bcdhive_generator/bcdhive_encoded.go.tmpl @@ -0,0 +1,31 @@ +package {{.PackageName}} + +// Code generated by github.com/buildpacks/imgutil/tools/bcdhive_generator DO NOT EDIT + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "io/ioutil" +) + +const encodedBytes = "{{.EncodedBytes}}" + +func {{.FuncName}}() ([]byte, error) { + gzipBytes, err := base64.StdEncoding.DecodeString(encodedBytes) + if err != nil { + return nil, err + } + + gzipReader, err := gzip.NewReader(bytes.NewBuffer(gzipBytes)) + if err != nil { + return nil, err + } + + decodedBytes, err := ioutil.ReadAll(gzipReader) + if err != nil { + return nil, err + } + + return decodedBytes, nil +} diff --git a/tools/bcdhive_generator/bcdhive_hivex.go b/tools/bcdhive_generator/bcdhive_hivex.go new file mode 100644 index 00000000..7535e611 --- /dev/null +++ b/tools/bcdhive_generator/bcdhive_hivex.go @@ -0,0 +1,168 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/gabriel-samfira/go-hivex" + "github.com/pkg/errors" + "golang.org/x/text/encoding/unicode" + "golang.org/x/tools/go/packages" +) + +// equivalent to `output-bcd` generated with: +// bcdedit /createstore c:\output-bcd +// bcdedit /create {6a6c1f1b-59d4-11ea-9438-9402e6abd998} /d buildpacks.io /application osloader /store c:\output-bcd +// bcdedit /create {bootmgr} /store c:\output-bcd +// bcdedit /set {bootmgr} default {6a6c1f1b-59d4-11ea-9438-9402e6abd998} /store c:\output-bcd +// bcdedit /enum all /store c:\output-bcd` +// Note: createstore guid is random +var orderedEntries = []entry{ + {"Description", []hivex.HiveValue{ + { + Type: hivex.RegDword, + Key: "FirmwareModified", + Value: toRegDword(0x00, 0x00, 0x00, 0x01), + }, + { + Type: hivex.RegSz, + Key: "KeyName", + Value: toRegSz("BCD00000000"), + }, + }}, + {`Objects/{6a6c1f1b-59d4-11ea-9438-9402e6abd998}/Description`, []hivex.HiveValue{ + { + Type: hivex.RegDword, + Key: "Type", + Value: toRegDword(0x10, 0x20, 0x00, 0x03), + }, + }}, + {`Objects/{6a6c1f1b-59d4-11ea-9438-9402e6abd998}/Elements/12000004`, []hivex.HiveValue{ + { + Type: hivex.RegSz, + Key: "Element", + Value: toRegSz("buildpacks.io"), + }, + }}, + {`Objects/{9dea862c-5cdd-4e70-acc1-f32b344d4795}/Description`, []hivex.HiveValue{ + { + Type: hivex.RegDword, + Key: "Type", + Value: toRegDword(0x10, 0x10, 0x00, 0x02), + }, + }}, + {`Objects/{9dea862c-5cdd-4e70-acc1-f32b344d4795}/Elements/23000003`, []hivex.HiveValue{ + { + Type: hivex.RegSz, + Key: "Element", + Value: toRegSz("{6a6c1f1b-59d4-11ea-9438-9402e6abd998}"), + }, + }}, +} + +type entry struct { + path string + hiveValues []hivex.HiveValue +} + +func HiveBCD() ([]byte, error) { + hiveFile, err := ioutil.TempFile("", "") + if err != nil { + return nil, err + } + hiveFile.Close() + defer os.Remove(hiveFile.Name()) + + origHiveBytes, err := readMinimalHiveContents() + if err != nil { + return nil, err + } + + if err := ioutil.WriteFile(hiveFile.Name(), origHiveBytes, 0666); err != nil { + return nil, errors.Wrap(err, "writing temp hive file") + } + + h, err := hivex.NewHivex(hiveFile.Name(), hivex.WRITE) + if err != nil { + return nil, errors.Wrap(err, "opening hive file") + } + defer h.Close() + + if err := addBCDHiveEntries(h); err != nil { + return nil, errors.Wrap(err, "adding hive entries") + } + + hiveBytes, err := ioutil.ReadFile(hiveFile.Name()) + if err != nil { + return nil, err + } + + return hiveBytes, nil +} + +//readMinimalHiveContents finds the `minimal` hive binary from the package as there's no way to create this file +func readMinimalHiveContents() ([]byte, error) { + pkgs, err := packages.Load(&packages.Config{}, "github.com/gabriel-samfira/go-hivex") + if err != nil { + return nil, err + } + if len(pkgs) != 1 || len(pkgs[0].GoFiles) != 1 { + return nil, errors.New("hivex module root not found") + } + hivexRootPath := filepath.Dir(pkgs[0].GoFiles[0]) + minimalHivePath := filepath.Join(hivexRootPath, "testdata", "minimal") + return ioutil.ReadFile(minimalHivePath) +} + +func addBCDHiveEntries(h *hivex.Hivex) error { + for _, ent := range orderedEntries { + node, err := h.Root() + if err != nil { + return err + } + + pathChildren := strings.Split(ent.path, "/") + for _, childPath := range pathChildren { + existingRoot, err := h.NodeGetChild(node, childPath) + if err != nil { + return err + } + + if existingRoot != 0 { + node = existingRoot + continue + } + + node, err = h.NodeAddChild(node, childPath) + if err != nil { + return err + } + } + + if _, err := h.NodeSetValues(node, ent.hiveValues); err != nil { + return err + } + } + + if _, err := h.Commit(); err != nil { + return err + } + return nil +} + +func toRegSz(inStr string) []byte { + utf16Encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + outStr, _ := utf16Encoder.String(inStr) + outBytes := append([]byte(outStr), []byte("\x00\x00")...) + return outBytes +} + +func toRegDword(values ...int) []byte { + var result []byte + for _, val := range values { + result = append([]byte{byte(val)}, result...) + } + return result +} diff --git a/tools/bcdhive_generator/bcdhive_stub.go b/tools/bcdhive_generator/bcdhive_stub.go new file mode 100644 index 00000000..dcae4373 --- /dev/null +++ b/tools/bcdhive_generator/bcdhive_stub.go @@ -0,0 +1,7 @@ +package main + +const encodedBytes = "" + +func StubHiveBCD() ([]byte, error) { + return nil, nil +} diff --git a/tools/bcdhive_generator/bcdhive_test.go b/tools/bcdhive_generator/bcdhive_test.go new file mode 100644 index 00000000..cb277001 --- /dev/null +++ b/tools/bcdhive_generator/bcdhive_test.go @@ -0,0 +1,105 @@ +package main + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "io" + "io/ioutil" + "os" + "os/exec" + "testing" + + "gotest.tools/assert" +) + +// you must `go generate` a valid stub before running tests +//go:generate go run ./ -file=bcdhive_stub.go -package=main -func=StubHiveBCD + +func TestBaseLayerBCDMemoMatchesActual(t *testing.T) { + bcdBytes, err := HiveBCD() + assert.NilError(t, err) + + gzipBuffer := &bytes.Buffer{} + gzipWriter, err := gzip.NewWriterLevel(gzipBuffer, gzip.BestCompression) + assert.NilError(t, err) + + _, err = io.Copy(gzipWriter, bytes.NewBuffer(bcdBytes)) + assert.NilError(t, err) + + assert.NilError(t, gzipWriter.Close()) + + expectedEncodedBCD := base64.StdEncoding.EncodeToString(gzipBuffer.Bytes()) + + assert.Equal(t, encodedBytes, expectedEncodedBCD) +} + +func TestBaseLayerBCDMemo(t *testing.T) { + bcdBytes, err := StubHiveBCD() + assert.NilError(t, err) + + assertIsBCDBaseLayer(t, bcdBytes) +} + +func TestBaseLayerBCDActual(t *testing.T) { + bcdBytes, err := HiveBCD() + assert.NilError(t, err) + + assertIsBCDBaseLayer(t, bcdBytes) +} + +func assertIsBCDBaseLayer(t *testing.T, bcdBytes []byte) { + t.Helper() + + hiveFile, err := ioutil.TempFile("", "") + assert.NilError(t, err) + defer hiveFile.Close() + defer os.Remove(hiveFile.Name()) + + _, err = io.Copy(hiveFile, bytes.NewBuffer(bcdBytes)) + assert.NilError(t, err) + + cmd := exec.Command("hivexregedit", "--export", hiveFile.Name(), "Description") + descriptionRegOutput, err := cmd.CombinedOutput() + assert.NilError(t, err) + + assert.Equal(t, string(descriptionRegOutput), + `Windows Registry Editor Version 5.00 + +[\Description] +"FirmwareModified"=dword:00000001 +"KeyName"=hex(1):42,00,43,00,44,00,30,00,30,00,30,00,30,00,30,00,30,00,30,00,30,00,00,00 + +`) + + cmd = exec.Command("hivexregedit", "--export", hiveFile.Name(), "Objects") + objectsRegOutput, err := cmd.CombinedOutput() + assert.NilError(t, err) + + assert.Equal(t, string(objectsRegOutput), + `Windows Registry Editor Version 5.00 + +[\Objects] + +[\Objects\{6a6c1f1b-59d4-11ea-9438-9402e6abd998}] + +[\Objects\{6a6c1f1b-59d4-11ea-9438-9402e6abd998}\Description] +"Type"=dword:10200003 + +[\Objects\{6a6c1f1b-59d4-11ea-9438-9402e6abd998}\Elements] + +[\Objects\{6a6c1f1b-59d4-11ea-9438-9402e6abd998}\Elements\12000004] +"Element"=hex(1):62,00,75,00,69,00,6c,00,64,00,70,00,61,00,63,00,6b,00,73,00,2e,00,69,00,6f,00,00,00 + +[\Objects\{9dea862c-5cdd-4e70-acc1-f32b344d4795}] + +[\Objects\{9dea862c-5cdd-4e70-acc1-f32b344d4795}\Description] +"Type"=dword:10100002 + +[\Objects\{9dea862c-5cdd-4e70-acc1-f32b344d4795}\Elements] + +[\Objects\{9dea862c-5cdd-4e70-acc1-f32b344d4795}\Elements\23000003] +"Element"=hex(1):7b,00,36,00,61,00,36,00,63,00,31,00,66,00,31,00,62,00,2d,00,35,00,39,00,64,00,34,00,2d,00,31,00,31,00,65,00,61,00,2d,00,39,00,34,00,33,00,38,00,2d,00,39,00,34,00,30,00,32,00,65,00,36,00,61,00,62,00,64,00,39,00,39,00,38,00,7d,00,00,00 + +`) +} diff --git a/tools/bcdhive_generator/go.mod b/tools/bcdhive_generator/go.mod new file mode 100644 index 00000000..ce3c415f --- /dev/null +++ b/tools/bcdhive_generator/go.mod @@ -0,0 +1,11 @@ +module bcdhive_gen + +go 1.15 + +require ( + github.com/gabriel-samfira/go-hivex v0.0.0-20190725123041-b40bc95a7ced + github.com/pkg/errors v0.9.1 + golang.org/x/text v0.3.3 + golang.org/x/tools v0.0.0-20200930213115-e57f6d466a48 + gotest.tools v2.2.0+incompatible +) diff --git a/tools/bcdhive_generator/go.sum b/tools/bcdhive_generator/go.sum new file mode 100644 index 00000000..e6b9a7c7 --- /dev/null +++ b/tools/bcdhive_generator/go.sum @@ -0,0 +1,34 @@ +github.com/gabriel-samfira/go-hivex v0.0.0-20190725123041-b40bc95a7ced h1:QGy2AdPMyJWF1pI/GaAxpEY0qIFn/ekrimYrucQeNNk= +github.com/gabriel-samfira/go-hivex v0.0.0-20190725123041-b40bc95a7ced/go.mod h1:2uhxVfr/8oFRFnCQbpoSzKG+qCvKH3yVt8FPASfJO28= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200930213115-e57f6d466a48 h1:/N7U6gDrclaaWCzrdxtKeyEcUx2goazt5PelznylQNM= +golang.org/x/tools v0.0.0-20200930213115-e57f6d466a48/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/tools/bcdhive_generator/main.go b/tools/bcdhive_generator/main.go new file mode 100644 index 00000000..0bd21932 --- /dev/null +++ b/tools/bcdhive_generator/main.go @@ -0,0 +1,98 @@ +package main + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "flag" + "fmt" + "go/format" + "io" + "io/ioutil" + "log" + "os" + "text/template" +) + +const tmplPath = `bcdhive_encoded.go.tmpl` + +type tmplData struct{ PackageName, FuncName, EncodedBytes string } + +func main() { + outputFilePathPtr := flag.String("file", "", "generated file path") + outputPackageNamePtr := flag.String("package", "", "generated package name") + outputFuncNamePtr := flag.String("func", "", "generated function name") + flag.Parse() + + if err := run(tmplPath, *outputFilePathPtr, *outputPackageNamePtr, *outputFuncNamePtr); err != nil { + log.Fatal(err) + } +} + +func run(tmplPath, outputFilePath, outputPackageName, outputFuncName string) error { + outputFile, err := os.OpenFile(outputFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) + if err != nil { + return err + } + + bcdBytes, err := HiveBCD() + if err != nil { + return err + } + + encodedBCD, err := encodeData(bcdBytes) + if err != nil { + return err + } + + out, err := generateGoSource(tmplPath, outputPackageName, outputFuncName, encodedBCD) + if err != nil { + return err + } + + if _, err := fmt.Fprint(outputFile, out); err != nil { + return err + } + + return nil +} + +func encodeData(rawBytes []byte) (string, error) { + gzipBuffer := &bytes.Buffer{} + gzipWriter, err := gzip.NewWriterLevel(gzipBuffer, gzip.BestCompression) + if err != nil { + return "", err + } + + if _, err := io.Copy(gzipWriter, bytes.NewBuffer(rawBytes)); err != nil { + return "", err + } + + if err := gzipWriter.Close(); err != nil { + return "", err + } + + encoded := base64.StdEncoding.EncodeToString(gzipBuffer.Bytes()) + return encoded, nil +} + +func generateGoSource(tmplPath, packageName, funcName, bcdData string) (string, error) { + tmpl, err := ioutil.ReadFile(tmplPath) + if err != nil { + return "", err + } + + t := template.Must(template.New("tmpl").Parse(string(tmpl))) + + buf := &bytes.Buffer{} + if err := t.Execute(buf, tmplData{packageName, funcName, bcdData}); err != nil { + return "", err + } + + src, err := format.Source(buf.Bytes()) + if err != nil { + return "", err + } + + return string(src), nil +}