diff --git a/internal/deb/extract.go b/internal/deb/extract.go index 5de2f403..3b837bb5 100644 --- a/internal/deb/extract.go +++ b/internal/deb/extract.go @@ -6,6 +6,7 @@ import ( "compress/gzip" "fmt" "io" + "io/fs" "os" "path/filepath" "sort" @@ -109,34 +110,8 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error { syscall.Umask(oldUmask) }() - shouldExtract := func(pkgPath string) (globPath string, ok bool) { - if pkgPath == "" { - return "", false - } - pkgPathIsDir := pkgPath[len(pkgPath)-1] == '/' - for extractPath, extractInfos := range options.Extract { - if extractPath == "" { - continue - } - switch { - case strings.ContainsAny(extractPath, "*?"): - if strdist.GlobPath(extractPath, pkgPath) { - return extractPath, true - } - case extractPath == pkgPath: - return "", true - case pkgPathIsDir: - for _, extractInfo := range extractInfos { - if strings.HasPrefix(extractInfo.Path, pkgPath) { - return "", true - } - } - } - } - return "", false - } - pendingPaths := make(map[string]bool) + var globs []string for extractPath, extractInfos := range options.Extract { for _, extractInfo := range extractInfos { if !extractInfo.Optional { @@ -144,8 +119,26 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error { break } } + if strings.ContainsAny(extractPath, "*?") { + globs = append(globs, extractPath) + } } + // dirInfo represents a directory that we + // a) encountered in the tarball, + // b) created, or + // c) both a) and c). + type dirInfo struct { + // mode of the directory with which we + // a) encountered it in the tarball, or + // b) created it + mode fs.FileMode + // whether we created this directory + created bool + } + // directories we encountered and/or created + dirInfos := make(map[string]dirInfo) + tarReader := tar.NewReader(dataReader) for { tarHeader, err := tarReader.Next() @@ -161,44 +154,83 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error { continue } sourcePath = sourcePath[1:] - globPath, ok := shouldExtract(sourcePath) - if !ok { - continue + sourceMode := tarHeader.FileInfo().Mode() + + globPath := "" + extractInfos := options.Extract[sourcePath] + + if extractInfos == nil { + for _, glob := range globs { + if strdist.GlobPath(glob, sourcePath) { + globPath = glob + extractInfos = []ExtractInfo{{Path: glob}} + break + } + } } - sourceIsDir := sourcePath[len(sourcePath)-1] == '/' + // Is this a directory path that was not requested? + if extractInfos == nil && sourceMode.IsDir() { + if info := dirInfos[sourcePath]; info.mode != sourceMode { + // We have not seen this directory yet, or we + // have seen or created it with a different mode + // before. Record the source path mode. + info.mode = sourceMode + dirInfos[sourcePath] = info + if info.created { + // We have created this directory before + // with a different mode. Create it + // again with the proper mode. + extractInfos = []ExtractInfo{{Path: sourcePath}} + } + } + } - //debugf("Extracting header: %#v", tarHeader) + if extractInfos == nil { + continue + } - var extractInfos []ExtractInfo if globPath != "" { - extractInfos = options.Extract[globPath] - delete(pendingPaths, globPath) if options.Globbed != nil { options.Globbed[globPath] = append(options.Globbed[globPath], sourcePath) } + delete(pendingPaths, globPath) } else { - extractInfos, ok = options.Extract[sourcePath] - if ok { - delete(pendingPaths, sourcePath) - } else { - // Base directory for extracted content. Relevant mainly to preserve - // the metadata, since the extracted content itself will also create - // any missing directories unaccounted for in the options. - err := fsutil.Create(&fsutil.CreateOptions{ - Path: filepath.Join(options.TargetDir, sourcePath), - Mode: tarHeader.FileInfo().Mode(), - MakeParents: true, - }) - if err != nil { - return err - } - continue + delete(pendingPaths, sourcePath) + } + + // createParents creates missing parent directories of the path + // with modes with which they were encountered in the tarball or + // 0755 if they were not encountered yet. + var createParents func(path string) error + createParents = func(path string) error { + dir := fsutil.SlashedPathDir(path) + if dir == "/" { + return nil } + info := dirInfos[dir] + if info.created { + return nil + } else if info.mode == 0 { + info.mode = fs.ModeDir | 0755 + } + if err := createParents(dir); err != nil { + return err + } + create := fsutil.CreateOptions{ + Path: filepath.Join(options.TargetDir, dir), + Mode: info.mode, + } + if err := fsutil.Create(&create); err != nil { + return err + } + info.created = true + dirInfos[dir] = info + return nil } var contentCache []byte - var contentIsCached = len(extractInfos) > 1 && !sourceIsDir && globPath == "" + var contentIsCached = len(extractInfos) > 1 && sourceMode.IsRegular() && globPath == "" if contentIsCached { // Read and cache the content so it may be reused. // As an alternative, to avoid having an entire file in @@ -219,23 +251,30 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error { } var targetPath string if globPath == "" { - targetPath = filepath.Join(options.TargetDir, extractInfo.Path) + targetPath = extractInfo.Path } else { - targetPath = filepath.Join(options.TargetDir, sourcePath) + targetPath = sourcePath + } + if err := createParents(targetPath); err != nil { + return err } if extractInfo.Mode != 0 { tarHeader.Mode = int64(extractInfo.Mode) } + fsMode := tarHeader.FileInfo().Mode() err := fsutil.Create(&fsutil.CreateOptions{ - Path: targetPath, - Mode: tarHeader.FileInfo().Mode(), - Data: pathReader, - Link: tarHeader.Linkname, - MakeParents: true, + Path: filepath.Join(options.TargetDir, targetPath), + Mode: fsMode, + Data: pathReader, + Link: tarHeader.Linkname, }) if err != nil { return err } + if fsMode.IsDir() { + // Record the target directory mode. + dirInfos[targetPath] = dirInfo{fsMode, true} + } if globPath != "" { break } diff --git a/internal/deb/extract_test.go b/internal/deb/extract_test.go index 3147dc36..7de591ab 100644 --- a/internal/deb/extract_test.go +++ b/internal/deb/extract_test.go @@ -9,6 +9,12 @@ import ( "github.com/canonical/chisel/internal/testutil" ) +var ( + Reg = testutil.Reg + Dir = testutil.Dir + Lnk = testutil.Lnk +) + type extractTest struct { summary string pkgdata []byte @@ -260,10 +266,7 @@ var extractTests = []extractTest{{ }, }, result: map[string]string{ - "/etc/": "dir 0755", - "/usr/": "dir 0755", - "/usr/bin/": "dir 0755", - "/tmp/": "dir 01777", + "/etc/": "dir 0755", }, }, { summary: "Optional entries mixed in cannot be missing", @@ -280,6 +283,136 @@ var extractTests = []extractTest{{ }, }, error: `cannot extract from package "base-files": no content at /usr/bin/hallo`, +}, { + summary: "Implicit parent directories", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + Dir(0701, "./a/"), + Dir(0702, "./a/b/"), + Reg(0601, "./a/b/c", ""), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/a/b/c": []deb.ExtractInfo{{Path: "/a/b/c"}}, + }, + }, + result: map[string]string{ + "/a/": "dir 0701", + "/a/b/": "dir 0702", + "/a/b/c": "file 0601 empty", + }, +}, { + summary: "Implicit parent directories with different target path", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + Dir(0701, "./a/"), + Dir(0702, "./b/"), + Reg(0601, "./b/x", "shark"), + Dir(0703, "./c/"), + Reg(0602, "./c/y", "octopus"), + Dir(0704, "./d/"), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/b/x": []deb.ExtractInfo{{Path: "/a/x"}}, + "/c/y": []deb.ExtractInfo{{Path: "/d/y"}}, + }, + }, + result: map[string]string{ + "/a/": "dir 0701", + "/a/x": "file 0601 31fc1594", + "/d/": "dir 0704", + "/d/y": "file 0602 5633c9b8", + }, +}, { + summary: "Implicit parent directories with a glob", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + Dir(0701, "./a/"), + Dir(0702, "./a/aa/"), + Dir(0703, "./a/aa/aaa/"), + Reg(0601, "./a/aa/aaa/ffff", ""), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/a/aa/a**": []deb.ExtractInfo{{ + Path: "/a/aa/a**", + }}, + }, + }, + result: map[string]string{ + "/a/": "dir 0701", + "/a/aa/": "dir 0702", + "/a/aa/aaa/": "dir 0703", + "/a/aa/aaa/ffff": "file 0601 empty", + }, +}, { + summary: "Implicit parent directories with a glob and non-sorted tarball", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + Reg(0601, "./a/b/c/d", ""), + Dir(0702, "./a/b/"), + Dir(0703, "./a/b/c/"), + Dir(0701, "./a/"), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/a/b/c/*": []deb.ExtractInfo{{ + Path: "/a/b/c/*", + }}, + }, + }, + result: map[string]string{ + "/a/": "dir 0701", + "/a/b/": "dir 0702", + "/a/b/c/": "dir 0703", + "/a/b/c/d": "file 0601 empty", + }, +}, { + summary: "Implicit parent directories with a glob and some parents missing in the tarball", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + Reg(0601, "./a/b/c/d", ""), + Dir(0702, "./a/b/"), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/a/b/c/*": []deb.ExtractInfo{{ + Path: "/a/b/c/*", + }}, + }, + }, + result: map[string]string{ + "/a/": "dir 0755", + "/a/b/": "dir 0702", + "/a/b/c/": "dir 0755", + "/a/b/c/d": "file 0601 empty", + }, +}, { + summary: "Implicit parent directories with copied dirs and different modes", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + Dir(0701, "./a/"), + Dir(0702, "./a/b/"), + Dir(0703, "./a/b/c/"), + Reg(0601, "./a/b/c/d", ""), + Dir(0704, "./e/"), + Dir(0705, "./e/f/"), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/a/b/**": []deb.ExtractInfo{{ + Path: "/a/b/**", + }}, + "/e/f/": []deb.ExtractInfo{{ + Path: "/a/", + }}, + "/e/": []deb.ExtractInfo{{ + Path: "/a/b/c/", + Mode: 0706, + }}, + }, + }, + result: map[string]string{ + "/a/": "dir 0705", + "/a/b/": "dir 0702", + "/a/b/c/": "dir 0706", + "/a/b/c/d": "file 0601 empty", + }, }} func (s *S) TestExtract(c *C) { diff --git a/internal/slicer/slicer.go b/internal/slicer/slicer.go index 9cd35aec..3152ca67 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -109,14 +109,13 @@ func Run(options *RunOptions) error { hasCopyright = true } } else { - targetDir := fsutil.SlashedPathDir(targetPath) - if targetDir == "" || targetDir == "/" { - continue + parent := fsutil.SlashedPathDir(targetPath) + for ; parent != "/"; parent = fsutil.SlashedPathDir(parent) { + extractPackage[parent] = append(extractPackage[parent], deb.ExtractInfo{ + Path: parent, + Optional: true, + }) } - extractPackage[targetDir] = append(extractPackage[targetDir], deb.ExtractInfo{ - Path: targetDir, - Optional: true, - }) } } if !hasCopyright {