blob: 16ab6f416158c665ef4516c7b6daefa1bded74ac [file]
// Copyright 2021, 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runfiles
import (
"io"
"io/fs"
"runtime"
"sort"
"strings"
"time"
)
// Open implements [fs.FS] for a [Runfiles] instance.
//
// Rlocation-style paths are supported with both apparent and canonical repo
// names. The root directory of the filesystem (".") additionally lists the
// apparent repo names that are visible to the current source repo
// (with --enable_bzlmod).
func (r *Runfiles) Open(name string) (fs.File, error) {
// Required by testfs.TestFS.
if !fs.ValidPath(name) || (runtime.GOOS == "windows" && strings.ContainsRune(name, '\\')) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
if name == "." {
return &rootDirFile{".", r, nil}, nil
}
repo, inRepoPath, hasInRepoPath := strings.Cut(name, "/")
key := repoMappingKey{r.sourceRepo, repo}
targetRepoDirectory, exists := r.repoMapping.Get(key)
if !exists {
// Either name uses a canonical repo name or refers to a root symlink.
// In both cases, we can just open the file directly.
return r.impl.open(name)
}
// Construct the path with the target repo name replaced by the canonical
// name.
mappedPath := targetRepoDirectory
if hasInRepoPath {
mappedPath += "/" + inRepoPath
}
f, err := r.impl.open(mappedPath)
if err != nil {
return nil, err
}
// The requested path is a child of a repo directory, return the unmodified
// file the implementation returned.
if hasInRepoPath {
return f, nil
}
// Return a special file for a repo dir that knows its apparent name.
return &renamedFile{f, repo}, nil
}
type rootDirFile struct {
dirFile
rf *Runfiles
entries []fs.DirEntry
}
func (r *rootDirFile) ReadDir(n int) ([]fs.DirEntry, error) {
if err := r.initEntries(); err != nil {
return nil, err
}
if n > 0 && len(r.entries) == 0 {
return nil, io.EOF
}
if n <= 0 || n > len(r.entries) {
n = len(r.entries)
}
entries := r.entries[:n]
r.entries = r.entries[n:]
return entries, nil
}
func (r *rootDirFile) initEntries() error {
if r.entries != nil {
return nil
}
// The entries of the root dir should be the apparent names of the repos
// visible to the main repo (plus root symlinks). We thus need to read
// the real entries and then transform and filter them.
canonicalToApparentName := make(map[string]string)
r.rf.repoMapping.ForEachVisible(r.rf.sourceRepo, func(targetApparentName, targetRepoDirectory string) {
canonicalToApparentName[targetRepoDirectory] = targetApparentName
})
rootFile, err := r.rf.impl.open(".")
if err != nil {
return err
}
realDirFile := rootFile.(fs.ReadDirFile)
realEntries, err := realDirFile.ReadDir(0)
if err != nil {
return err
}
for _, e := range realEntries {
r.entries = append(r.entries, e)
if apparent, ok := canonicalToApparentName[e.Name()]; ok && e.IsDir() && apparent != e.Name() {
// A repo directory that is visible to the current source repo is additionally
// materialized under its apparent name. We do not use a symlink as
// fs.WalkDir doesn't descend into symlinks.
r.entries = append(r.entries, renamedDirEntry{e, apparent})
}
}
sort.Slice(r.entries, func(i, j int) bool {
return r.entries[i].Name() < r.entries[j].Name()
})
return nil
}
type renamedFile struct {
fs.File
name string
}
func (r renamedFile) Stat() (fs.FileInfo, error) {
info, err := r.File.Stat()
if err != nil {
return nil, err
}
return renamedFileInfo{info, r.name}, nil
}
func (r renamedFile) ReadDir(n int) ([]fs.DirEntry, error) {
readDirFile, ok := r.File.(fs.ReadDirFile)
if !ok {
return nil, &fs.PathError{Op: "readdir", Path: r.name, Err: fs.ErrInvalid}
}
return readDirFile.ReadDir(n)
}
type renamedDirEntry struct {
fs.DirEntry
name string
}
func (r renamedDirEntry) Name() string { return r.name }
func (r renamedDirEntry) Info() (fs.FileInfo, error) {
info, err := r.DirEntry.Info()
if err != nil {
return nil, err
}
return renamedFileInfo{info, r.name}, nil
}
func (r renamedDirEntry) String() string { return fs.FormatDirEntry(r) }
type renamedFileInfo struct {
fs.FileInfo
name string
}
func (r renamedFileInfo) Name() string { return r.name }
func (r renamedFileInfo) String() string { return fs.FormatFileInfo(r) }
type emptyFile string
func (f emptyFile) Stat() (fs.FileInfo, error) { return emptyFileInfo(f), nil }
func (f emptyFile) Read([]byte) (int, error) { return 0, io.EOF }
func (emptyFile) Close() error { return nil }
type emptyFileInfo string
func (i emptyFileInfo) Name() string { return string(i) }
func (emptyFileInfo) Size() int64 { return 0 }
func (emptyFileInfo) Mode() fs.FileMode { return 0444 }
func (emptyFileInfo) ModTime() time.Time { return time.Time{} }
func (emptyFileInfo) IsDir() bool { return false }
func (emptyFileInfo) Sys() interface{} { return nil }
func (i emptyFileInfo) String() string { return fs.FormatFileInfo(i) }