github.com/mgoltzsche/ctnr@v0.7.1-alpha/pkg/fs/writer/tarwriter.go (about) 1 package writer 2 3 import ( 4 "archive/tar" 5 "io" 6 "os" 7 "path/filepath" 8 "syscall" 9 "time" 10 11 "github.com/mgoltzsche/ctnr/pkg/fs" 12 "github.com/pkg/errors" 13 ) 14 15 var _ fs.Writer = &TarWriter{} 16 17 // A mapping file system writer that secures root directory boundaries. 18 // Derived from umoci's tar_extract.go to allow separate source/dest interfaces 19 // and filter archive contents on extraction 20 type TarWriter struct { 21 writer *tar.Writer 22 written map[string]*fs.FileAttrs 23 } 24 25 func NewTarWriter(writer io.Writer) (w *TarWriter) { 26 return &TarWriter{tar.NewWriter(writer), map[string]*fs.FileAttrs{}} 27 } 28 29 func (w *TarWriter) Close() error { 30 return errors.Wrap(w.writer.Close(), "close tar writer") 31 } 32 33 func (w *TarWriter) Parent() error { return nil } 34 func (w *TarWriter) LowerNode(path, name string, a *fs.NodeAttrs) error { return nil } 35 func (w *TarWriter) LowerLink(path, target string, a *fs.NodeAttrs) error { return nil } 36 37 func (w *TarWriter) Lazy(path, name string, src fs.LazySource, written map[fs.Source]string) (err error) { 38 return errors.Errorf("refused to write lazy source %s into tar writer directly at %s since resulting tar could contain overridden entries. lazy sources must be resolved first.", src, path) 39 } 40 41 func (w *TarWriter) File(path string, src fs.FileSource) (r fs.Source, err error) { 42 a := src.Attrs() 43 if path, err = normalize(path); err != nil { 44 return 45 } 46 if err = w.writeTarHeader(path, a.FileAttrs); err != nil { 47 return 48 } 49 50 if a.NodeType != fs.TypeFile { 51 return src, nil 52 } 53 54 // Copy file 55 f, err := src.Reader() 56 if err != nil { 57 return 58 } 59 defer func() { 60 if e := f.Close(); e != nil && err == nil { 61 err = errors.Wrap(e, "write tar") 62 } 63 }() 64 n, err := io.Copy(w.writer, f) 65 if err != nil { 66 return nil, errors.Wrap(err, "write tar: file entry") 67 } 68 if n != a.Size { 69 return nil, errors.Wrap(io.ErrShortWrite, "write tar: file entry") 70 } 71 return src, nil 72 } 73 74 func (w *TarWriter) writeTarHeader(path string, a fs.FileAttrs) (err error) { 75 hdr, err := w.toTarHeader(path, a) 76 if err != nil { 77 return 78 } 79 err = w.writer.WriteHeader(hdr) 80 return errors.Wrapf(err, "write tar header for %q", path) 81 } 82 83 func (w *TarWriter) toTarHeader(path string, a fs.FileAttrs) (hdr *tar.Header, err error) { 84 a.Mtime = time.Unix(a.Mtime.Unix(), 0) // use floor(mtime) to preserve mtime which otherwise is not guaranteed due to rounding to seconds within tar 85 hdr, err = tar.FileInfoHeader(fs.NewFileInfo(path, &a), a.Symlink) 86 if err != nil { 87 return nil, errors.Wrapf(err, "to tar header: %s", path) 88 } 89 hdr.Uid = int(a.Uid) 90 hdr.Gid = int(a.Gid) 91 hdr.AccessTime = a.Atime 92 hdr.Xattrs = a.Xattrs 93 w.addWritten(path, &a) 94 return 95 } 96 97 // Taken from umoci 98 func normalize(path string) (string, error) { 99 path = filepath.Clean(string(os.PathSeparator) + path) 100 path, _ = filepath.Rel(string(os.PathSeparator), path) 101 path = filepath.Clean(path) 102 if !isValidPath(path) { 103 return "", errors.Errorf("tar writer: path outside tar root: %s", path) 104 } 105 return path, nil 106 } 107 108 func isValidPath(path string) bool { 109 prfx := string(os.PathSeparator) + "___" 110 return filepath.HasPrefix(filepath.Join(prfx, path), prfx) 111 } 112 113 func (w *TarWriter) addWritten(path string, a *fs.FileAttrs) { 114 w.written[path] = a 115 } 116 117 func (w *TarWriter) Link(path, target string) (err error) { 118 if path, err = normalize(path); err != nil { 119 return 120 } 121 if !filepath.IsAbs(target) { 122 target = filepath.Join(filepath.Dir(path), target) 123 } 124 if target, err = normalize(target); err != nil { 125 return errors.Wrap(err, "link") 126 } 127 128 a := w.written[target] 129 if a == nil { 130 return errors.Errorf("write tar: link entry %s: target %s does not exist", path, target) 131 } 132 hdr, err := w.toTarHeader(path, *a) 133 if err != nil { 134 return 135 } 136 hdr.Typeflag = tar.TypeLink 137 hdr.Linkname = target 138 hdr.Size = 0 139 140 err = w.writer.WriteHeader(hdr) 141 return errors.Wrap(err, "tar writer: write link") 142 } 143 144 func (w *TarWriter) Symlink(path string, a fs.FileAttrs) (err error) { 145 if path, err = normalize(path); err != nil { 146 return 147 } 148 149 a.Mode |= os.ModeSymlink | 0777 150 a.Symlink, err = normalizeLinkDest(path, a.Symlink) 151 if err != nil { 152 return 153 } 154 return w.writeTarHeader(path, a) 155 } 156 157 func (w *TarWriter) Fifo(path string, a fs.DeviceAttrs) (err error) { 158 a.Mode |= syscall.S_IFIFO 159 return w.device(path, &a) 160 } 161 162 func (w *TarWriter) device(path string, a *fs.DeviceAttrs) (err error) { 163 if path, err = normalize(path); err != nil { 164 return 165 } 166 hdr, err := w.toTarHeader(path, a.FileAttrs) 167 if err != nil { 168 return 169 } 170 hdr.Size = 0 171 hdr.Devmajor = a.Devmajor 172 hdr.Devminor = a.Devminor 173 err = w.writer.WriteHeader(hdr) 174 return errors.Wrap(err, "tar writer: write device") 175 } 176 177 func (w *TarWriter) Device(path string, a fs.DeviceAttrs) (err error) { 178 return w.device(path, &a) 179 } 180 181 func (w *TarWriter) Mkdir(path string) (err error) { 182 if path, err = normalize(path); err != nil { 183 return 184 } 185 return w.writeTarHeader(path+string(os.PathSeparator), fs.FileAttrs{Mode: os.ModeDir | 0755}) 186 } 187 188 func (w *TarWriter) Dir(path, base string, a fs.FileAttrs) (err error) { 189 if path, err = normalize(path); err != nil { 190 return 191 } 192 a.Mode |= os.ModeDir 193 return w.writeTarHeader(path+string(os.PathSeparator), a) 194 } 195 196 func (w *TarWriter) Remove(path string) (err error) { 197 if path, err = normalize(path); err != nil { 198 return 199 } 200 delete(w.written, path) 201 dir, file := filepath.Split(path) 202 file = fs.WhiteoutPrefix + file 203 now := time.Now() 204 // Using current time for header values which leads to unreproducable layer 205 // TODO: maybe change to fixed time instead of now() 206 return w.writeTarHeader(filepath.Join(dir, file), fs.FileAttrs{FileTimes: fs.FileTimes{Atime: now, Mtime: now}}) 207 }