github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/snap/squashfs/squashfs.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package squashfs
    21  
    22  import (
    23  	"bufio"
    24  	"bytes"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"os"
    29  	"os/exec"
    30  	"path"
    31  	"path/filepath"
    32  	"regexp"
    33  	"strings"
    34  	"syscall"
    35  	"time"
    36  
    37  	"github.com/snapcore/snapd/dirs"
    38  	"github.com/snapcore/snapd/logger"
    39  	"github.com/snapcore/snapd/osutil"
    40  	"github.com/snapcore/snapd/snap"
    41  	"github.com/snapcore/snapd/snap/internal"
    42  	"github.com/snapcore/snapd/snapdtool"
    43  	"github.com/snapcore/snapd/strutil"
    44  )
    45  
    46  const (
    47  	// https://github.com/plougher/squashfs-tools/blob/master/squashfs-tools/squashfs_fs.h#L289
    48  	superblockSize = 96
    49  )
    50  
    51  var (
    52  	// magic is the magic prefix of squashfs snap files.
    53  	magic = []byte{'h', 's', 'q', 's'}
    54  
    55  	// for testing
    56  	isRootWritableOverlay = osutil.IsRootWritableOverlay
    57  )
    58  
    59  func FileHasSquashfsHeader(path string) bool {
    60  	f, err := os.Open(path)
    61  	if err != nil {
    62  		return false
    63  	}
    64  	defer f.Close()
    65  
    66  	// a squashfs file would contain at least the superblock + some data
    67  	header := make([]byte, superblockSize+1)
    68  	if _, err := f.ReadAt(header, 0); err != nil {
    69  		return false
    70  	}
    71  
    72  	return bytes.HasPrefix(header, magic)
    73  }
    74  
    75  // Snap is the squashfs based snap.
    76  type Snap struct {
    77  	path string
    78  }
    79  
    80  // Path returns the path of the backing file.
    81  func (s *Snap) Path() string {
    82  	return s.path
    83  }
    84  
    85  // New returns a new Squashfs snap.
    86  func New(snapPath string) *Snap {
    87  	return &Snap{path: snapPath}
    88  }
    89  
    90  var osLink = os.Link
    91  var snapdtoolCommandFromSystemSnap = snapdtool.CommandFromSystemSnap
    92  
    93  // Install installs a squashfs snap file through an appropriate method.
    94  func (s *Snap) Install(targetPath, mountDir string, opts *snap.InstallOptions) (bool, error) {
    95  
    96  	// ensure mount-point and blob target dir.
    97  	for _, dir := range []string{mountDir, filepath.Dir(targetPath)} {
    98  		if err := os.MkdirAll(dir, 0755); err != nil {
    99  			return false, err
   100  		}
   101  	}
   102  
   103  	// This is required so that the tests can simulate a mounted
   104  	// snap when we "install" a squashfs snap in the tests.
   105  	// We can not mount it for real in the tests, so we just unpack
   106  	// it to the location which is good enough for the tests.
   107  	if osutil.GetenvBool("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS") {
   108  		if err := s.Unpack("*", mountDir); err != nil {
   109  			return false, err
   110  		}
   111  	}
   112  
   113  	// nothing to do, happens on e.g. first-boot when we already
   114  	// booted with the OS snap but its also in the seed.yaml
   115  	if s.path == targetPath || osutil.FilesAreEqual(s.path, targetPath) {
   116  		didNothing := true
   117  		return didNothing, nil
   118  	}
   119  
   120  	overlayRoot, err := isRootWritableOverlay()
   121  	if err != nil {
   122  		logger.Noticef("cannot detect root filesystem on overlay: %v", err)
   123  	}
   124  	// Hard-linking on overlayfs is identical to a full blown
   125  	// copy.  When we are operating on a overlayfs based system (e.g. live
   126  	// installer) use symbolic links.
   127  	// https://bugs.launchpad.net/snapd/+bug/1867415
   128  	if overlayRoot == "" {
   129  		// try to (hard)link the file, but go on to trying to copy it
   130  		// if it fails for whatever reason
   131  		//
   132  		// link(2) returns EPERM on filesystems that don't support
   133  		// hard links (like vfat), so checking the error here doesn't
   134  		// make sense vs just trying to copy it.
   135  		if err := osLink(s.path, targetPath); err == nil {
   136  			return false, nil
   137  		}
   138  	}
   139  
   140  	// if the installation must not cross devices, then we should not use
   141  	// symlinks and instead must copy the file entirely, this is the case
   142  	// during seeding on uc20 in run mode for example
   143  	if opts == nil || !opts.MustNotCrossDevices {
   144  		// if the source snap file is in seed, but the hardlink failed, symlinking
   145  		// it saves the copy (which in livecd is expensive) so try that next
   146  		// note that on UC20, the snap file could be in a deep subdir of
   147  		// SnapSeedDir, i.e. /var/lib/snapd/seed/systems/20200521/snaps/<name>.snap
   148  		// so we need to check if it has the prefix of the seed dir
   149  		cleanSrc := filepath.Clean(s.path)
   150  		if strings.HasPrefix(cleanSrc, dirs.SnapSeedDir) {
   151  			if os.Symlink(s.path, targetPath) == nil {
   152  				return false, nil
   153  			}
   154  		}
   155  	}
   156  
   157  	return false, osutil.CopyFile(s.path, targetPath, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync)
   158  }
   159  
   160  // unsquashfsStderrWriter is a helper that captures errors from
   161  // unsquashfs on stderr. Because unsquashfs will potentially
   162  // (e.g. on out-of-diskspace) report an error on every single
   163  // file we limit the reported error lines to 4.
   164  //
   165  // unsquashfs does not exit with an exit code for write errors
   166  // (e.g. no space left on device). There is an upstream PR
   167  // to fix this https://github.com/plougher/squashfs-tools/pull/46
   168  //
   169  // However in the meantime we can detect errors by looking
   170  // on stderr for "failed" which is pretty consistently used in
   171  // the unsquashfs.c source in case of errors.
   172  type unsquashfsStderrWriter struct {
   173  	strutil.MatchCounter
   174  }
   175  
   176  var unsquashfsStderrRegexp = regexp.MustCompile(`(?m).*\b[Ff]ailed\b.*`)
   177  
   178  func newUnsquashfsStderrWriter() *unsquashfsStderrWriter {
   179  	return &unsquashfsStderrWriter{strutil.MatchCounter{
   180  		Regexp: unsquashfsStderrRegexp,
   181  		N:      4, // note Err below uses this value
   182  	}}
   183  }
   184  
   185  func (u *unsquashfsStderrWriter) Err() error {
   186  	// here we use that our N is 4.
   187  	errors, count := u.Matches()
   188  	switch count {
   189  	case 0:
   190  		return nil
   191  	case 1:
   192  		return fmt.Errorf("failed: %q", errors[0])
   193  	case 2, 3, 4:
   194  		return fmt.Errorf("failed: %s, and %q", strutil.Quoted(errors[:len(errors)-1]), errors[len(errors)-1])
   195  	default:
   196  		// count > len(matches)
   197  		extra := count - len(errors)
   198  		return fmt.Errorf("failed: %s, and %d more", strutil.Quoted(errors), extra)
   199  	}
   200  }
   201  
   202  func (s *Snap) Unpack(src, dstDir string) error {
   203  	usw := newUnsquashfsStderrWriter()
   204  
   205  	cmd := exec.Command("unsquashfs", "-n", "-f", "-d", dstDir, s.path, src)
   206  	cmd.Stderr = usw
   207  	if err := cmd.Run(); err != nil {
   208  		return err
   209  	}
   210  	if usw.Err() != nil {
   211  		return fmt.Errorf("cannot extract %q to %q: %v", src, dstDir, usw.Err())
   212  	}
   213  	return nil
   214  }
   215  
   216  // Size returns the size of a squashfs snap.
   217  func (s *Snap) Size() (size int64, err error) {
   218  	st, err := os.Stat(s.path)
   219  	if err != nil {
   220  		return 0, err
   221  	}
   222  
   223  	return st.Size(), nil
   224  }
   225  
   226  func (s *Snap) withUnpackedFile(filePath string, f func(p string) error) error {
   227  	tmpdir, err := ioutil.TempDir("", "read-file")
   228  	if err != nil {
   229  		return err
   230  	}
   231  	defer os.RemoveAll(tmpdir)
   232  
   233  	unpackDir := filepath.Join(tmpdir, "unpack")
   234  	if output, err := exec.Command("unsquashfs", "-n", "-i", "-d", unpackDir, s.path, filePath).CombinedOutput(); err != nil {
   235  		return fmt.Errorf("cannot run unsquashfs: %v", osutil.OutputErr(output, err))
   236  	}
   237  
   238  	return f(filepath.Join(unpackDir, filePath))
   239  }
   240  
   241  // RandomAccessFile returns an implementation to read at any given
   242  // location for a single file inside the squashfs snap plus
   243  // information about the file size.
   244  func (s *Snap) RandomAccessFile(filePath string) (interface {
   245  	io.ReaderAt
   246  	io.Closer
   247  	Size() int64
   248  }, error) {
   249  	var f *os.File
   250  	err := s.withUnpackedFile(filePath, func(p string) (err error) {
   251  		f, err = os.Open(p)
   252  		return
   253  	})
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	return internal.NewSizedFile(f)
   258  }
   259  
   260  // ReadFile returns the content of a single file inside a squashfs snap.
   261  func (s *Snap) ReadFile(filePath string) (content []byte, err error) {
   262  	err = s.withUnpackedFile(filePath, func(p string) (err error) {
   263  		content, err = ioutil.ReadFile(p)
   264  		return
   265  	})
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	return content, nil
   270  }
   271  
   272  // skipper is used to track directories that should be skipped
   273  //
   274  // Given sk := make(skipper), if you sk.Add("foo/bar"), then
   275  // sk.Has("foo/bar") is true, but also sk.Has("foo/bar/baz")
   276  //
   277  // It could also be a map[string]bool, but because it's only supposed
   278  // to be checked through its Has method as above, the small added
   279  // complexity of it being a map[string]struct{} lose to the associated
   280  // space savings.
   281  type skipper map[string]struct{}
   282  
   283  func (sk skipper) Add(path string) {
   284  	sk[filepath.Clean(path)] = struct{}{}
   285  }
   286  
   287  func (sk skipper) Has(path string) bool {
   288  	for p := filepath.Clean(path); p != "." && p != "/"; p = filepath.Dir(p) {
   289  		if _, ok := sk[p]; ok {
   290  			return true
   291  		}
   292  	}
   293  
   294  	return false
   295  }
   296  
   297  // pre-4.5 unsquashfs writes a funny header like:
   298  //     "Parallel unsquashfs: Using 1 processor"
   299  //     "1 inodes (1 blocks) to write"
   300  //     ""   <-- empty line
   301  var maybeHeaderRegex = regexp.MustCompile(`^(Parallel unsquashfs: Using .* processor.*|[0-9]+ inodes .* to write)$`)
   302  
   303  // Walk (part of snap.Container) is like filepath.Walk, without the ordering guarantee.
   304  func (s *Snap) Walk(relative string, walkFn filepath.WalkFunc) error {
   305  	relative = filepath.Clean(relative)
   306  	if relative == "" || relative == "/" {
   307  		relative = "."
   308  	} else if relative[0] == '/' {
   309  		// I said relative, darn it :-)
   310  		relative = relative[1:]
   311  	}
   312  
   313  	var cmd *exec.Cmd
   314  	if relative == "." {
   315  		cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path)
   316  	} else {
   317  		cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path, relative)
   318  	}
   319  	cmd.Env = []string{"TZ=UTC"}
   320  	stdout, err := cmd.StdoutPipe()
   321  	if err != nil {
   322  		return walkFn(relative, nil, err)
   323  	}
   324  	if err := cmd.Start(); err != nil {
   325  		return walkFn(relative, nil, err)
   326  	}
   327  	defer cmd.Process.Kill()
   328  
   329  	scanner := bufio.NewScanner(stdout)
   330  	skipper := make(skipper)
   331  	seenHeader := false
   332  	for scanner.Scan() {
   333  		raw := scanner.Bytes()
   334  		if !seenHeader {
   335  			// try to match the header written by older (pre-4.5)
   336  			// squashfs tools
   337  			if len(scanner.Bytes()) == 0 ||
   338  				maybeHeaderRegex.Match(raw) {
   339  				continue
   340  			} else {
   341  				seenHeader = true
   342  			}
   343  		}
   344  		st, err := fromRaw(raw)
   345  		if err != nil {
   346  			err = walkFn(relative, nil, err)
   347  			if err != nil {
   348  				return err
   349  			}
   350  		} else {
   351  			path := filepath.Join(relative, st.Path())
   352  			if skipper.Has(path) {
   353  				continue
   354  			}
   355  			err = walkFn(path, st, nil)
   356  			if err != nil {
   357  				if err == filepath.SkipDir && st.IsDir() {
   358  					skipper.Add(path)
   359  				} else {
   360  					return err
   361  				}
   362  			}
   363  		}
   364  	}
   365  
   366  	if err := scanner.Err(); err != nil {
   367  		return walkFn(relative, nil, err)
   368  	}
   369  
   370  	if err := cmd.Wait(); err != nil {
   371  		return walkFn(relative, nil, err)
   372  	}
   373  	return nil
   374  }
   375  
   376  // ListDir returns the content of a single directory inside a squashfs snap.
   377  func (s *Snap) ListDir(dirPath string) ([]string, error) {
   378  	output, err := exec.Command(
   379  		"unsquashfs", "-no-progress", "-dest", "_", "-l", s.path, dirPath).CombinedOutput()
   380  	if err != nil {
   381  		return nil, osutil.OutputErr(output, err)
   382  	}
   383  
   384  	prefixPath := path.Join("_", dirPath)
   385  	pattern, err := regexp.Compile("(?m)^" + regexp.QuoteMeta(prefixPath) + "/([^/\r\n]+)$")
   386  	if err != nil {
   387  		return nil, fmt.Errorf("internal error: cannot compile squashfs list dir regexp for %q: %s", dirPath, err)
   388  	}
   389  
   390  	var directoryContents []string
   391  	for _, groups := range pattern.FindAllSubmatch(output, -1) {
   392  		if len(groups) > 1 {
   393  			directoryContents = append(directoryContents, string(groups[1]))
   394  		}
   395  	}
   396  
   397  	return directoryContents, nil
   398  }
   399  
   400  const maxErrPaths = 10
   401  
   402  type errPathsNotReadable struct {
   403  	paths []string
   404  }
   405  
   406  func (e *errPathsNotReadable) accumulate(p string, fi os.FileInfo) error {
   407  	if len(e.paths) >= maxErrPaths {
   408  		return e
   409  	}
   410  	if st, ok := fi.Sys().(*syscall.Stat_t); ok {
   411  		e.paths = append(e.paths, fmt.Sprintf("%s (owner %v:%v mode %#03o)", p, st.Uid, st.Gid, fi.Mode().Perm()))
   412  	} else {
   413  		e.paths = append(e.paths, p)
   414  	}
   415  	return nil
   416  }
   417  
   418  func (e *errPathsNotReadable) asErr() error {
   419  	if len(e.paths) > 0 {
   420  		return e
   421  	}
   422  	return nil
   423  }
   424  
   425  func (e *errPathsNotReadable) Error() string {
   426  	var b bytes.Buffer
   427  
   428  	b.WriteString("cannot access the following locations in the snap source directory:\n")
   429  	for _, p := range e.paths {
   430  		fmt.Fprintf(&b, "- ")
   431  		fmt.Fprintf(&b, p)
   432  		fmt.Fprintf(&b, "\n")
   433  	}
   434  	if len(e.paths) == maxErrPaths {
   435  		fmt.Fprintf(&b, "- too many errors, listing first %v entries\n", maxErrPaths)
   436  	}
   437  	return b.String()
   438  }
   439  
   440  // verifyContentAccessibleForBuild checks whether the content under source
   441  // directory is usable to the user and can be represented by mksquashfs.
   442  func verifyContentAccessibleForBuild(sourceDir string) error {
   443  	var errPaths errPathsNotReadable
   444  
   445  	withSlash := filepath.Clean(sourceDir) + "/"
   446  	err := filepath.Walk(withSlash, func(path string, st os.FileInfo, err error) error {
   447  		if err != nil {
   448  			if !os.IsPermission(err) {
   449  				return err
   450  			}
   451  			// accumulate permission errors
   452  			return errPaths.accumulate(strings.TrimPrefix(path, withSlash), st)
   453  		}
   454  		mode := st.Mode()
   455  		if !mode.IsRegular() && !mode.IsDir() {
   456  			// device nodes are just recreated by mksquashfs
   457  			return nil
   458  		}
   459  		if mode.IsRegular() && st.Size() == 0 {
   460  			// empty files are also recreated
   461  			return nil
   462  		}
   463  
   464  		f, err := os.Open(path)
   465  		if err != nil {
   466  			if !os.IsPermission(err) {
   467  				return err
   468  			}
   469  			// accumulate permission errors
   470  			if err = errPaths.accumulate(strings.TrimPrefix(path, withSlash), st); err != nil {
   471  				return err
   472  			}
   473  			// workaround for https://github.com/golang/go/issues/21758
   474  			// with pre 1.10 go, explicitly skip directory
   475  			if mode.IsDir() {
   476  				return filepath.SkipDir
   477  			}
   478  			return nil
   479  		}
   480  		f.Close()
   481  		return nil
   482  	})
   483  	if err != nil {
   484  		return err
   485  	}
   486  	return errPaths.asErr()
   487  }
   488  
   489  type MksquashfsError struct {
   490  	msg string
   491  }
   492  
   493  func (m MksquashfsError) Error() string {
   494  	return m.msg
   495  }
   496  
   497  type BuildOpts struct {
   498  	SnapType     string
   499  	Compression  string
   500  	ExcludeFiles []string
   501  }
   502  
   503  // Build builds the snap.
   504  func (s *Snap) Build(sourceDir string, opts *BuildOpts) error {
   505  	if opts == nil {
   506  		opts = &BuildOpts{}
   507  	}
   508  	if err := verifyContentAccessibleForBuild(sourceDir); err != nil {
   509  		return err
   510  	}
   511  
   512  	fullSnapPath, err := filepath.Abs(s.path)
   513  	if err != nil {
   514  		return err
   515  	}
   516  	// default to xz
   517  	compression := opts.Compression
   518  	if compression == "" {
   519  		// TODO: support other compression options, xz is very
   520  		// slow for certain apps, see
   521  		// https://forum.snapcraft.io/t/squashfs-performance-effect-on-snap-startup-time/13920
   522  		compression = "xz"
   523  	}
   524  	cmd, err := snapdtoolCommandFromSystemSnap("/usr/bin/mksquashfs")
   525  	if err != nil {
   526  		cmd = exec.Command("mksquashfs")
   527  	}
   528  	cmd.Args = append(cmd.Args,
   529  		".", fullSnapPath,
   530  		"-noappend",
   531  		"-comp", compression,
   532  		"-no-fragments",
   533  		"-no-progress",
   534  	)
   535  	if len(opts.ExcludeFiles) > 0 {
   536  		cmd.Args = append(cmd.Args, "-wildcards")
   537  		for _, excludeFile := range opts.ExcludeFiles {
   538  			cmd.Args = append(cmd.Args, "-ef", excludeFile)
   539  		}
   540  	}
   541  	snapType := opts.SnapType
   542  	if snapType != "os" && snapType != "core" && snapType != "base" {
   543  		cmd.Args = append(cmd.Args, "-all-root", "-no-xattrs")
   544  	}
   545  
   546  	return osutil.ChDir(sourceDir, func() error {
   547  		output, err := cmd.CombinedOutput()
   548  		if err != nil {
   549  			return MksquashfsError{fmt.Sprintf("mksquashfs call failed: %s", osutil.OutputErr(output, err))}
   550  		}
   551  
   552  		return nil
   553  	})
   554  }
   555  
   556  // BuildDate returns the "Creation or last append time" as reported by unsquashfs.
   557  func (s *Snap) BuildDate() time.Time {
   558  	return BuildDate(s.path)
   559  }
   560  
   561  // BuildDate returns the "Creation or last append time" as reported by unsquashfs.
   562  func BuildDate(path string) time.Time {
   563  	var t0 time.Time
   564  
   565  	const prefix = "Creation or last append time "
   566  	m := &strutil.MatchCounter{
   567  		Regexp: regexp.MustCompile("(?m)^" + prefix + ".*$"),
   568  		N:      1,
   569  	}
   570  
   571  	cmd := exec.Command("unsquashfs", "-n", "-s", path)
   572  	cmd.Env = []string{"TZ=UTC"}
   573  	cmd.Stdout = m
   574  	cmd.Stderr = m
   575  	if err := cmd.Run(); err != nil {
   576  		return t0
   577  	}
   578  	matches, count := m.Matches()
   579  	if count != 1 {
   580  		return t0
   581  	}
   582  	t0, _ = time.Parse(time.ANSIC, matches[0][len(prefix):])
   583  	return t0
   584  }