github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/flatpak/flatpak_test.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package flatpak_test 16 17 import ( 18 "fmt" 19 "io/fs" 20 "os" 21 "path/filepath" 22 "testing" 23 24 "github.com/google/go-cmp/cmp" 25 "github.com/google/go-cmp/cmp/cmpopts" 26 "github.com/google/osv-scalibr/extractor" 27 "github.com/google/osv-scalibr/extractor/filesystem" 28 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 29 "github.com/google/osv-scalibr/extractor/filesystem/os/flatpak" 30 flatpakmeta "github.com/google/osv-scalibr/extractor/filesystem/os/flatpak/metadata" 31 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 32 scalibrfs "github.com/google/osv-scalibr/fs" 33 "github.com/google/osv-scalibr/inventory" 34 "github.com/google/osv-scalibr/purl" 35 "github.com/google/osv-scalibr/stats" 36 "github.com/google/osv-scalibr/testing/fakefs" 37 "github.com/google/osv-scalibr/testing/testcollector" 38 ) 39 40 func TestFileRequired(t *testing.T) { 41 tests := []struct { 42 name string 43 path string 44 fileSizeBytes int64 45 maxFileSizeBytes int64 46 wantRequired bool 47 wantResultMetric stats.FileRequiredResult 48 }{ 49 { 50 name: "metainfo xml file required if in global flatpak metainfo dir", 51 path: "var/lib/flatpak/app/org.gimp.GIMP/current/export/share/metainfo/org.gimp.GIMP.metainfo.xml", 52 wantRequired: true, 53 wantResultMetric: stats.FileRequiredResultOK, 54 }, 55 { 56 name: "metainfo xml file required if in user local flatpak metainfo dir", 57 path: "home/testuser/.local/share/flatpak/app/org.gimp.GIMP/current/export/share/metainfo/org.gimp.GIMP.metainfo.xml", 58 wantRequired: true, 59 wantResultMetric: stats.FileRequiredResultOK, 60 }, 61 { 62 name: "metainfo xml file required if file size < max file size", 63 path: "var/lib/flatpak/app/org.gimp.GIMP/current/export/share/metainfo/org.gimp.GIMP.metainfo.xml", 64 fileSizeBytes: 100 * units.KiB, 65 maxFileSizeBytes: 1000 * units.KiB, 66 wantRequired: true, 67 wantResultMetric: stats.FileRequiredResultOK, 68 }, 69 { 70 name: "metainfo xml file required if file size == max file size", 71 path: "var/lib/flatpak/app/org.gimp.GIMP/current/export/share/metainfo/org.gimp.GIMP.metainfo.xml", 72 fileSizeBytes: 1000 * units.KiB, 73 maxFileSizeBytes: 1000 * units.KiB, 74 wantRequired: true, 75 wantResultMetric: stats.FileRequiredResultOK, 76 }, 77 { 78 name: "metainfo xml file not required if file size > max file size", 79 path: "var/lib/flatpak/app/org.gimp.GIMP/current/export/share/metainfo/org.gimp.GIMP.metainfo.xml", 80 fileSizeBytes: 1000 * units.KiB, 81 maxFileSizeBytes: 100 * units.KiB, 82 wantRequired: false, 83 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 84 }, 85 { 86 name: "metainfo xml file required if max file size = 0", 87 path: "var/lib/flatpak/app/org.gimp.GIMP/current/export/share/metainfo/org.gimp.GIMP.metainfo.xml", 88 fileSizeBytes: 100 * units.KiB, 89 maxFileSizeBytes: 0, 90 wantRequired: true, 91 wantResultMetric: stats.FileRequiredResultOK, 92 }, 93 { 94 name: "xml file not required if not in flatpak metainfo dir", 95 path: "var/lib/xml-dir/metadata.xml", 96 wantRequired: false, 97 }, 98 { 99 name: "some other file in flatpak metainfo dir not required", 100 path: "var/lib/flatpak/exports/share/metainfo/test.txt", 101 wantRequired: false, 102 }, 103 } 104 105 for _, tt := range tests { 106 // Note the subtest here 107 t.Run(tt.name, func(t *testing.T) { 108 collector := testcollector.New() 109 var e filesystem.Extractor = flatpak.New(flatpak.Config{ 110 Stats: collector, 111 MaxFileSizeBytes: tt.maxFileSizeBytes, 112 }) 113 114 // Set a default file size if not specified. 115 fileSizeBytes := tt.fileSizeBytes 116 if fileSizeBytes == 0 { 117 fileSizeBytes = 1000 118 } 119 120 isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 121 FileName: filepath.Base(tt.path), 122 FileMode: fs.ModePerm, 123 FileSize: fileSizeBytes, 124 })) 125 if isRequired != tt.wantRequired { 126 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) 127 } 128 129 gotResultMetric := collector.FileRequiredResult(tt.path) 130 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 131 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 132 } 133 }) 134 } 135 } 136 137 const DebianBookworm = `PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" 138 NAME="Debian GNU/Linux" 139 VERSION_ID="12" 140 VERSION="12 (bookworm)" 141 VERSION_CODENAME=bookworm 142 ID=debian` 143 144 func TestExtract(t *testing.T) { 145 tests := []struct { 146 name string 147 path string 148 osrelease string 149 cfg flatpak.Config 150 wantPackages []*extractor.Package 151 wantErr error 152 wantResultMetric stats.FileExtractedResult 153 }{ 154 { 155 name: "valid metainfo xml file is extracted", 156 path: "testdata/valid.xml", 157 osrelease: DebianBookworm, 158 wantPackages: []*extractor.Package{ 159 { 160 Name: "org.gimp.GIMP", 161 Version: "2.10.38", 162 PURLType: purl.TypeFlatpak, 163 Metadata: &flatpakmeta.Metadata{ 164 PackageName: "GNU Image Manipulation Program", 165 PackageID: "org.gimp.GIMP", 166 PackageVersion: "2.10.38", 167 ReleaseDate: "2024-05-02", 168 OSID: "debian", 169 OSVersionID: "12", 170 OSName: "Debian GNU/Linux", 171 Developer: "The GIMP team", 172 }, 173 Locations: []string{"testdata/valid.xml"}, 174 }, 175 }, 176 wantResultMetric: stats.FileExtractedResultSuccess, 177 }, 178 { 179 name: "metainfo xml file without package name is extracted", 180 path: "testdata/noname.xml", 181 osrelease: DebianBookworm, 182 wantPackages: []*extractor.Package{ 183 { 184 Name: "org.gimp.GIMP", 185 Version: "2.10.38", 186 PURLType: purl.TypeFlatpak, 187 Metadata: &flatpakmeta.Metadata{ 188 PackageName: "", 189 PackageID: "org.gimp.GIMP", 190 PackageVersion: "2.10.38", 191 ReleaseDate: "2024-05-02", 192 OSID: "debian", 193 OSVersionID: "12", 194 OSName: "Debian GNU/Linux", 195 Developer: "The GIMP team", 196 }, 197 Locations: []string{"testdata/noname.xml"}, 198 }, 199 }, 200 wantResultMetric: stats.FileExtractedResultSuccess, 201 }, 202 { 203 name: "metainfo xml file without package version is skipped", 204 path: "testdata/noversion.xml", 205 osrelease: DebianBookworm, 206 wantErr: cmpopts.AnyError, 207 wantResultMetric: stats.FileExtractedResultErrorUnknown, 208 }, 209 { 210 name: "malformed metainfo xml file is skipped", 211 path: "testdata/bad.xml", 212 osrelease: DebianBookworm, 213 wantErr: cmpopts.AnyError, 214 wantResultMetric: stats.FileExtractedResultErrorUnknown, 215 }, 216 } 217 218 for _, tt := range tests { 219 // Note the subtest here 220 t.Run(tt.name, func(t *testing.T) { 221 collector := testcollector.New() 222 tt.cfg.Stats = collector 223 224 d := t.TempDir() 225 createOsRelease(t, d, tt.osrelease) 226 227 r, err := os.Open(tt.path) 228 defer func() { 229 if err = r.Close(); err != nil { 230 t.Errorf("Close(): %v", err) 231 } 232 }() 233 if err != nil { 234 t.Fatal(err) 235 } 236 237 info, err := os.Stat(tt.path) 238 if err != nil { 239 t.Fatalf("Failed to stat test file: %v", err) 240 } 241 242 input := &filesystem.ScanInput{FS: scalibrfs.DirFS(d), Path: tt.path, Reader: r, Root: d, Info: info} 243 244 e := flatpak.New(defaultConfigWith(tt.cfg)) 245 got, err := e.Extract(t.Context(), input) 246 if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) { 247 t.Fatalf("Extract(%+v) error: got %v, want %v\n", tt.path, err, tt.wantErr) 248 } 249 250 ignoreOrder := cmpopts.SortSlices(func(a, b any) bool { 251 return fmt.Sprintf("%+v", a) < fmt.Sprintf("%+v", b) 252 }) 253 wantInv := inventory.Inventory{Packages: tt.wantPackages} 254 if diff := cmp.Diff(wantInv, got, ignoreOrder); diff != "" { 255 t.Errorf("Extract(%s) (-want +got):\n%s", tt.path, diff) 256 } 257 258 gotResultMetric := collector.FileExtractedResult(tt.path) 259 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 260 t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 261 } 262 263 gotFileSizeMetric := collector.FileExtractedFileSize(tt.path) 264 if gotFileSizeMetric != info.Size() { 265 t.Errorf("Extract(%s) recorded file size %v, want file size %v", tt.path, gotFileSizeMetric, info.Size()) 266 } 267 }) 268 } 269 } 270 271 func createOsRelease(t *testing.T, root string, content string) { 272 t.Helper() 273 _ = os.MkdirAll(filepath.Join(root, "etc"), 0755) 274 err := os.WriteFile(filepath.Join(root, "etc/os-release"), []byte(content), 0644) 275 if err != nil { 276 t.Fatalf("write to %s: %v\n", filepath.Join(root, "etc/os-release"), err) 277 } 278 } 279 280 // defaultConfigWith combines any non-zero fields of cfg with packagejson.DefaultConfig(). 281 func defaultConfigWith(cfg flatpak.Config) flatpak.Config { 282 newCfg := flatpak.DefaultConfig() 283 284 if cfg.Stats != nil { 285 newCfg.Stats = cfg.Stats 286 } 287 288 if cfg.MaxFileSizeBytes > 0 { 289 newCfg.MaxFileSizeBytes = cfg.MaxFileSizeBytes 290 } 291 292 return newCfg 293 }