oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/internal/fs/tarfs/tarfs.go (about) 1 /* 2 Copyright The ORAS Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package tarfs 17 18 import ( 19 "archive/tar" 20 "errors" 21 "fmt" 22 "io" 23 "io/fs" 24 "os" 25 "path/filepath" 26 27 "oras.land/oras-go/v2/errdef" 28 ) 29 30 // blockSize is the size of each block in a tar archive. 31 const blockSize int64 = 512 32 33 // TarFS represents a file system (an fs.FS) based on a tar archive. 34 type TarFS struct { 35 path string 36 entries map[string]*entry 37 } 38 39 // entry represents an entry in a tar archive. 40 type entry struct { 41 header *tar.Header 42 pos int64 43 } 44 45 // New returns a file system (an fs.FS) for a tar archive located at path. 46 func New(path string) (*TarFS, error) { 47 pathAbs, err := filepath.Abs(path) 48 if err != nil { 49 return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", path, err) 50 } 51 tarfs := &TarFS{ 52 path: pathAbs, 53 entries: make(map[string]*entry), 54 } 55 if err := tarfs.indexEntries(); err != nil { 56 return nil, err 57 } 58 return tarfs, nil 59 } 60 61 // Open opens the named file. 62 // When Open returns an error, it should be of type *PathError 63 // with the Op field set to "open", the Path field set to name, 64 // and the Err field describing the problem. 65 // 66 // Open should reject attempts to open names that do not satisfy 67 // ValidPath(name), returning a *PathError with Err set to 68 // ErrInvalid or ErrNotExist. 69 func (tfs *TarFS) Open(name string) (file fs.File, openErr error) { 70 entry, err := tfs.getEntry(name) 71 if err != nil { 72 return nil, err 73 } 74 tarFile, err := os.Open(tfs.path) 75 if err != nil { 76 return nil, err 77 } 78 defer func() { 79 if openErr != nil { 80 tarFile.Close() 81 } 82 }() 83 84 if _, err := tarFile.Seek(entry.pos, io.SeekStart); err != nil { 85 return nil, err 86 } 87 tr := tar.NewReader(tarFile) 88 if _, err := tr.Next(); err != nil { 89 return nil, err 90 } 91 return &entryFile{ 92 Reader: tr, 93 Closer: tarFile, 94 header: entry.header, 95 }, nil 96 } 97 98 // Stat returns a FileInfo describing the file. 99 // If there is an error, it should be of type *PathError. 100 func (tfs *TarFS) Stat(name string) (fs.FileInfo, error) { 101 entry, err := tfs.getEntry(name) 102 if err != nil { 103 return nil, err 104 } 105 return entry.header.FileInfo(), nil 106 } 107 108 // getEntry returns the named entry. 109 func (tfs *TarFS) getEntry(name string) (*entry, error) { 110 if !fs.ValidPath(name) { 111 return nil, &fs.PathError{Path: name, Err: fs.ErrInvalid} 112 } 113 entry, ok := tfs.entries[name] 114 if !ok { 115 return nil, &fs.PathError{Path: name, Err: fs.ErrNotExist} 116 } 117 if entry.header.Typeflag != tar.TypeReg { 118 // support regular files only 119 return nil, fmt.Errorf("%s: type flag %c is not supported: %w", 120 name, entry.header.Typeflag, errdef.ErrUnsupported) 121 } 122 return entry, nil 123 } 124 125 // indexEntries index entries in the tar archive. 126 func (tfs *TarFS) indexEntries() error { 127 tarFile, err := os.Open(tfs.path) 128 if err != nil { 129 return err 130 } 131 defer tarFile.Close() 132 133 tr := tar.NewReader(tarFile) 134 for { 135 header, err := tr.Next() 136 if err != nil { 137 if errors.Is(err, io.EOF) { 138 break 139 } 140 return err 141 } 142 pos, err := tarFile.Seek(0, io.SeekCurrent) 143 if err != nil { 144 return err 145 } 146 tfs.entries[header.Name] = &entry{ 147 header: header, 148 pos: pos - blockSize, 149 } 150 } 151 152 return nil 153 } 154 155 // entryFile represents an entryFile in a tar archive and implements `fs.File`. 156 type entryFile struct { 157 io.Reader 158 io.Closer 159 header *tar.Header 160 } 161 162 // Stat returns a fs.FileInfo describing e. 163 func (e *entryFile) Stat() (fs.FileInfo, error) { 164 return e.header.FileInfo(), nil 165 }