From 98a0ec950f3586eb2b0e6cfe3d10b046e5cc7e4f Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 30 Jun 2026 10:59:55 +0200 Subject: [PATCH] refactor: move tarball extract to dedicated pkg --- cmd/chisel/main.go | 2 + internal/archive/archive_test.go | 6 +- internal/deb/extract.go | 391 --------------------- internal/slicer/slicer.go | 14 +- internal/tarball/extract.go | 398 ++++++++++++++++++++++ internal/{deb => tarball}/extract_test.go | 246 ++++++------- internal/tarball/log.go | 53 +++ internal/tarball/suite_test.go | 20 ++ 8 files changed, 606 insertions(+), 524 deletions(-) create mode 100644 internal/tarball/extract.go rename internal/{deb => tarball}/extract_test.go (71%) create mode 100644 internal/tarball/log.go create mode 100644 internal/tarball/suite_test.go diff --git a/cmd/chisel/main.go b/cmd/chisel/main.go index a982e3ef3..0450e901c 100644 --- a/cmd/chisel/main.go +++ b/cmd/chisel/main.go @@ -17,6 +17,7 @@ import ( "github.com/canonical/chisel/internal/setup" "github.com/canonical/chisel/internal/slicer" "github.com/canonical/chisel/internal/store" + "github.com/canonical/chisel/internal/tarball" //"github.com/canonical/chisel/internal/logger" ) @@ -329,6 +330,7 @@ func run() error { setup.SetLogger(log.Default()) slicer.SetLogger(log.Default()) store.SetLogger(log.Default()) + tarball.SetLogger(log.Default()) SetLogger(log.Default()) parser := Parser() diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 8ca840e59..c21e48161 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -18,7 +18,7 @@ import ( "github.com/canonical/chisel/internal/archive" "github.com/canonical/chisel/internal/archive/testarchive" - "github.com/canonical/chisel/internal/deb" + "github.com/canonical/chisel/internal/tarball" "github.com/canonical/chisel/internal/testutil" ) @@ -856,10 +856,10 @@ func (s *S) testOpenArchiveArch(c *C, test realArchiveTest, arch string) { c.Assert(info.Name, DeepEquals, test.pkg) c.Assert(info.Arch, DeepEquals, arch) - err = deb.Extract(pkg, &deb.ExtractOptions{ + err = tarball.Extract(pkg, &tarball.ExtractOptions{ Package: test.pkg, TargetDir: extractDir, - Extract: map[string][]deb.ExtractInfo{ + Extract: map[string][]tarball.ExtractInfo{ fmt.Sprintf("/usr/share/doc/%s/copyright", test.pkg): { {Path: "/copyright"}, }, diff --git a/internal/deb/extract.go b/internal/deb/extract.go index 67bee3f3d..bf543c2d8 100644 --- a/internal/deb/extract.go +++ b/internal/deb/extract.go @@ -1,384 +1,15 @@ package deb import ( - "archive/tar" - "bytes" "compress/gzip" "fmt" "io" - "io/fs" - "os" - "path/filepath" - "sort" - "strings" - "syscall" "github.com/blakesmith/ar" "github.com/klauspost/compress/zstd" "github.com/ulikunitz/xz" - - "github.com/canonical/chisel/internal/fsutil" - "github.com/canonical/chisel/internal/strdist" ) -type ExtractOptions struct { - Package string - TargetDir string - Extract map[string][]ExtractInfo - // Create can optionally be set to control the creation of extracted entries. - // extractInfos is set to the matching entries in Extract, and is nil in cases where - // the created entry is implicit and unlisted (for example, parent directories). - Create func(extractInfos []ExtractInfo, options *fsutil.CreateOptions) error -} - -type ExtractInfo struct { - Path string - Mode uint - Optional bool - Context any -} - -func getValidOptions(options *ExtractOptions) (*ExtractOptions, error) { - for extractPath, extractInfos := range options.Extract { - isGlob := strings.ContainsAny(extractPath, "*?") - if isGlob { - for _, extractInfo := range extractInfos { - if extractInfo.Path != extractPath || extractInfo.Mode != 0 { - return nil, fmt.Errorf("when using wildcards source and target paths must match: %s", extractPath) - } - } - } - } - - if options.Create == nil { - validOpts := *options - validOpts.Create = func(_ []ExtractInfo, o *fsutil.CreateOptions) error { - _, err := fsutil.Create(o) - return err - } - return &validOpts, nil - } - - return options, nil -} - -func Extract(pkgReader io.ReadSeeker, options *ExtractOptions) (err error) { - defer func() { - if err != nil { - err = fmt.Errorf("cannot extract from package %q: %w", options.Package, err) - } - }() - - logf("Extracting files from package %q...", options.Package) - - validOpts, err := getValidOptions(options) - if err != nil { - return err - } - - _, err = os.Stat(validOpts.TargetDir) - if os.IsNotExist(err) { - return fmt.Errorf("target directory does not exist") - } else if err != nil { - return err - } - - return extractData(pkgReader, validOpts) -} - -func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error { - dataReader, err := DataReader(pkgReader) - if err != nil { - return err - } - defer dataReader.Close() - - oldUmask := syscall.Umask(0) - defer func() { - syscall.Umask(oldUmask) - }() - - pendingPaths := make(map[string]bool) - for extractPath, extractInfos := range options.Extract { - for _, extractInfo := range extractInfos { - if !extractInfo.Optional { - pendingPaths[extractPath] = true - break - } - } - } - - // Store the hard links that we cannot extract when we first iterate over - // the tarball. - // - // This happens because the tarball only stores the contents once in the - // first entry and the rest of them point to the first one. Therefore, we - // cannot tell whether we need to extract the content until after we get to - // a hard link. In this case, we need a second pass. - pendingHardLinks := make(map[string][]pendingHardLink) - - // When creating a file we will iterate through its parent directories and - // create them with the permissions defined in the tarball. - // - // The assumption is that the tar entries of the parent directories appear - // before the entry for the file itself. This is the case for .deb files but - // not for all tarballs. - tarDirMode := make(map[string]fs.FileMode) - tarReader := tar.NewReader(dataReader) - for { - tarHeader, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - - sourcePath, ok := sanitizeTarPath(tarHeader.Name) - if !ok { - continue - } - - sourceIsDir := sourcePath[len(sourcePath)-1] == '/' - if sourceIsDir { - tarDirMode[sourcePath] = tarHeader.FileInfo().Mode() - } - - // Find all globs and copies that require this source, and map them by - // their target paths on disk. - targetPaths := map[string][]ExtractInfo{} - for extractPath, extractInfos := range options.Extract { - if extractPath == "" { - continue - } - if strings.ContainsAny(extractPath, "*?") { - if strdist.GlobPath(extractPath, sourcePath) { - targetPaths[sourcePath] = append(targetPaths[sourcePath], extractInfos...) - delete(pendingPaths, extractPath) - } - } else if extractPath == sourcePath { - for _, extractInfo := range extractInfos { - targetPaths[extractInfo.Path] = append(targetPaths[extractInfo.Path], extractInfo) - } - delete(pendingPaths, extractPath) - } - } - if len(targetPaths) == 0 { - // Nothing to do. - continue - } - - var contentCache []byte - var contentIsCached = len(targetPaths) > 1 && !sourceIsDir - if contentIsCached { - // Read and cache the content so it may be reused. - // As an alternative, to avoid having an entire file in - // memory at once this logic might open the first file - // written and copy it every time. For now, the choice - // is speed over memory efficiency. - data, err := io.ReadAll(tarReader) - if err != nil { - return err - } - contentCache = data - } - - var pathReader io.Reader = tarReader - for targetPath, extractInfos := range targetPaths { - if contentIsCached { - pathReader = bytes.NewReader(contentCache) - } - mode := extractInfos[0].Mode - for _, extractInfo := range extractInfos { - if extractInfo.Mode != mode { - if mode < extractInfo.Mode { - mode, extractInfo.Mode = extractInfo.Mode, mode - } - return fmt.Errorf("path %s requested twice with diverging mode: 0%03o != 0%03o", targetPath, mode, extractInfo.Mode) - } - } - if mode != 0 { - tarHeader.Mode = int64(mode) - } - // Create the parent directories using the permissions from the tarball. - parents := parentDirs(targetPath) - for _, path := range parents { - if path == "/" { - continue - } - mode, ok := tarDirMode[path] - if !ok { - continue - } - delete(tarDirMode, path) - - createOptions := &fsutil.CreateOptions{ - Root: options.TargetDir, - Path: path, - Mode: mode, - MakeParents: true, - } - err := options.Create(nil, createOptions) - if err != nil { - return err - } - } - link := tarHeader.Linkname - if tarHeader.Typeflag == tar.TypeLink { - // A hard link requires the real path of the target file. - link = filepath.Join(options.TargetDir, link) - } - - // Create the entry itself. - createOptions := &fsutil.CreateOptions{ - Root: options.TargetDir, - Path: targetPath, - Mode: tarHeader.FileInfo().Mode(), - Data: pathReader, - Link: link, - MakeParents: true, - OverrideMode: true, - } - err := options.Create(extractInfos, createOptions) - if err != nil && os.IsNotExist(err) && tarHeader.Typeflag == tar.TypeLink { - // The hard link could not be created because the content - // was not extracted previously. Add this hard link entry - // to the pending list to extract later. - relLinkPath, ok := sanitizeTarPath(tarHeader.Linkname) - if !ok { - return fmt.Errorf("invalid link target %s", tarHeader.Linkname) - } - info := pendingHardLink{ - path: targetPath, - extractInfos: extractInfos, - } - pendingHardLinks[relLinkPath] = append(pendingHardLinks[relLinkPath], info) - } else if err != nil { - return err - } - } - } - - if len(pendingHardLinks) > 0 { - // Go over the tarball again to textract the pending hard links. - extractHardLinkOptions := &extractHardLinkOptions{ - ExtractOptions: options, - pendingLinks: pendingHardLinks, - } - _, err := pkgReader.Seek(0, io.SeekStart) - if err != nil { - return err - } - err = extractHardLinks(pkgReader, extractHardLinkOptions) - if err != nil { - return err - } - } - - if len(pendingPaths) > 0 { - pendingList := make([]string, 0, len(pendingPaths)) - for pendingPath := range pendingPaths { - pendingList = append(pendingList, pendingPath) - } - if len(pendingList) == 1 { - return fmt.Errorf("no content at %s", pendingList[0]) - } else { - sort.Strings(pendingList) - return fmt.Errorf("no content at:\n- %s", strings.Join(pendingList, "\n- ")) - } - } - - return nil -} - -type pendingHardLink struct { - path string - extractInfos []ExtractInfo -} - -type extractHardLinkOptions struct { - *ExtractOptions - pendingLinks map[string][]pendingHardLink -} - -// extractHardLinks iterates through the tarball a second time to extract the -// hard links that were not extracted in the first pass. -func extractHardLinks(pkgReader io.ReadSeeker, opts *extractHardLinkOptions) error { - dataReader, err := DataReader(pkgReader) - if err != nil { - return err - } - defer dataReader.Close() - - tarReader := tar.NewReader(dataReader) - for { - tarHeader, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - - sourcePath, ok := sanitizeTarPath(tarHeader.Name) - if !ok { - continue - } - - links := opts.pendingLinks[sourcePath] - if len(links) == 0 { - continue - } - - // For a target path, the first hard link will be created as a file with - // the content of the target path. If there are more pending hard links, - // the remaining ones will be created as hard links with the newly - // created file as their target. - absLink := filepath.Join(opts.TargetDir, links[0].path) - // Extract the content to the first hard link path. - createOptions := &fsutil.CreateOptions{ - Root: opts.TargetDir, - Path: links[0].path, - Mode: tarHeader.FileInfo().Mode(), - Data: tarReader, - } - err = opts.Create(links[0].extractInfos, createOptions) - if err != nil { - return err - } - - // Create the remaining hard links. - for _, link := range links[1:] { - createOptions := &fsutil.CreateOptions{ - Root: opts.TargetDir, - Path: link.path, - Mode: tarHeader.FileInfo().Mode(), - // Link to the first file extracted for the hard links. - Link: absLink, - } - err := opts.Create(link.extractInfos, createOptions) - if err != nil { - return err - } - } - delete(opts.pendingLinks, sourcePath) - } - - // If there are pending links, that means the link targets do not come from - // this package. - if len(opts.pendingLinks) > 0 { - var targets []string - for target := range opts.pendingLinks { - targets = append(targets, target) - } - sort.Strings(targets) - link := opts.pendingLinks[targets[0]][0] - return fmt.Errorf("cannot create hard link %s: no content at %s", link.path, targets[0]) - } - - return nil -} - // DataReader takes a Reader for the ar file belonging to a Debian package and // returns a Reader to the inner tarball. func DataReader(pkgReader io.ReadSeeker) (io.ReadCloser, error) { @@ -416,25 +47,3 @@ func DataReader(pkgReader io.ReadSeeker) (io.ReadCloser, error) { return dataReader, nil } - -func parentDirs(path string) []string { - path = filepath.Clean(path) - parents := make([]string, strings.Count(path, "/")) - count := 0 - for i, c := range path { - if c == '/' { - parents[count] = path[:i+1] - count++ - } - } - return parents -} - -// sanitizeTarPath removes the leading "./" from the source path in the tarball, -// and verifies that the path is not empty. -func sanitizeTarPath(path string) (string, bool) { - if len(path) < 3 || path[0] != '.' || path[1] != '/' { - return "", false - } - return path[1:], true -} diff --git a/internal/slicer/slicer.go b/internal/slicer/slicer.go index a0aae7519..b62266795 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -16,12 +16,12 @@ import ( "github.com/klauspost/compress/zstd" "github.com/canonical/chisel/internal/archive" - "github.com/canonical/chisel/internal/deb" "github.com/canonical/chisel/internal/fsutil" "github.com/canonical/chisel/internal/manifestutil" "github.com/canonical/chisel/internal/scripts" "github.com/canonical/chisel/internal/setup" "github.com/canonical/chisel/internal/store" + "github.com/canonical/chisel/internal/tarball" ) const manifestMode fs.FileMode = 0644 @@ -120,11 +120,11 @@ func Run(options *RunOptions) error { } // Build information to process the selection. - extract := make(map[string]map[string][]deb.ExtractInfo) + extract := make(map[string]map[string][]tarball.ExtractInfo) for _, slice := range options.Selection.Slices { extractPackage := extract[slice.Package] if extractPackage == nil { - extractPackage = make(map[string][]deb.ExtractInfo) + extractPackage = make(map[string][]tarball.ExtractInfo) extract[slice.Package] = extractPackage } src := pkgSources[slice.Package] @@ -144,7 +144,7 @@ func Run(options *RunOptions) error { if sourcePath == "" { sourcePath = targetPath } - extractPackage[sourcePath] = append(extractPackage[sourcePath], deb.ExtractInfo{ + extractPackage[sourcePath] = append(extractPackage[sourcePath], tarball.ExtractInfo{ Path: targetPath, Context: slice, }) @@ -156,7 +156,7 @@ func Run(options *RunOptions) error { if targetDir == "" || targetDir == "/" { continue } - extractPackage[targetDir] = append(extractPackage[targetDir], deb.ExtractInfo{ + extractPackage[targetDir] = append(extractPackage[targetDir], tarball.ExtractInfo{ Path: targetDir, Optional: true, }) @@ -211,7 +211,7 @@ func Run(options *RunOptions) error { var implicitConflicts []string // Creates the filesystem entry and adds it to the report. It also updates // knownPaths with the files created. - create := func(extractInfos []deb.ExtractInfo, o *fsutil.CreateOptions) error { + create := func(extractInfos []tarball.ExtractInfo, o *fsutil.CreateOptions) error { entry, err := fsutil.Create(o) if err != nil { return err @@ -278,7 +278,7 @@ func Run(options *RunOptions) error { if src.kind != sourceArchive { return fmt.Errorf("cannot extract package %q from store: store packages are not yet supported", src.pkg.RealName) } - err := deb.Extract(reader, &deb.ExtractOptions{ + err := tarball.Extract(reader, &tarball.ExtractOptions{ Package: slice.Package, Extract: extract[slice.Package], TargetDir: targetDir, diff --git a/internal/tarball/extract.go b/internal/tarball/extract.go new file mode 100644 index 000000000..40db2cbac --- /dev/null +++ b/internal/tarball/extract.go @@ -0,0 +1,398 @@ +package tarball + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + "syscall" + + "github.com/canonical/chisel/internal/deb" + "github.com/canonical/chisel/internal/fsutil" + "github.com/canonical/chisel/internal/strdist" +) + +type ExtractOptions struct { + Package string + TargetDir string + Extract map[string][]ExtractInfo + // Create can optionally be set to control the creation of extracted entries. + // extractInfos is set to the matching entries in Extract, and is nil in cases where + // the created entry is implicit and unlisted (for example, parent directories). + Create func(extractInfos []ExtractInfo, options *fsutil.CreateOptions) error +} + +type ExtractInfo struct { + Path string + Mode uint + Optional bool + Context any +} + +func getValidOptions(options *ExtractOptions) (*ExtractOptions, error) { + for extractPath, extractInfos := range options.Extract { + isGlob := strings.ContainsAny(extractPath, "*?") + if isGlob { + for _, extractInfo := range extractInfos { + if extractInfo.Path != extractPath || extractInfo.Mode != 0 { + return nil, fmt.Errorf("when using wildcards source and target paths must match: %s", extractPath) + } + } + } + } + + if options.Create == nil { + validOpts := *options + validOpts.Create = func(_ []ExtractInfo, o *fsutil.CreateOptions) error { + _, err := fsutil.Create(o) + return err + } + return &validOpts, nil + } + + return options, nil +} + +func Extract(pkgReader io.ReadSeeker, options *ExtractOptions) (err error) { + defer func() { + if err != nil { + err = fmt.Errorf("cannot extract from package %q: %w", options.Package, err) + } + }() + + logf("Extracting files from package %q...", options.Package) + + validOpts, err := getValidOptions(options) + if err != nil { + return err + } + + _, err = os.Stat(validOpts.TargetDir) + if os.IsNotExist(err) { + return fmt.Errorf("target directory does not exist") + } else if err != nil { + return err + } + + return extractData(pkgReader, validOpts) +} + +func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error { + dataReader, err := deb.DataReader(pkgReader) + if err != nil { + return err + } + defer dataReader.Close() + + oldUmask := syscall.Umask(0) + defer func() { + syscall.Umask(oldUmask) + }() + + pendingPaths := make(map[string]bool) + for extractPath, extractInfos := range options.Extract { + for _, extractInfo := range extractInfos { + if !extractInfo.Optional { + pendingPaths[extractPath] = true + break + } + } + } + + // Store the hard links that we cannot extract when we first iterate over + // the tarball. + // + // This happens because the tarball only stores the contents once in the + // first entry and the rest of them point to the first one. Therefore, we + // cannot tell whether we need to extract the content until after we get to + // a hard link. In this case, we need a second pass. + pendingHardLinks := make(map[string][]pendingHardLink) + + // When creating a file we will iterate through its parent directories and + // create them with the permissions defined in the tarball. + // + // The assumption is that the tar entries of the parent directories appear + // before the entry for the file itself. This is the case for .deb files but + // not for all tarballs. + tarDirMode := make(map[string]fs.FileMode) + tarReader := tar.NewReader(dataReader) + for { + tarHeader, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + sourcePath, ok := sanitizeTarPath(tarHeader.Name) + if !ok { + continue + } + + sourceIsDir := sourcePath[len(sourcePath)-1] == '/' + if sourceIsDir { + tarDirMode[sourcePath] = tarHeader.FileInfo().Mode() + } + + // Find all globs and copies that require this source, and map them by + // their target paths on disk. + targetPaths := map[string][]ExtractInfo{} + for extractPath, extractInfos := range options.Extract { + if extractPath == "" { + continue + } + if strings.ContainsAny(extractPath, "*?") { + if strdist.GlobPath(extractPath, sourcePath) { + targetPaths[sourcePath] = append(targetPaths[sourcePath], extractInfos...) + delete(pendingPaths, extractPath) + } + } else if extractPath == sourcePath { + for _, extractInfo := range extractInfos { + targetPaths[extractInfo.Path] = append(targetPaths[extractInfo.Path], extractInfo) + } + delete(pendingPaths, extractPath) + } + } + if len(targetPaths) == 0 { + // Nothing to do. + continue + } + + var contentCache []byte + var contentIsCached = len(targetPaths) > 1 && !sourceIsDir + if contentIsCached { + // Read and cache the content so it may be reused. + // As an alternative, to avoid having an entire file in + // memory at once this logic might open the first file + // written and copy it every time. For now, the choice + // is speed over memory efficiency. + data, err := io.ReadAll(tarReader) + if err != nil { + return err + } + contentCache = data + } + + var pathReader io.Reader = tarReader + for targetPath, extractInfos := range targetPaths { + if contentIsCached { + pathReader = bytes.NewReader(contentCache) + } + mode := extractInfos[0].Mode + for _, extractInfo := range extractInfos { + if extractInfo.Mode != mode { + if mode < extractInfo.Mode { + mode, extractInfo.Mode = extractInfo.Mode, mode + } + return fmt.Errorf("path %s requested twice with diverging mode: 0%03o != 0%03o", targetPath, mode, extractInfo.Mode) + } + } + if mode != 0 { + tarHeader.Mode = int64(mode) + } + // Create the parent directories using the permissions from the tarball. + parents := parentDirs(targetPath) + for _, path := range parents { + if path == "/" { + continue + } + mode, ok := tarDirMode[path] + if !ok { + continue + } + delete(tarDirMode, path) + + createOptions := &fsutil.CreateOptions{ + Root: options.TargetDir, + Path: path, + Mode: mode, + MakeParents: true, + } + err := options.Create(nil, createOptions) + if err != nil { + return err + } + } + link := tarHeader.Linkname + if tarHeader.Typeflag == tar.TypeLink { + // A hard link requires the real path of the target file. + link = filepath.Join(options.TargetDir, link) + } + + // Create the entry itself. + createOptions := &fsutil.CreateOptions{ + Root: options.TargetDir, + Path: targetPath, + Mode: tarHeader.FileInfo().Mode(), + Data: pathReader, + Link: link, + MakeParents: true, + OverrideMode: true, + } + err := options.Create(extractInfos, createOptions) + if err != nil && os.IsNotExist(err) && tarHeader.Typeflag == tar.TypeLink { + // The hard link could not be created because the content + // was not extracted previously. Add this hard link entry + // to the pending list to extract later. + relLinkPath, ok := sanitizeTarPath(tarHeader.Linkname) + if !ok { + return fmt.Errorf("invalid link target %s", tarHeader.Linkname) + } + info := pendingHardLink{ + path: targetPath, + extractInfos: extractInfos, + } + pendingHardLinks[relLinkPath] = append(pendingHardLinks[relLinkPath], info) + } else if err != nil { + return err + } + } + } + + if len(pendingHardLinks) > 0 { + // Go over the tarball again to textract the pending hard links. + extractHardLinkOptions := &extractHardLinkOptions{ + ExtractOptions: options, + pendingLinks: pendingHardLinks, + } + _, err := pkgReader.Seek(0, io.SeekStart) + if err != nil { + return err + } + err = extractHardLinks(pkgReader, extractHardLinkOptions) + if err != nil { + return err + } + } + + if len(pendingPaths) > 0 { + pendingList := make([]string, 0, len(pendingPaths)) + for pendingPath := range pendingPaths { + pendingList = append(pendingList, pendingPath) + } + if len(pendingList) == 1 { + return fmt.Errorf("no content at %s", pendingList[0]) + } else { + sort.Strings(pendingList) + return fmt.Errorf("no content at:\n- %s", strings.Join(pendingList, "\n- ")) + } + } + + return nil +} + +type pendingHardLink struct { + path string + extractInfos []ExtractInfo +} + +type extractHardLinkOptions struct { + *ExtractOptions + pendingLinks map[string][]pendingHardLink +} + +// extractHardLinks iterates through the tarball a second time to extract the +// hard links that were not extracted in the first pass. +func extractHardLinks(pkgReader io.ReadSeeker, opts *extractHardLinkOptions) error { + dataReader, err := deb.DataReader(pkgReader) + if err != nil { + return err + } + defer dataReader.Close() + + tarReader := tar.NewReader(dataReader) + for { + tarHeader, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + sourcePath, ok := sanitizeTarPath(tarHeader.Name) + if !ok { + continue + } + + links := opts.pendingLinks[sourcePath] + if len(links) == 0 { + continue + } + + // For a target path, the first hard link will be created as a file with + // the content of the target path. If there are more pending hard links, + // the remaining ones will be created as hard links with the newly + // created file as their target. + absLink := filepath.Join(opts.TargetDir, links[0].path) + // Extract the content to the first hard link path. + createOptions := &fsutil.CreateOptions{ + Root: opts.TargetDir, + Path: links[0].path, + Mode: tarHeader.FileInfo().Mode(), + Data: tarReader, + } + err = opts.Create(links[0].extractInfos, createOptions) + if err != nil { + return err + } + + // Create the remaining hard links. + for _, link := range links[1:] { + createOptions := &fsutil.CreateOptions{ + Root: opts.TargetDir, + Path: link.path, + Mode: tarHeader.FileInfo().Mode(), + // Link to the first file extracted for the hard links. + Link: absLink, + } + err := opts.Create(link.extractInfos, createOptions) + if err != nil { + return err + } + } + delete(opts.pendingLinks, sourcePath) + } + + // If there are pending links, that means the link targets do not come from + // this package. + if len(opts.pendingLinks) > 0 { + var targets []string + for target := range opts.pendingLinks { + targets = append(targets, target) + } + sort.Strings(targets) + link := opts.pendingLinks[targets[0]][0] + return fmt.Errorf("cannot create hard link %s: no content at %s", link.path, targets[0]) + } + + return nil +} + +func parentDirs(path string) []string { + path = filepath.Clean(path) + parents := make([]string, strings.Count(path, "/")) + count := 0 + for i, c := range path { + if c == '/' { + parents[count] = path[:i+1] + count++ + } + } + return parents +} + +// sanitizeTarPath removes the leading "./" from the source path in the tarball, +// and verifies that the path is not empty. +func sanitizeTarPath(path string) (string, bool) { + if len(path) < 3 || path[0] != '.' || path[1] != '/' { + return "", false + } + return path[1:], true +} diff --git a/internal/deb/extract_test.go b/internal/tarball/extract_test.go similarity index 71% rename from internal/deb/extract_test.go rename to internal/tarball/extract_test.go index 1ec0b8a5f..fd5eb6147 100644 --- a/internal/deb/extract_test.go +++ b/internal/tarball/extract_test.go @@ -1,4 +1,4 @@ -package deb_test +package tarball_test import ( "bytes" @@ -10,16 +10,16 @@ import ( . "gopkg.in/check.v1" - "github.com/canonical/chisel/internal/deb" "github.com/canonical/chisel/internal/fsutil" + "github.com/canonical/chisel/internal/tarball" "github.com/canonical/chisel/internal/testutil" ) type extractTest struct { summary string pkgdata []byte - options deb.ExtractOptions - hackopt func(c *C, o *deb.ExtractOptions) + options tarball.ExtractOptions + hackopt func(c *C, o *tarball.ExtractOptions) result map[string]string // paths which the extractor did not create explicitly. notCreated []string @@ -29,28 +29,28 @@ type extractTest struct { var extractTests = []extractTest{{ summary: "Extract nothing", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ + options: tarball.ExtractOptions{ Extract: nil, }, result: map[string]string{}, }, { summary: "Extract a few entries", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/file": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/file": []tarball.ExtractInfo{{ Path: "/dir/file", }}, - "/dir/other-file": []deb.ExtractInfo{{ + "/dir/other-file": []tarball.ExtractInfo{{ Path: "/dir/other-file", }}, - "/dir/several/levels/deep/file": []deb.ExtractInfo{{ + "/dir/several/levels/deep/file": []tarball.ExtractInfo{{ Path: "/dir/several/levels/deep/file", }}, - "/dir/nested/": []deb.ExtractInfo{{ + "/dir/nested/": []tarball.ExtractInfo{{ Path: "/dir/nested/", }}, - "/other-dir/": []deb.ExtractInfo{{ + "/other-dir/": []tarball.ExtractInfo{{ Path: "/other-dir/", }}, }, @@ -70,21 +70,21 @@ var extractTests = []extractTest{{ }, { summary: "Extract a few entries, nil Create closure", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/file": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/file": []tarball.ExtractInfo{{ Path: "/dir/file", }}, - "/dir/other-file": []deb.ExtractInfo{{ + "/dir/other-file": []tarball.ExtractInfo{{ Path: "/dir/other-file", }}, - "/dir/several/levels/deep/file": []deb.ExtractInfo{{ + "/dir/several/levels/deep/file": []tarball.ExtractInfo{{ Path: "/dir/several/levels/deep/file", }}, - "/dir/nested/": []deb.ExtractInfo{{ + "/dir/nested/": []tarball.ExtractInfo{{ Path: "/dir/nested/", }}, - "/other-dir/": []deb.ExtractInfo{{ + "/other-dir/": []tarball.ExtractInfo{{ Path: "/other-dir/", }}, }, @@ -100,19 +100,19 @@ var extractTests = []extractTest{{ "/dir/several/levels/deep/file": "file 0644 6bc26dff", "/other-dir/": "dir 0755", }, - hackopt: func(c *C, o *deb.ExtractOptions) { + hackopt: func(c *C, o *tarball.ExtractOptions) { o.Create = nil }, }, { summary: "Copy a couple of entries elsewhere", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/file": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/file": []tarball.ExtractInfo{{ Path: "/foo/file-copy", Mode: 0600, }}, - "/dir/several/levels/deep/": []deb.ExtractInfo{{ + "/dir/several/levels/deep/": []tarball.ExtractInfo{{ Path: "/foo/bar/dir-copy", Mode: 0700, }}, @@ -128,9 +128,9 @@ var extractTests = []extractTest{{ }, { summary: "Copy same file twice", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/file": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/file": []tarball.ExtractInfo{{ Path: "/dir/foo/file-copy-1", }, { Path: "/dir/bar/file-copy-2", @@ -148,9 +148,9 @@ var extractTests = []extractTest{{ }, { summary: "Globbing a single dir level", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/s*/": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/s*/": []tarball.ExtractInfo{{ Path: "/dir/s*/", }}, }, @@ -163,9 +163,9 @@ var extractTests = []extractTest{{ }, { summary: "Globbing for files with multiple levels at once", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/s**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/s**": []tarball.ExtractInfo{{ Path: "/dir/s**", }}, }, @@ -181,12 +181,12 @@ var extractTests = []extractTest{{ }, { summary: "Globbing multiple paths", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/s**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/s**": []tarball.ExtractInfo{{ Path: "/dir/s**", }}, - "/dir/n*/": []deb.ExtractInfo{{ + "/dir/n*/": []tarball.ExtractInfo{{ Path: "/dir/n*/", }}, }, @@ -203,9 +203,9 @@ var extractTests = []extractTest{{ }, { summary: "Globbing must have matching source and target", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/foo/b**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/foo/b**": []tarball.ExtractInfo{{ Path: "/foo/g**", }}, }, @@ -214,9 +214,9 @@ var extractTests = []extractTest{{ }, { summary: "Globbing must also have a single target", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/foo/b**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/foo/b**": []tarball.ExtractInfo{{ Path: "/foo/b**", }, { Path: "/foo/g**", @@ -227,9 +227,9 @@ var extractTests = []extractTest{{ }, { summary: "Globbing cannot change modes", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/n**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/n**": []tarball.ExtractInfo{{ Path: "/dir/n**", Mode: 0777, }}, @@ -239,9 +239,9 @@ var extractTests = []extractTest{{ }, { summary: "Missing file", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/missing-file": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/missing-file": []tarball.ExtractInfo{{ Path: "/missing-file", }}, }, @@ -250,9 +250,9 @@ var extractTests = []extractTest{{ }, { summary: "Missing directory", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/missing-dir/": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/missing-dir/": []tarball.ExtractInfo{{ Path: "/missing-dir/", }}, }, @@ -261,9 +261,9 @@ var extractTests = []extractTest{{ }, { summary: "Missing glob", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/missing-dir/**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/missing-dir/**": []tarball.ExtractInfo{{ Path: "/missing-dir/**", }}, }, @@ -272,12 +272,12 @@ var extractTests = []extractTest{{ }, { summary: "Missing multiple entries", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/missing-file": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/missing-file": []tarball.ExtractInfo{{ Path: "missing-file", }}, - "/missing-dir/": []deb.ExtractInfo{{ + "/missing-dir/": []tarball.ExtractInfo{{ Path: "/missing-dir/", }}, }, @@ -286,16 +286,16 @@ var extractTests = []extractTest{{ }, { summary: "Optional entries may be missing", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/": []tarball.ExtractInfo{{ Path: "/dir/", }}, - "/dir/optional": []deb.ExtractInfo{{ + "/dir/optional": []tarball.ExtractInfo{{ Path: "/other-dir/foo", Optional: true, }}, - "/optional-dir/": []deb.ExtractInfo{{ + "/optional-dir/": []tarball.ExtractInfo{{ Path: "/foo/optional-dir/", Optional: true, }}, @@ -308,9 +308,9 @@ var extractTests = []extractTest{{ }, { summary: "Optional entries mixed in cannot be missing", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/missing-file": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/missing-file": []tarball.ExtractInfo{{ Path: "/dir/optional", Optional: true, }, { @@ -327,9 +327,9 @@ var extractTests = []extractTest{{ testutil.Dir(0766, "./日本/"), testutil.Reg(0644, "./日本/語", "whatever"), }), - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/日本/語": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/日本/語": []tarball.ExtractInfo{{ Path: "/日本/語", }}, }, @@ -342,13 +342,13 @@ var extractTests = []extractTest{{ }, { summary: "Entries for same destination must have the same mode", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/": []tarball.ExtractInfo{{ Path: "/dir/", Mode: 0777, }}, - "/d**": []deb.ExtractInfo{{ + "/d**": []tarball.ExtractInfo{{ Path: "/d**", }}, }, @@ -361,9 +361,9 @@ var extractTests = []extractTest{{ testutil.Reg(0644, "./file", "text for file"), testutil.Hrd(0644, "./hardlink", "./file"), }), - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/**": []tarball.ExtractInfo{{ Path: "/**", }}, }, @@ -380,9 +380,9 @@ var extractTests = []extractTest{{ testutil.Reg(0644, "./file", "text for file"), testutil.Hrd(0644, "./hardlink", "./file"), }), - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/hardlink": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/hardlink": []tarball.ExtractInfo{{ Path: "/hardlink", }}, }, @@ -397,9 +397,9 @@ var extractTests = []extractTest{{ testutil.Dir(0755, "./"), testutil.Hrd(0644, "./hardlink", "./non-existing-target"), }), - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/hardlink": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/hardlink": []tarball.ExtractInfo{{ Path: "/hardlink", }}, }, @@ -412,9 +412,9 @@ var extractTests = []extractTest{{ testutil.Hrd(0644, "./hardlink1", "./non-existing-target"), testutil.Hrd(0644, "./hardlink2", "./non-existing-target"), }), - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/**": []tarball.ExtractInfo{{ Path: "/**", }}, }, @@ -427,9 +427,9 @@ var extractTests = []extractTest{{ testutil.Lnk(0644, "./symlink", "./file"), testutil.Hrd(0644, "./hardlink", "./symlink"), }), - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/**": []tarball.ExtractInfo{{ Path: "/**", }}, }, @@ -442,15 +442,15 @@ var extractTests = []extractTest{{ }, { summary: "Explicit extraction overrides existing file", pkgdata: testutil.PackageData["test-package"], - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/": []tarball.ExtractInfo{{ Path: "/dir/", Mode: 0777, }}, }, }, - hackopt: func(c *C, o *deb.ExtractOptions) { + hackopt: func(c *C, o *tarball.ExtractOptions) { err := os.Mkdir(path.Join(o.TargetDir, "/dir"), 0666) c.Assert(err, IsNil) }, @@ -464,9 +464,9 @@ var extractTests = []extractTest{{ testutil.Dir(0755, "./"), testutil.Hrd(0644, "./hardlink", "/etc/group"), }), - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/**": []tarball.ExtractInfo{{ Path: "/**", }}, }, @@ -478,9 +478,9 @@ var extractTests = []extractTest{{ testutil.Dir(0755, "./"), testutil.Reg(0644, "./../file", "hijacking system file"), }), - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/**": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/**": []tarball.ExtractInfo{{ Path: "/**", }}, }, @@ -497,7 +497,7 @@ func (s *S) TestExtract(c *C) { options.Package = "test-package" options.TargetDir = dir createdPaths := make(map[string]bool) - options.Create = func(_ []deb.ExtractInfo, o *fsutil.CreateOptions) error { + options.Create = func(_ []tarball.ExtractInfo, o *fsutil.CreateOptions) error { relPath := filepath.Clean("/" + strings.TrimPrefix(o.Path, dir)) if o.Mode.IsDir() { relPath = relPath + "/" @@ -511,7 +511,7 @@ func (s *S) TestExtract(c *C) { test.hackopt(c, &options) } - err := deb.Extract(bytes.NewReader(test.pkgdata), &options) + err := tarball.Extract(bytes.NewReader(test.pkgdata), &options) if test.error != "" { c.Assert(err, ErrorMatches, test.error) continue @@ -539,8 +539,8 @@ func (s *S) TestExtract(c *C) { var extractCreateCallbackTests = []struct { summary string pkgdata []byte - options deb.ExtractOptions - calls map[string][]deb.ExtractInfo + options tarball.ExtractOptions + calls map[string][]tarball.ExtractInfo }{{ summary: "Create is called with the set of ExtractInfo(s) that match the file", pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -548,50 +548,50 @@ var extractCreateCallbackTests = []struct { testutil.Dir(0766, "./dir/"), testutil.Reg(0644, "./dir/file", "whatever"), }), - options: deb.ExtractOptions{ - Extract: map[string][]deb.ExtractInfo{ - "/dir/": []deb.ExtractInfo{{ + options: tarball.ExtractOptions{ + Extract: map[string][]tarball.ExtractInfo{ + "/dir/": []tarball.ExtractInfo{{ Path: "/dir/", }}, - "/d**": []deb.ExtractInfo{{ + "/d**": []tarball.ExtractInfo{{ Path: "/d**", }}, - "/d?r/": []deb.ExtractInfo{{ + "/d?r/": []tarball.ExtractInfo{{ Path: "/d?r/", }}, - "/dir/file": []deb.ExtractInfo{{ + "/dir/file": []tarball.ExtractInfo{{ Path: "/dir/file", }, { Path: "/dir/file-cpy", }}, - "/foo/": []deb.ExtractInfo{{ + "/foo/": []tarball.ExtractInfo{{ Path: "/foo/", Optional: true, }}, }, }, - calls: map[string][]deb.ExtractInfo{ - "/dir/": []deb.ExtractInfo{ - deb.ExtractInfo{ + calls: map[string][]tarball.ExtractInfo{ + "/dir/": []tarball.ExtractInfo{ + tarball.ExtractInfo{ Path: "/d**", }, - deb.ExtractInfo{ + tarball.ExtractInfo{ Path: "/d?r/", }, - deb.ExtractInfo{ + tarball.ExtractInfo{ Path: "/dir/", }, }, - "/dir/file": []deb.ExtractInfo{ - deb.ExtractInfo{ + "/dir/file": []tarball.ExtractInfo{ + tarball.ExtractInfo{ Path: "/d**", }, - deb.ExtractInfo{ + tarball.ExtractInfo{ Path: "/dir/file", }, }, - "/dir/file-cpy": []deb.ExtractInfo{ - deb.ExtractInfo{ + "/dir/file-cpy": []tarball.ExtractInfo{ + tarball.ExtractInfo{ Path: "/dir/file-cpy", }, }, @@ -605,8 +605,8 @@ func (s *S) TestExtractCreateCallback(c *C) { options := test.options options.Package = "test-package" options.TargetDir = dir - createExtractInfos := map[string][]deb.ExtractInfo{} - options.Create = func(extractInfos []deb.ExtractInfo, o *fsutil.CreateOptions) error { + createExtractInfos := map[string][]tarball.ExtractInfo{} + options.Create = func(extractInfos []tarball.ExtractInfo, o *fsutil.CreateOptions) error { if extractInfos == nil { // Creating implicit parent directories, we don't care about those. return nil @@ -622,7 +622,7 @@ func (s *S) TestExtractCreateCallback(c *C) { return nil } - err := deb.Extract(bytes.NewReader(test.pkgdata), &options) + err := tarball.Extract(bytes.NewReader(test.pkgdata), &options) c.Assert(err, IsNil) c.Assert(createExtractInfos, DeepEquals, test.calls) diff --git a/internal/tarball/log.go b/internal/tarball/log.go new file mode 100644 index 000000000..903bf279d --- /dev/null +++ b/internal/tarball/log.go @@ -0,0 +1,53 @@ +package tarball + +import ( + "fmt" + "sync" +) + +// Avoid importing the log type information unnecessarily. There's a small cost +// associated with using an interface rather than the type. Depending on how +// often the logger is plugged in, it would be worth using the type instead. +type log_Logger interface { + Output(calldepth int, s string) error +} + +var globalLoggerLock sync.Mutex +var globalLogger log_Logger +var globalDebug bool + +// Specify the *log.Logger object where log messages should be sent to. +func SetLogger(logger log_Logger) { + globalLoggerLock.Lock() + globalLogger = logger + globalLoggerLock.Unlock() +} + +// Enable the delivery of debug messages to the logger. Only meaningful +// if a logger is also set. +func SetDebug(debug bool) { + globalLoggerLock.Lock() + globalDebug = debug + globalLoggerLock.Unlock() +} + +// logf sends to the logger registered via SetLogger the string resulting +// from running format and args through Sprintf. +func logf(format string, args ...any) { + globalLoggerLock.Lock() + defer globalLoggerLock.Unlock() + if globalLogger != nil { + globalLogger.Output(2, fmt.Sprintf(format, args...)) + } +} + +// debugf sends to the logger registered via SetLogger the string resulting +// from running format and args through Sprintf, but only if debugging was +// enabled via SetDebug. +func debugf(format string, args ...any) { + globalLoggerLock.Lock() + defer globalLoggerLock.Unlock() + if globalDebug && globalLogger != nil { + globalLogger.Output(2, fmt.Sprintf(format, args...)) + } +} diff --git a/internal/tarball/suite_test.go b/internal/tarball/suite_test.go new file mode 100644 index 000000000..e99ce3e92 --- /dev/null +++ b/internal/tarball/suite_test.go @@ -0,0 +1,20 @@ +package tarball_test + +import ( + "testing" + + . "gopkg.in/check.v1" + + "github.com/canonical/chisel/internal/tarball" +) + +func Test(t *testing.T) { TestingT(t) } + +type S struct{} + +var _ = Suite(&S{}) + +func (s *S) SetUpTest(c *C) { + tarball.SetDebug(true) + tarball.SetLogger(c) +}