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  }