github.com/google/osv-scalibr@v0.4.1/common/linux/dpkg/dpkg.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 dpkg provides utilities for interacting with the dpkg package manager database.
    16  package dpkg
    17  
    18  import (
    19  	"bufio"
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"io/fs"
    25  	"path"
    26  
    27  	scalibrfs "github.com/google/osv-scalibr/fs"
    28  	"github.com/google/osv-scalibr/fs/diriterate"
    29  )
    30  
    31  const (
    32  	infoDirPath = "var/lib/dpkg/info"
    33  )
    34  
    35  // filePathIterator is an iterator over all paths found in dpkg files with a specific extension.
    36  type filePathIterator struct {
    37  	rootFs            scalibrfs.FS
    38  	dirs              *diriterate.DirIterator
    39  	currentFileReader io.ReadCloser
    40  	currentScanner    *bufio.Scanner
    41  	fileExt           string
    42  }
    43  
    44  func newFilePathIterator(rootFs scalibrfs.FS, fileExt string) (*filePathIterator, error) {
    45  	if _, err := rootFs.Stat(infoDirPath); err != nil {
    46  		if errors.Is(err, fs.ErrNotExist) {
    47  			// If info dir doesn't exist, dpkg is not installed or has no package info.
    48  			// Return an iterator that doesn't iterate over any files.
    49  			return &filePathIterator{rootFs: rootFs, fileExt: fileExt}, nil
    50  		}
    51  		return nil, err
    52  	}
    53  	dirs, err := diriterate.ReadDir(rootFs, infoDirPath)
    54  	if err != nil {
    55  		return nil, fmt.Errorf("failed to read dpkg info dir at %s: %w", infoDirPath, err)
    56  	}
    57  	return &filePathIterator{rootFs: rootFs, dirs: dirs, fileExt: fileExt}, nil
    58  }
    59  
    60  // Next returns the path of the next installed file.
    61  // It returns io.EOF when there are no more files.
    62  func (it *filePathIterator) Next(ctx context.Context) (string, error) {
    63  	if err := ctx.Err(); err != nil {
    64  		return "", fmt.Errorf("dpkg.filePathIterator.Next halted because of context error: %w", err)
    65  	}
    66  
    67  	for {
    68  		if it.currentScanner != nil && it.currentScanner.Scan() {
    69  			return it.currentScanner.Text(), nil
    70  		}
    71  		if it.currentFileReader != nil {
    72  			it.currentFileReader.Close()
    73  			it.currentFileReader = nil
    74  			it.currentScanner = nil
    75  		}
    76  
    77  		listPath, err := it.nextFileWithExt()
    78  		if err != nil {
    79  			return "", err
    80  		}
    81  
    82  		reader, err := it.rootFs.Open(listPath)
    83  		if err != nil {
    84  			return "", err
    85  		}
    86  		it.currentFileReader = reader
    87  		it.currentScanner = bufio.NewScanner(reader)
    88  	}
    89  }
    90  
    91  func (it *filePathIterator) nextFileWithExt() (string, error) {
    92  	if it.dirs == nil {
    93  		return "", io.EOF
    94  	}
    95  	for {
    96  		f, err := it.dirs.Next()
    97  		if err != nil {
    98  			return "", err
    99  		}
   100  
   101  		if !f.IsDir() && path.Ext(f.Name()) == it.fileExt {
   102  			return path.Join(infoDirPath, f.Name()), nil
   103  		}
   104  	}
   105  }
   106  
   107  // Close closes the iterator and releases any resources.
   108  func (it *filePathIterator) Close() error {
   109  	var errs []error
   110  	if it.currentFileReader != nil {
   111  		if err := it.currentFileReader.Close(); err != nil {
   112  			errs = append(errs, err)
   113  		}
   114  	}
   115  	if it.dirs != nil {
   116  		if err := it.dirs.Close(); err != nil {
   117  			errs = append(errs, err)
   118  		}
   119  	}
   120  	return errors.Join(errs...)
   121  }
   122  
   123  // ListFilePathIterator is an iterator over all paths found in dpkg .list files.
   124  type ListFilePathIterator struct {
   125  	*filePathIterator
   126  }
   127  
   128  // NewListFilePathIterator creates a new iterator over files installed by dpkg.
   129  // The caller is responsible for calling Close() on the returned iterator.
   130  func NewListFilePathIterator(rootFs scalibrfs.FS) (*ListFilePathIterator, error) {
   131  	it, err := newFilePathIterator(rootFs, ".list")
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	return &ListFilePathIterator{it}, nil
   136  }
   137  
   138  // ConffilePathIterator is an iterator over all paths found in dpkg .conffiles files.
   139  type ConffilePathIterator struct {
   140  	*filePathIterator
   141  }
   142  
   143  // NewConffilePathIterator creates a new iterator over conffiles managed by dpkg.
   144  // The caller is responsible for calling Close() on the returned iterator.
   145  func NewConffilePathIterator(rootFs scalibrfs.FS) (*ConffilePathIterator, error) {
   146  	it, err := newFilePathIterator(rootFs, ".conffiles")
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	return &ConffilePathIterator{it}, nil
   151  }