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  }