gitee.com/mysnapcore/mysnapd@v0.1.0/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  	"gitee.com/mysnapcore/mysnapd/dirs"
    38  	"gitee.com/mysnapcore/mysnapd/logger"
    39  	"gitee.com/mysnapcore/mysnapd/osutil"
    40  	"gitee.com/mysnapcore/mysnapd/snap"
    41  	"gitee.com/mysnapcore/mysnapd/snap/internal"
    42  	"gitee.com/mysnapcore/mysnapd/snapdtool"
    43  	"gitee.com/mysnapcore/mysnapd/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  	var output bytes.Buffer
   206  	cmd := exec.Command("unsquashfs", "-n", "-f", "-d", dstDir, s.path, src)
   207  	cmd.Stderr = io.MultiWriter(&output, usw)
   208  	if err := cmd.Run(); err != nil {
   209  		return fmt.Errorf("cannot extract %q to %q: %v", src, dstDir, osutil.OutputErr(output.Bytes(), err))
   210  	}
   211  	// older versions of unsquashfs do not report errors via exit code,
   212  	// so we need this extra check.
   213  	if usw.Err() != nil {
   214  		return fmt.Errorf("cannot extract %q to %q: %v", src, dstDir, usw.Err())
   215  	}
   216  
   217  	return nil
   218  }
   219  
   220  // Size returns the size of a squashfs snap.
   221  func (s *Snap) Size() (size int64, err error) {
   222  	st, err := os.Stat(s.path)
   223  	if err != nil {
   224  		return 0, err
   225  	}
   226  
   227  	return st.Size(), nil
   228  }
   229  
   230  func (s *Snap) withUnpackedFile(filePath string, f func(p string) error) error {
   231  	tmpdir, err := ioutil.TempDir("", "read-file")
   232  	if err != nil {
   233  		return err
   234  	}
   235  	defer os.RemoveAll(tmpdir)
   236  
   237  	unpackDir := filepath.Join(tmpdir, "unpack")
   238  	if output, err := exec.Command("unsquashfs", "-n", "-i", "-d", unpackDir, s.path, filePath).CombinedOutput(); err != nil {
   239  		return fmt.Errorf("cannot run unsquashfs: %v", osutil.OutputErr(output, err))
   240  	}
   241  
   242  	return f(filepath.Join(unpackDir, filePath))
   243  }
   244  
   245  // RandomAccessFile returns an implementation to read at any given
   246  // location for a single file inside the squashfs snap plus
   247  // information about the file size.
   248  func (s *Snap) RandomAccessFile(filePath string) (interface {
   249  	io.ReaderAt
   250  	io.Closer
   251  	Size() int64
   252  }, error) {
   253  	var f *os.File
   254  	err := s.withUnpackedFile(filePath, func(p string) (err error) {
   255  		f, err = os.Open(p)
   256  		return
   257  	})
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  	return internal.NewSizedFile(f)
   262  }
   263  
   264  // ReadFile returns the content of a single file inside a squashfs snap.
   265  func (s *Snap) ReadFile(filePath string) (content []byte, err error) {
   266  	err = s.withUnpackedFile(filePath, func(p string) (err error) {
   267  		content, err = ioutil.ReadFile(p)
   268  		return
   269  	})
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  	return content, nil
   274  }
   275  
   276  // skipper is used to track directories that should be skipped
   277  //
   278  // Given sk := make(skipper), if you sk.Add("foo/bar"), then
   279  // sk.Has("foo/bar") is true, but also sk.Has("foo/bar/baz")
   280  //
   281  // It could also be a map[string]bool, but because it's only supposed
   282  // to be checked through its Has method as above, the small added
   283  // complexity of it being a map[string]struct{} lose to the associated
   284  // space savings.
   285  type skipper map[string]struct{}
   286  
   287  func (sk skipper) Add(path string) {
   288  	sk[filepath.Clean(path)] = struct{}{}
   289  }
   290  
   291  func (sk skipper) Has(path string) bool {
   292  	for p := filepath.Clean(path); p != "." && p != "/"; p = filepath.Dir(p) {
   293  		if _, ok := sk[p]; ok {
   294  			return true
   295  		}
   296  	}
   297  
   298  	return false
   299  }
   300  
   301  // pre-4.5 unsquashfs writes a funny header like:
   302  //     "Parallel unsquashfs: Using 1 processor"
   303  //     "1 inodes (1 blocks) to write"
   304  //     ""   <-- empty line
   305  var maybeHeaderRegex = regexp.MustCompile(`^(Parallel unsquashfs: Using .* processor.*|[0-9]+ inodes .* to write)$`)
   306  
   307  // Walk (part of snap.Container) is like filepath.Walk, without the ordering guarantee.
   308  func (s *Snap) Walk(relative string, walkFn filepath.WalkFunc) error {
   309  	relative = filepath.Clean(relative)
   310  	if relative == "" || relative == "/" {
   311  		relative = "."
   312  	} else if relative[0] == '/' {
   313  		// I said relative, darn it :-)
   314  		relative = relative[1:]
   315  	}
   316  
   317  	var cmd *exec.Cmd
   318  	if relative == "." {
   319  		cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path)
   320  	} else {
   321  		cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path, relative)
   322  	}
   323  	cmd.Env = []string{"TZ=UTC"}
   324  	stdout, err := cmd.StdoutPipe()
   325  	if err != nil {
   326  		return walkFn(relative, nil, err)
   327  	}
   328  	if err := cmd.Start(); err != nil {
   329  		return walkFn(relative, nil, err)
   330  	}
   331  	defer cmd.Process.Kill()
   332  
   333  	scanner := bufio.NewScanner(stdout)
   334  	skipper := make(skipper)
   335  	seenHeader := false
   336  	for scanner.Scan() {
   337  		raw := scanner.Bytes()
   338  		if !seenHeader {
   339  			// try to match the header written by older (pre-4.5)
   340  			// squashfs tools
   341  			if len(scanner.Bytes()) == 0 ||
   342  				maybeHeaderRegex.Match(raw) {
   343  				continue
   344  			} else {
   345  				seenHeader = true
   346  			}
   347  		}
   348  		st, err := fromRaw(raw)
   349  		if err != nil {
   350  			err = walkFn(relative, nil, err)
   351  			if err != nil {
   352  				return err
   353  			}
   354  		} else {
   355  			path := filepath.Join(relative, st.Path())
   356  			if skipper.Has(path) {
   357  				continue
   358  			}
   359  			err = walkFn(path, st, nil)
   360  			if err != nil {
   361  				if err == filepath.SkipDir && st.IsDir() {
   362  					skipper.Add(path)
   363  				} else {
   364  					return err
   365  				}
   366  			}
   367  		}
   368  	}
   369  
   370  	if err := scanner.Err(); err != nil {
   371  		return walkFn(relative, nil, err)
   372  	}
   373  
   374  	if err := cmd.Wait(); err != nil {
   375  		return walkFn(relative, nil, err)
   376  	}
   377  	return nil
   378  }
   379  
   380  // ListDir returns the content of a single directory inside a squashfs snap.
   381  func (s *Snap) ListDir(dirPath string) ([]string, error) {
   382  	output, err := exec.Command(
   383  		"unsquashfs", "-no-progress", "-dest", "_", "-l", s.path, dirPath).CombinedOutput()
   384  	if err != nil {
   385  		return nil, osutil.OutputErr(output, err)
   386  	}
   387  
   388  	prefixPath := path.Join("_", dirPath)
   389  	pattern, err := regexp.Compile("(?m)^" + regexp.QuoteMeta(prefixPath) + "/([^/\r\n]+)$")
   390  	if err != nil {
   391  		return nil, fmt.Errorf("internal error: cannot compile squashfs list dir regexp for %q: %s", dirPath, err)
   392  	}
   393  
   394  	var directoryContents []string
   395  	for _, groups := range pattern.FindAllSubmatch(output, -1) {
   396  		if len(groups) > 1 {
   397  			directoryContents = append(directoryContents, string(groups[1]))
   398  		}
   399  	}
   400  
   401  	return directoryContents, nil
   402  }
   403  
   404  const maxErrPaths = 10
   405  
   406  type errPathsNotReadable struct {
   407  	paths []string
   408  }
   409  
   410  func (e *errPathsNotReadable) accumulate(p string, fi os.FileInfo) error {
   411  	if len(e.paths) >= maxErrPaths {
   412  		return e
   413  	}
   414  	if st, ok := fi.Sys().(*syscall.Stat_t); ok {
   415  		e.paths = append(e.paths, fmt.Sprintf("%s (owner %v:%v mode %#03o)", p, st.Uid, st.Gid, fi.Mode().Perm()))
   416  	} else {
   417  		e.paths = append(e.paths, p)
   418  	}
   419  	return nil
   420  }
   421  
   422  func (e *errPathsNotReadable) asErr() error {
   423  	if len(e.paths) > 0 {
   424  		return e
   425  	}
   426  	return nil
   427  }
   428  
   429  func (e *errPathsNotReadable) Error() string {
   430  	var b bytes.Buffer
   431  
   432  	b.WriteString("cannot access the following locations in the snap source directory:\n")
   433  	for _, p := range e.paths {
   434  		fmt.Fprintf(&b, "- ")
   435  		fmt.Fprintf(&b, p)
   436  		fmt.Fprintf(&b, "\n")
   437  	}
   438  	if len(e.paths) == maxErrPaths {
   439  		fmt.Fprintf(&b, "- too many errors, listing first %v entries\n", maxErrPaths)
   440  	}
   441  	return b.String()
   442  }
   443  
   444  // verifyContentAccessibleForBuild checks whether the content under source
   445  // directory is usable to the user and can be represented by mksquashfs.
   446  func verifyContentAccessibleForBuild(sourceDir string) error {
   447  	var errPaths errPathsNotReadable
   448  
   449  	withSlash := filepath.Clean(sourceDir) + "/"
   450  	err := filepath.Walk(withSlash, func(path string, st os.FileInfo, err error) error {
   451  		if err != nil {
   452  			if !os.IsPermission(err) {
   453  				return err
   454  			}
   455  			// accumulate permission errors
   456  			return errPaths.accumulate(strings.TrimPrefix(path, withSlash), st)
   457  		}
   458  		mode := st.Mode()
   459  		if !mode.IsRegular() && !mode.IsDir() {
   460  			// device nodes are just recreated by mksquashfs
   461  			return nil
   462  		}
   463  		if mode.IsRegular() && st.Size() == 0 {
   464  			// empty files are also recreated
   465  			return nil
   466  		}
   467  
   468  		f, err := os.Open(path)
   469  		if err != nil {
   470  			if !os.IsPermission(err) {
   471  				return err
   472  			}
   473  			// accumulate permission errors
   474  			if err = errPaths.accumulate(strings.TrimPrefix(path, withSlash), st); err != nil {
   475  				return err
   476  			}
   477  			// workaround for https://github.com/golang/go/issues/21758
   478  			// with pre 1.10 go, explicitly skip directory
   479  			if mode.IsDir() {
   480  				return filepath.SkipDir
   481  			}
   482  			return nil
   483  		}
   484  		f.Close()
   485  		return nil
   486  	})
   487  	if err != nil {
   488  		return err
   489  	}
   490  	return errPaths.asErr()
   491  }
   492  
   493  type MksquashfsError struct {
   494  	msg string
   495  }
   496  
   497  func (m MksquashfsError) Error() string {
   498  	return m.msg
   499  }
   500  
   501  type BuildOpts struct {
   502  	SnapType     string
   503  	Compression  string
   504  	ExcludeFiles []string
   505  }
   506  
   507  // Build builds the snap.
   508  func (s *Snap) Build(sourceDir string, opts *BuildOpts) error {
   509  	if opts == nil {
   510  		opts = &BuildOpts{}
   511  	}
   512  	if err := verifyContentAccessibleForBuild(sourceDir); err != nil {
   513  		return err
   514  	}
   515  
   516  	fullSnapPath, err := filepath.Abs(s.path)
   517  	if err != nil {
   518  		return err
   519  	}
   520  	// default to xz
   521  	compression := opts.Compression
   522  	if compression == "" {
   523  		// TODO: support other compression options, xz is very
   524  		// slow for certain apps, see
   525  		// https://forum.snapcraft.io/t/squashfs-performance-effect-on-snap-startup-time/13920
   526  		compression = "xz"
   527  	}
   528  	cmd, err := snapdtoolCommandFromSystemSnap("/usr/bin/mksquashfs")
   529  	if err != nil {
   530  		cmd = exec.Command("mksquashfs")
   531  	}
   532  	cmd.Args = append(cmd.Args,
   533  		".", fullSnapPath,
   534  		"-noappend",
   535  		"-comp", compression,
   536  		"-no-fragments",
   537  		"-no-progress",
   538  	)
   539  	if len(opts.ExcludeFiles) > 0 {
   540  		cmd.Args = append(cmd.Args, "-wildcards")
   541  		for _, excludeFile := range opts.ExcludeFiles {
   542  			cmd.Args = append(cmd.Args, "-ef", excludeFile)
   543  		}
   544  	}
   545  	snapType := opts.SnapType
   546  	if snapType != "os" && snapType != "core" && snapType != "base" {
   547  		cmd.Args = append(cmd.Args, "-all-root", "-no-xattrs")
   548  	}
   549  
   550  	return osutil.ChDir(sourceDir, func() error {
   551  		output, err := cmd.CombinedOutput()
   552  		if err != nil {
   553  			return MksquashfsError{fmt.Sprintf("mksquashfs call failed: %s", osutil.OutputErr(output, err))}
   554  		}
   555  
   556  		return nil
   557  	})
   558  }
   559  
   560  // BuildDate returns the "Creation or last append time" as reported by unsquashfs.
   561  func (s *Snap) BuildDate() time.Time {
   562  	return BuildDate(s.path)
   563  }
   564  
   565  // BuildDate returns the "Creation or last append time" as reported by unsquashfs.
   566  func BuildDate(path string) time.Time {
   567  	var t0 time.Time
   568  
   569  	const prefix = "Creation or last append time "
   570  	m := &strutil.MatchCounter{
   571  		Regexp: regexp.MustCompile("(?m)^" + prefix + ".*$"),
   572  		N:      1,
   573  	}
   574  
   575  	cmd := exec.Command("unsquashfs", "-n", "-s", path)
   576  	cmd.Env = []string{"TZ=UTC"}
   577  	cmd.Stdout = m
   578  	cmd.Stderr = m
   579  	if err := cmd.Run(); err != nil {
   580  		return t0
   581  	}
   582  	matches, count := m.Matches()
   583  	if count != 1 {
   584  		return t0
   585  	}
   586  	t0, _ = time.Parse(time.ANSIC, matches[0][len(prefix):])
   587  	return t0
   588  }