diff --git a/differs/apk_diff.go b/differs/apk_diff.go new file mode 100644 index 00000000..cd1b4b33 --- /dev/null +++ b/differs/apk_diff.go @@ -0,0 +1,167 @@ +/* +Copyright 2018 Google, Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package differs + +import ( + "bufio" + "os" + "path/filepath" + "strconv" + "strings" + + pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" + "github.com/GoogleContainerTools/container-diff/util" + "github.com/sirupsen/logrus" +) + +// APK package database location +const apkInstalledDbFile string = "lib/apk/db/installed" + +type ApkAnalyzer struct { +} + +func (a ApkAnalyzer) Name() string { + return "ApkAnalyzer" +} + +// ApkDiff compares the packages installed by apt-get. +func (a ApkAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { + diff, err := singleVersionDiff(image1, image2, a) + return diff, err +} + +func (a ApkAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { + analysis, err := singleVersionAnalysis(image, a) + return analysis, err +} + +func (a ApkAnalyzer) getPackages(image pkgutil.Image) (map[string]util.PackageInfo, error) { + return apkReadInstalledDbFile(image.FSPath) +} + +type ApkLayerAnalyzer struct { +} + +func (a ApkLayerAnalyzer) Name() string { + return "ApkLayerAnalyzer" +} + +// ApkDiff compares the packages installed by apt-get. +func (a ApkLayerAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { + diff, err := singleVersionLayerDiff(image1, image2, a) + return diff, err +} + +func (a ApkLayerAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { + analysis, err := singleVersionLayerAnalysis(image, a) + return analysis, err +} + +func (a ApkLayerAnalyzer) getPackages(image pkgutil.Image) ([]map[string]util.PackageInfo, error) { + var packages []map[string]util.PackageInfo + if _, err := os.Stat(image.FSPath); err != nil { + // invalid image directory path + return packages, err + } + installedDbFile := filepath.Join(image.FSPath, apkInstalledDbFile) + if _, err := os.Stat(installedDbFile); err != nil { + // installed DB file does not exist in this image + return packages, nil + } + for _, layer := range image.Layers { + layerPackages, err := apkReadInstalledDbFile(layer.FSPath) + if err != nil { + return packages, err + } + packages = append(packages, layerPackages) + } + + return packages, nil +} + +func apkReadInstalledDbFile(root string) (map[string]util.PackageInfo, error) { + packages := make(map[string]util.PackageInfo) + if _, err := os.Stat(root); err != nil { + // invalid image directory path + return packages, err + } + installedDbFile := filepath.Join(root, apkInstalledDbFile) + if _, err := os.Stat(installedDbFile); err != nil { + // installed DB file does not exist in this layer + return packages, nil + } + if file, err := os.Open(installedDbFile); err == nil { + // make sure it gets closed + defer file.Close() + + // create a new scanner and read the file line by line + scanner := bufio.NewScanner(file) + var currPackage string + for scanner.Scan() { + currPackage = apkParseLine(scanner.Text(), currPackage, packages) + } + } else { + return packages, err + } + + return packages, nil +} + +func apkParseLine(text string, currPackage string, packages map[string]util.PackageInfo) string { + line := strings.Split(text, ":") + if len(line) == 2 { + key := line[0] + value := line[1] + + switch key { + case "P": + return value + case "V": + if packages[currPackage].Version != "" { + logrus.Warningln("Multiple versions of same package detected. Diffing such multi-versioning not yet supported.") + return currPackage + } + currPackageInfo, ok := packages[currPackage] + if !ok { + currPackageInfo = util.PackageInfo{} + } + currPackageInfo.Version = value + packages[currPackage] = currPackageInfo + return currPackage + + case "I": + currPackageInfo, ok := packages[currPackage] + if !ok { + currPackageInfo = util.PackageInfo{} + } + var size int64 + var err error + size, err = strconv.ParseInt(value, 10, 64) + if err != nil { + logrus.Errorf("Could not get size for %s: %s", currPackage, err) + size = -1 + } + // I is in bytes, so *no* conversion needed to keep consistent with the tool's size units + currPackageInfo.Size = size + packages[currPackage] = currPackageInfo + return currPackage + default: + return currPackage + } + } + return currPackage +} diff --git a/differs/apk_diff_test.go b/differs/apk_diff_test.go new file mode 100644 index 00000000..198bd724 --- /dev/null +++ b/differs/apk_diff_test.go @@ -0,0 +1,128 @@ +/* +Copyright 2018 Google, Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package differs + +import ( + "reflect" + "testing" + + pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" + "github.com/GoogleContainerTools/container-diff/util" +) + +func TestApkParseLine(t *testing.T) { + testCases := []struct { + description string + line string + packages map[string]util.PackageInfo + currPackage string + expPackage string + expected map[string]util.PackageInfo + }{ + { + description: "Not applicable line", + line: "G:garbage info", + packages: map[string]util.PackageInfo{}, + expPackage: "", + expected: map[string]util.PackageInfo{}, + }, + { + description: "Package line", + line: "P:musl", + currPackage: "foo", + expPackage: "musl", + packages: map[string]util.PackageInfo{}, + expected: map[string]util.PackageInfo{}, + }, + { + description: "Version line", + line: "V:1.2.2-r1", + packages: map[string]util.PackageInfo{}, + currPackage: "musl", + expPackage: "musl", + expected: map[string]util.PackageInfo{"musl": {Version: "1.2.2-r1"}}, + }, + { + description: "Size line", + line: "I:622592", + packages: map[string]util.PackageInfo{}, + currPackage: "musl", + expPackage: "musl", + expected: map[string]util.PackageInfo{"musl": {Size: 622592}}, + }, + { + description: "Pre-existing PackageInfo struct", + line: "I:622592", + packages: map[string]util.PackageInfo{"musl": {Version: "1.2.2-r1"}}, + currPackage: "musl", + expPackage: "musl", + expected: map[string]util.PackageInfo{"musl": {Version: "1.2.2-r1", Size: 622592}}, + }, + } + + for _, test := range testCases { + currPackage := apkParseLine(test.line, test.currPackage, test.packages) + if currPackage != test.expPackage { + t.Errorf("Expected current package to be: %s, but got: %s.", test.expPackage, currPackage) + } + if !reflect.DeepEqual(test.packages, test.expected) { + t.Errorf("Expected: %v but got: %v", test.expected, test.packages) + } + } +} + +func TestGetApkPackages(t *testing.T) { + testCases := []struct { + description string + path string + expected map[string]util.PackageInfo + err bool + }{ + { + description: "no directory", + path: "testDirs/notThere", + expected: map[string]util.PackageInfo{}, + err: true, + }, + { + description: "no packages", + path: "testDirs/noPackages", + expected: map[string]util.PackageInfo{}, + }, + { + description: "packages in expected location", + path: "testDirs/packageOne", + expected: map[string]util.PackageInfo{ + "musl": {Version: "1.2.2-r1", Size: 622592}, + "busybox": {Version: "1.32.1-r7", Size: 946176}}, + }, + } + for _, test := range testCases { + d := ApkAnalyzer{} + image := pkgutil.Image{FSPath: test.path} + packages, err := d.getPackages(image) + if err != nil && !test.err { + t.Errorf("Got unexpected error: %s", err) + } + if err == nil && test.err { + t.Errorf("Expected error but got none.") + } + if !reflect.DeepEqual(packages, test.expected) { + t.Errorf("Expected: %v but got: %v", test.expected, packages) + } + } +} diff --git a/differs/differs.go b/differs/differs.go index 6439168c..4dea581e 100644 --- a/differs/differs.go +++ b/differs/differs.go @@ -30,6 +30,8 @@ const fileAnalyzer = "file" const layerAnalyzer = "layer" const sizeAnalyzer = "size" const sizeLayerAnalyzer = "sizelayer" +const apkAnalyzer = "apk" +const apkLayerAnalyzer = "apklayer" const aptAnalyzer = "apt" const aptLayerAnalyzer = "aptlayer" const rpmAnalyzer = "rpm" @@ -62,6 +64,8 @@ var Analyzers = map[string]Analyzer{ layerAnalyzer: FileLayerAnalyzer{}, sizeAnalyzer: SizeAnalyzer{}, sizeLayerAnalyzer: SizeLayerAnalyzer{}, + apkAnalyzer: ApkAnalyzer{}, + apkLayerAnalyzer: ApkLayerAnalyzer{}, aptAnalyzer: AptAnalyzer{}, aptLayerAnalyzer: AptLayerAnalyzer{}, rpmAnalyzer: RPMAnalyzer{}, diff --git a/differs/testDirs/noPackages/lib/apk/db/installed b/differs/testDirs/noPackages/lib/apk/db/installed new file mode 100644 index 00000000..e69de29b diff --git a/differs/testDirs/packageOne/lib/apk/db/installed b/differs/testDirs/packageOne/lib/apk/db/installed new file mode 100644 index 00000000..dd0d55e7 --- /dev/null +++ b/differs/testDirs/packageOne/lib/apk/db/installed @@ -0,0 +1,78 @@ +C:Q1PLxH/xkzg+5Ny+Sid1STPEHi8e8= +P:musl +V:1.2.2-r1 +A:x86_64 +S:382799 +I:622592 +T:the musl c library (libc) implementation +U:https://musl.libc.org/ +L:MIT +o:musl +m:Timo Teräs +t:1623058830 +c:bbbf656a5c008cd72af26290e7933a515af7f11f +p:so:libc.musl-x86_64.so.1=1 +F:lib +R:ld-musl-x86_64.so.1 +a:0:0:755 +Z:Q1YVYdCRbKnapM6sWXMBeIM5V6azc= +R:libc.musl-x86_64.so.1 +a:0:0:777 +Z:Q17yJ3JFNypA4mxhJJr0ou6CzsJVI= + +C:Q1URbAAbfw05cknLVZMabtNfnjbtA= +P:busybox +V:1.32.1-r7 +A:x86_64 +S:497882 +I:946176 +T:Size optimized toolbox of many common UNIX utilities +U:https://busybox.net/ +L:GPL-2.0-only +o:busybox +m:Natanael Copa +t:1636715120 +c:c434d5ff440c3e8bea3dbe139875e98a8c112202 +D:so:libc.musl-x86_64.so.1 +p:/bin/sh cmd:busybox cmd:sh +r:busybox-initscripts +F:bin +R:busybox +a:0:0:755 +Z:Q1PtQZENNCmILe4Zi742XWtTRj+iw= +R:sh +a:0:0:777 +Z:Q1pcfTfDNEbNKQc2s1tia7da05M8Q= +F:etc +R:securetty +Z:Q1mB95Hq2NUTZ599RDiSsj9w5FrOU= +R:udhcpd.conf +Z:Q1UAiPZcDIW1ClRzobfggcCQ77V28= +F:etc/logrotate.d +R:acpid +Z:Q1TylyCINVmnS+A/Tead4vZhE7Bks= +F:etc/network +F:etc/network/if-down.d +F:etc/network/if-post-down.d +F:etc/network/if-post-up.d +F:etc/network/if-pre-down.d +F:etc/network/if-pre-up.d +F:etc/network/if-up.d +R:dad +a:0:0:775 +Z:Q1hlxd3qExrihH8bYxDQ3i7TsM/44= +F:sbin +F:tmp +M:0:0:1777 +F:usr +F:usr/sbin +F:usr/share +F:usr/share/udhcpc +R:default.script +a:0:0:755 +Z:Q1t9vir/ZrX3nbSIYT9BDLWZenkVQ= +F:var +F:var/cache +F:var/cache/misc +F:var/lib +F:var/lib/udhcpd