// Copyright 2014-2022 Ulrich Kunitz. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "bufio" "errors" "fmt" "io" "os" "os/signal" "path/filepath" "strings" "syscall" "github.com/ulikunitz/xz" "github.com/ulikunitz/xz/internal/xlog" "github.com/ulikunitz/xz/lzma" ) // signalHandler establishes the signal handler for SIGTERM(1) and // handles it in its own go routine. The returned quit channel must be // closed to terminate the signal handler go routine. func signalHandler(w *writer) chan<- struct{} { quit := make(chan struct{}) sigch := make(chan os.Signal, 1) signal.Notify(sigch, os.Interrupt, syscall.SIGPIPE) go func() { select { case <-quit: signal.Stop(sigch) return case <-sigch: w.removeTmpFile() os.Exit(7) } }() return quit } // format defines the newCompressor and newDecompressor functions for a // compression format. type format struct { newCompressor func(w io.Writer, opts *options) (c io.WriteCloser, err error) newDecompressor func(r io.Reader, opts *options) (d io.Reader, err error) validHeader func(br *bufio.Reader) bool } // dictCapExps maps preset values to exponent for dictionary capacity // sizes. var lzmaDictCapExps = []uint{18, 20, 21, 22, 22, 23, 23, 24, 25, 26} // formats contains the formats supported by gxz. var formats = map[string]*format{ "lzma": &format{ newCompressor: func(w io.Writer, opts *options, ) (c io.WriteCloser, err error) { lc := lzma.WriterConfig{ Properties: &lzma.Properties{LC: 3, LP: 0, PB: 2}, DictCap: 1 << lzmaDictCapExps[opts.preset], } return lc.NewWriter(w) }, newDecompressor: func(r io.Reader, opts *options, ) (d io.Reader, err error) { lc := lzma.ReaderConfig{ DictCap: 1 << lzmaDictCapExps[opts.preset], } return lc.NewReader(r) }, validHeader: func(br *bufio.Reader) bool { h, err := br.Peek(lzma.HeaderLen) if err != nil { return false } return lzma.ValidHeader(h) }, }, "xz": &format{ newCompressor: func(w io.Writer, opts *options, ) (c io.WriteCloser, err error) { cfg := xz.WriterConfig{ DictCap: 1 << lzmaDictCapExps[opts.preset], } return cfg.NewWriter(w) }, newDecompressor: func(r io.Reader, opts *options, ) (d io.Reader, err error) { cfg := xz.ReaderConfig{ DictCap: 1 << lzmaDictCapExps[opts.preset], } return cfg.NewReader(r) }, validHeader: func(br *bufio.Reader) bool { h, err := br.Peek(xz.HeaderLen) if err != nil { return false } return xz.ValidHeader(h) }, }, } var errBase = errors.New("name has no base part") // targetName finds the correct target name taking the options into // account. func targetName(path string, opts *options) (target string, err error) { if path == "-" { panic("path name - not supported") } if len(path) == 0 { return "", errors.New("empty file name not supported") } ext := "." + opts.format tarExt := ".txz" if opts.format == "lzma" { tarExt = ".tlz" } if !opts.decompress { if strings.HasSuffix(path, ext) { return "", fmt.Errorf( "%s: file has already %s suffix", path, ext) } if strings.HasSuffix(path, tarExt) { return "", fmt.Errorf( "%s: file has already %s suffix", path, tarExt) } return path + ext, nil } if strings.HasSuffix(path, ext) { target = path[:len(path)-len(ext)] if filepath.Base(target) == "" { return "", &userPathError{path, errBase} } return target, nil } if strings.HasSuffix(path, tarExt) { target = path[:len(path)-len(tarExt)] if filepath.Base(target) == "" { return "", &userPathError{path, errBase} } return target + ".tar", nil } return path, nil } // tmpName converts the path string into a temporary name by appending // .decompress or .compress to the file path. func tmpName(path string, decompress bool) string { var ext string if decompress { ext = ".decompress" } else { ext = ".compress" } return path + ext } // writer is used as file writer for decompression and file compressor // for compression. type writer struct { f *os.File name string bw *bufio.Writer io.Writer cmp io.WriteCloser success bool } // writerFormat select the writer format. func writerFormat(opts *options) (f *format, err error) { var ok bool if f, ok = formats[opts.format]; !ok { return nil, fmt.Errorf("compression format %q not supported", opts.format) } return f, nil } // newCompressor creates a compressor for the given writer. func newCompressor(w io.Writer, opts *options) (cmp io.WriteCloser, err error) { if opts.decompress { panic("no compressor needed") } f, err := writerFormat(opts) if err != nil { return nil, err } if cmp, err = f.newCompressor(w, opts); err != nil { return nil, err } return cmp, nil } // newWriter creates a new file writer. Note that options must contain // the actual compression format supported and not just auto. func newWriter(path string, perm os.FileMode, opts *options, ) (w *writer, err error) { w = &writer{name: path} if opts.stdout { w.f = os.Stdout w.name = "-" } else { name, err := targetName(path, opts) if err != nil { return nil, err } if _, err = os.Stat(name); !os.IsNotExist(err) { if !opts.force { return nil, &userPathError{ Path: name, Err: errors.New("file exists")} } } tmp := tmpName(name, opts.decompress) if w.f, err = os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm); err != nil { return nil, err } w.name = name } w.bw = bufio.NewWriter(w.f) if opts.decompress { w.Writer = w.bw return w, nil } w.cmp, err = newCompressor(w.bw, opts) if err != nil { return nil, &userPathError{w.name, err} } w.Writer = w.cmp return w, nil } // isStdout checks whether the parameter refers to stdout. func isStdout(f *os.File) bool { return f.Fd() == uintptr(syscall.Stdout) } var errInval = errors.New("invalid value") // Close closes the writer. Note that the behavior depends whether // success has been set for the writer. func (w *writer) Close() error { var err error if w.f == nil { return errInval } defer func() { w.f = nil }() if !w.success { if isStdout(w.f) { return nil } if err = w.f.Close(); err != nil { return err } if err = os.Remove(w.f.Name()); err != nil { return err } return nil } if w.cmp != nil { if err = w.cmp.Close(); err != nil { return err } } if err = w.bw.Flush(); err != nil { return err } if isStdout(w.f) { return nil } if err = w.f.Close(); err != nil { return err } if err = os.Rename(w.f.Name(), w.name); err != nil { return err } return nil } // removeTmpFile removes the temporary file for the writer. It is used // by the signal handler goroutine. func (w *writer) removeTmpFile() { os.Remove(w.f.Name()) } // SetSuccess sets the success variable to true. func (w *writer) SetSuccess() { w.success = true } // reader is used as a file reader. type reader struct { f *os.File io.Reader success bool keep bool } // errNoRegular indicates that a file is not regular. var errNoRegular = errors.New("no regular file") // specialBits contain the special bits, which are not supported by gxz. const specialBits = os.ModeSetuid | os.ModeSetgid | os.ModeSticky // openFile opens the given path with the given options. func openFile(path string, opts *options) (f *os.File, err error) { if path == "-" { return os.Stdin, nil } fi, err := os.Lstat(path) if err != nil { return nil, err } fm := fi.Mode() if !fm.IsRegular() { if !opts.force || fm&os.ModeSymlink == 0 { return nil, &userPathError{Path: path, Err: errNoRegular} } } if f, err = os.Open(path); err != nil { return nil, err } if fi, err = f.Stat(); err != nil { return nil, err } fm = fi.Mode() if !fm.IsRegular() { return nil, &userPathError{Path: path, Err: errNoRegular} } if fm&specialBits != 0 && !opts.force { return nil, &userPathError{Path: path, Err: errors.New("setuid, setgid and/or sticky bit set")} } return f, nil } var errInvalidFormat = errors.New("file format not recognized") // readerFormat tries to determine the type of a file. Currently it // checks for the XZ header magic and if it is not present assumes that // the file has been encoded by LZMA. The format field in options is // updated. func readerFormat(br *bufio.Reader, opts *options) (f *format, err error) { var ok bool if f, ok = formats[opts.format]; ok { if !f.validHeader(br) { return nil, errInvalidFormat } return f, nil } if opts.format != "auto" { return nil, fmt.Errorf("compression format %s not supported", opts.format) } for format, f := range formats { if f.validHeader(br) { opts.format = format return f, nil } } return nil, errInvalidFormat } // newDecompressor creates a new decompressor. func newDecompressor(br *bufio.Reader, opts *options) (dec io.Reader, err error) { if !opts.decompress { panic("no decompressor needed") } f, err := readerFormat(br, opts) if err != nil { return nil, err } if dec, err = f.newDecompressor(br, opts); err != nil { return nil, err } return dec, nil } // newReader creates a new reader for files. func newReader(path string, opts *options) (r *reader, err error) { f, err := openFile(path, opts) if err != nil { return nil, err } br := bufio.NewReader(f) if !opts.decompress { r = &reader{f: f, Reader: br, keep: opts.keep || opts.stdout} return r, nil } dec, err := newDecompressor(br, opts) if err != nil { return nil, &userPathError{path, err} } r = &reader{f: f, Reader: dec, keep: opts.keep || opts.stdout} return r, nil } // isStdin checks whether the given file reference is stdin. func isStdin(f *os.File) bool { return f.Fd() == uintptr(syscall.Stdin) } // Close closes the reader. The behavior can be influences by the // success attribute of reader. func (r *reader) Close() error { if r.f == nil { return errInval } defer func() { r.f = nil }() if isStdin(r.f) { return nil } if err := r.f.Close(); err != nil { return err } if r.keep || !r.success { return nil } if err := os.Remove(r.f.Name()); err != nil { return err } return nil } func (r *reader) SetSuccess() { r.success = true } func (r *reader) Perm() os.FileMode { const defaultPerm os.FileMode = 0666 fi, err := r.f.Stat() if err != nil { return defaultPerm } return fi.Mode() & defaultPerm } // userPathError represents a path error presentable to a user. In // difference to os.PathError it removes the information of the // operation returning the error. type userPathError struct { Path string Err error } // Error provides the error string for the path error. func (e *userPathError) Error() string { return e.Path + ": " + e.Err.Error() } // userError converts path error to an error message that is // acceptable for gxz users. PathError provides information about the // command that has created an error. For instance Lstat informs that // lstat detected that a file didn't exist this information is not // relevant for users of the gxz program. This function converts a // path error into a generic error removing the operation information. func userError(err error) error { pe, ok := err.(*os.PathError) if !ok { return err } return &userPathError{Path: pe.Path, Err: pe.Err} } func printErr(err error) { if err != nil { xlog.Warn(userError(err)) } } // processFile process the file with the given path applying the // provided options. func processFile(path string, opts *options) (err error) { r, err := newReader(path, opts) if err != nil { printErr(err) return } defer r.Close() w, err := newWriter(path, r.Perm(), opts) if err != nil { printErr(err) return } defer w.Close() quitSignalHandler := signalHandler(w) if _, err = io.Copy(w, r); err != nil { close(quitSignalHandler) printErr(err) return err } close(quitSignalHandler) w.SetSuccess() if err = w.Close(); err != nil { printErr(err) return err } r.SetSuccess() if err = r.Close(); err != nil { printErr(err) return err } return nil }