github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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 }