github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/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 // Walk (part of snap.Container) is like filepath.Walk, without the ordering guarantee. 298 func (s *Snap) Walk(relative string, walkFn filepath.WalkFunc) error { 299 relative = filepath.Clean(relative) 300 if relative == "" || relative == "/" { 301 relative = "." 302 } else if relative[0] == '/' { 303 // I said relative, darn it :-) 304 relative = relative[1:] 305 } 306 307 var cmd *exec.Cmd 308 if relative == "." { 309 cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path) 310 } else { 311 cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path, relative) 312 } 313 cmd.Env = []string{"TZ=UTC"} 314 stdout, err := cmd.StdoutPipe() 315 if err != nil { 316 return walkFn(relative, nil, err) 317 } 318 if err := cmd.Start(); err != nil { 319 return walkFn(relative, nil, err) 320 } 321 defer cmd.Process.Kill() 322 323 scanner := bufio.NewScanner(stdout) 324 // skip the header 325 for scanner.Scan() { 326 if len(scanner.Bytes()) == 0 { 327 break 328 } 329 } 330 331 skipper := make(skipper) 332 for scanner.Scan() { 333 st, err := fromRaw(scanner.Bytes()) 334 if err != nil { 335 err = walkFn(relative, nil, err) 336 if err != nil { 337 return err 338 } 339 } else { 340 path := filepath.Join(relative, st.Path()) 341 if skipper.Has(path) { 342 continue 343 } 344 err = walkFn(path, st, nil) 345 if err != nil { 346 if err == filepath.SkipDir && st.IsDir() { 347 skipper.Add(path) 348 } else { 349 return err 350 } 351 } 352 } 353 } 354 355 if err := scanner.Err(); err != nil { 356 return walkFn(relative, nil, err) 357 } 358 359 if err := cmd.Wait(); err != nil { 360 return walkFn(relative, nil, err) 361 } 362 return nil 363 } 364 365 // ListDir returns the content of a single directory inside a squashfs snap. 366 func (s *Snap) ListDir(dirPath string) ([]string, error) { 367 output, err := exec.Command( 368 "unsquashfs", "-no-progress", "-dest", "_", "-l", s.path, dirPath).CombinedOutput() 369 if err != nil { 370 return nil, osutil.OutputErr(output, err) 371 } 372 373 prefixPath := path.Join("_", dirPath) 374 pattern, err := regexp.Compile("(?m)^" + regexp.QuoteMeta(prefixPath) + "/([^/\r\n]+)$") 375 if err != nil { 376 return nil, fmt.Errorf("internal error: cannot compile squashfs list dir regexp for %q: %s", dirPath, err) 377 } 378 379 var directoryContents []string 380 for _, groups := range pattern.FindAllSubmatch(output, -1) { 381 if len(groups) > 1 { 382 directoryContents = append(directoryContents, string(groups[1])) 383 } 384 } 385 386 return directoryContents, nil 387 } 388 389 const maxErrPaths = 10 390 391 type errPathsNotReadable struct { 392 paths []string 393 } 394 395 func (e *errPathsNotReadable) accumulate(p string, fi os.FileInfo) error { 396 if len(e.paths) >= maxErrPaths { 397 return e 398 } 399 if st, ok := fi.Sys().(*syscall.Stat_t); ok { 400 e.paths = append(e.paths, fmt.Sprintf("%s (owner %v:%v mode %#03o)", p, st.Uid, st.Gid, fi.Mode().Perm())) 401 } else { 402 e.paths = append(e.paths, p) 403 } 404 return nil 405 } 406 407 func (e *errPathsNotReadable) asErr() error { 408 if len(e.paths) > 0 { 409 return e 410 } 411 return nil 412 } 413 414 func (e *errPathsNotReadable) Error() string { 415 var b bytes.Buffer 416 417 b.WriteString("cannot access the following locations in the snap source directory:\n") 418 for _, p := range e.paths { 419 fmt.Fprintf(&b, "- ") 420 fmt.Fprintf(&b, p) 421 fmt.Fprintf(&b, "\n") 422 } 423 if len(e.paths) == maxErrPaths { 424 fmt.Fprintf(&b, "- too many errors, listing first %v entries\n", maxErrPaths) 425 } 426 return b.String() 427 } 428 429 // verifyContentAccessibleForBuild checks whether the content under source 430 // directory is usable to the user and can be represented by mksquashfs. 431 func verifyContentAccessibleForBuild(sourceDir string) error { 432 var errPaths errPathsNotReadable 433 434 withSlash := filepath.Clean(sourceDir) + "/" 435 err := filepath.Walk(withSlash, func(path string, st os.FileInfo, err error) error { 436 if err != nil { 437 if !os.IsPermission(err) { 438 return err 439 } 440 // accumulate permission errors 441 return errPaths.accumulate(strings.TrimPrefix(path, withSlash), st) 442 } 443 mode := st.Mode() 444 if !mode.IsRegular() && !mode.IsDir() { 445 // device nodes are just recreated by mksquashfs 446 return nil 447 } 448 if mode.IsRegular() && st.Size() == 0 { 449 // empty files are also recreated 450 return nil 451 } 452 453 f, err := os.Open(path) 454 if err != nil { 455 if !os.IsPermission(err) { 456 return err 457 } 458 // accumulate permission errors 459 if err = errPaths.accumulate(strings.TrimPrefix(path, withSlash), st); err != nil { 460 return err 461 } 462 // workaround for https://github.com/golang/go/issues/21758 463 // with pre 1.10 go, explicitly skip directory 464 if mode.IsDir() { 465 return filepath.SkipDir 466 } 467 return nil 468 } 469 f.Close() 470 return nil 471 }) 472 if err != nil { 473 return err 474 } 475 return errPaths.asErr() 476 } 477 478 type MksquashfsError struct { 479 msg string 480 } 481 482 func (m MksquashfsError) Error() string { 483 return m.msg 484 } 485 486 type BuildOpts struct { 487 SnapType string 488 Compression string 489 ExcludeFiles []string 490 } 491 492 // Build builds the snap. 493 func (s *Snap) Build(sourceDir string, opts *BuildOpts) error { 494 if opts == nil { 495 opts = &BuildOpts{} 496 } 497 if err := verifyContentAccessibleForBuild(sourceDir); err != nil { 498 return err 499 } 500 501 fullSnapPath, err := filepath.Abs(s.path) 502 if err != nil { 503 return err 504 } 505 // default to xz 506 compression := opts.Compression 507 if compression == "" { 508 // TODO: support other compression options, xz is very 509 // slow for certain apps, see 510 // https://forum.snapcraft.io/t/squashfs-performance-effect-on-snap-startup-time/13920 511 compression = "xz" 512 } 513 cmd, err := snapdtoolCommandFromSystemSnap("/usr/bin/mksquashfs") 514 if err != nil { 515 cmd = exec.Command("mksquashfs") 516 } 517 cmd.Args = append(cmd.Args, 518 ".", fullSnapPath, 519 "-noappend", 520 "-comp", compression, 521 "-no-fragments", 522 "-no-progress", 523 ) 524 if len(opts.ExcludeFiles) > 0 { 525 cmd.Args = append(cmd.Args, "-wildcards") 526 for _, excludeFile := range opts.ExcludeFiles { 527 cmd.Args = append(cmd.Args, "-ef", excludeFile) 528 } 529 } 530 snapType := opts.SnapType 531 if snapType != "os" && snapType != "core" && snapType != "base" { 532 cmd.Args = append(cmd.Args, "-all-root", "-no-xattrs") 533 } 534 535 return osutil.ChDir(sourceDir, func() error { 536 output, err := cmd.CombinedOutput() 537 if err != nil { 538 return MksquashfsError{fmt.Sprintf("mksquashfs call failed: %s", osutil.OutputErr(output, err))} 539 } 540 541 return nil 542 }) 543 } 544 545 // BuildDate returns the "Creation or last append time" as reported by unsquashfs. 546 func (s *Snap) BuildDate() time.Time { 547 return BuildDate(s.path) 548 } 549 550 // BuildDate returns the "Creation or last append time" as reported by unsquashfs. 551 func BuildDate(path string) time.Time { 552 var t0 time.Time 553 554 const prefix = "Creation or last append time " 555 m := &strutil.MatchCounter{ 556 Regexp: regexp.MustCompile("(?m)^" + prefix + ".*$"), 557 N: 1, 558 } 559 560 cmd := exec.Command("unsquashfs", "-n", "-s", path) 561 cmd.Env = []string{"TZ=UTC"} 562 cmd.Stdout = m 563 cmd.Stderr = m 564 if err := cmd.Run(); err != nil { 565 return t0 566 } 567 matches, count := m.Matches() 568 if count != 1 { 569 return t0 570 } 571 t0, _ = time.Parse(time.ANSIC, matches[0][len(prefix):]) 572 return t0 573 }