github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/internal/pkg/inputprocessor/fs_case_insensitive.go (about)

     1  // Copyright 2023 Google LLC
     2  //
     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  //go:build windows || darwin
    16  
    17  package inputprocessor
    18  
    19  // Handle cache insensitive file-system.
    20  // On such file-system, filename "foo" and "Foo" are considered as the same
    21  // file, but remote-apis-sdks don't unity them. Thus, remote apis backend (RBE)
    22  // would fail with ExitCode:45 code=Invalid Argument, desc=failed to populate
    23  // working directory: failed to download inputs: already exists.
    24  //
    25  // To avoid such error, use the file name stored on the disk.
    26  // b/171018900
    27  
    28  import (
    29  	"fmt"
    30  	"os"
    31  	"path/filepath"
    32  	"strings"
    33  	"sync"
    34  
    35  	log "github.com/golang/glog"
    36  )
    37  
    38  type dirEntCache struct {
    39  	mu sync.Mutex
    40  	m  map[string]*dirEnt
    41  }
    42  
    43  // process global cache.
    44  var dirCache = dirEntCache{
    45  	m: make(map[string]*dirEnt),
    46  }
    47  
    48  type dirEnt struct {
    49  	dir     os.FileInfo
    50  	entries []os.FileInfo
    51  }
    52  
    53  // get gets dirEnt of dir.
    54  func (c *dirEntCache) get(dir string) *dirEnt {
    55  	c.mu.Lock()
    56  	defer c.mu.Unlock()
    57  	if c.m == nil {
    58  		c.m = make(map[string]*dirEnt)
    59  	}
    60  	de := c.m[dir]
    61  	if de == nil {
    62  		de = &dirEnt{}
    63  		c.m[dir] = de
    64  	}
    65  	de.update(dir)
    66  	return de.clone()
    67  }
    68  
    69  // update updates dirEnt for dir.
    70  // it checks dir is updated since last update, and if so, update entries.
    71  func (de *dirEnt) update(dir string) {
    72  	fi, err := os.Stat(dir)
    73  	if err != nil {
    74  		log.Errorf("stat %q: %v", dir, err)
    75  		return
    76  	}
    77  	if de.dir != nil && os.SameFile(fi, de.dir) && fi.ModTime().Equal(de.dir.ModTime()) {
    78  		// not changed. no need to update
    79  		return
    80  	}
    81  	// dir is new, or has been changed?
    82  	f, err := os.Open(dir)
    83  	if err != nil {
    84  		log.Errorf("open %q: %v", dir, err)
    85  		return
    86  	}
    87  	defer f.Close()
    88  	entries, err := f.Readdir(-1)
    89  	if err != nil {
    90  		log.Errorf("readdir %q: %v", dir, err)
    91  	}
    92  	de.dir = fi
    93  	de.entries = entries
    94  }
    95  
    96  func (de *dirEnt) clone() *dirEnt {
    97  	nde := &dirEnt{
    98  		dir:     de.dir,
    99  		entries: make([]os.FileInfo, len(de.entries)),
   100  	}
   101  	copy(nde.entries, de.entries)
   102  	return nde
   103  }
   104  
   105  type pathNormalizer interface {
   106  	normalize(execRoot, pathname string) (string, error)
   107  }
   108  
   109  type pathNormalizerNative struct {
   110  	// m keeps normalized filename for a filename.
   111  	// key is filename used in inputprocessor.
   112  	// value is filename stored in the disk for the key's filename.
   113  	// filename is relative to execRoot.
   114  	m map[string]string
   115  
   116  	// dirs keeps *dirEnt fo a directory.
   117  	// key is directory name.
   118  	// value is *dirEnt for the directory.
   119  	dirs map[string]*dirEnt
   120  }
   121  
   122  func newPathNormalizer(cross bool) pathNormalizer {
   123  	if cross {
   124  		return pathNormalizerCross{
   125  			dirs: make(map[string]string),
   126  		}
   127  	}
   128  	return pathNormalizerNative{
   129  		m:    make(map[string]string),
   130  		dirs: make(map[string]*dirEnt),
   131  	}
   132  }
   133  
   134  func (p pathNormalizerNative) normalize(execRoot, pathname string) (string, error) {
   135  	segs := strings.Split(filepath.Clean(pathname), string(filepath.Separator))
   136  	var nsegs []string
   137  	var pathBuilder strings.Builder
   138  loop:
   139  	for _, seg := range segs {
   140  		if seg == "" {
   141  			// ignore empty seg. e.g. "//path/name".
   142  			// http://b/170593203 http://b/171203933
   143  			continue
   144  		}
   145  		dir := pathBuilder.String()
   146  		if pathBuilder.Len() > 0 {
   147  			pathBuilder.WriteByte(filepath.Separator)
   148  		}
   149  		fmt.Fprintf(&pathBuilder, seg)
   150  		pathname := pathBuilder.String()
   151  		s, ok := p.m[pathname]
   152  		if ok {
   153  			// actual name of pathname's base on disk is known to be `s`.
   154  			nsegs = append(nsegs, s)
   155  			continue loop
   156  		}
   157  		absDir := filepath.Join(execRoot, dir)
   158  		de, ok := p.dirs[absDir]
   159  		if !ok {
   160  			// first visit to dir.
   161  			de = dirCache.get(absDir)
   162  			p.dirs[pathname] = de
   163  			// populate actual name of pathname on disk in `p.m`.
   164  			for _, ent := range de.entries {
   165  				canonicalPathname := filepath.Join(dir, ent.Name())
   166  				p.m[canonicalPathname] = ent.Name()
   167  			}
   168  		}
   169  		// check again if we can find it in `p.m` by updating with `de`.
   170  		s, ok = p.m[pathname]
   171  		if ok {
   172  			nsegs = append(nsegs, s)
   173  			continue loop
   174  		}
   175  		// it is not the same name on the disk.
   176  		fi, err := os.Stat(filepath.Join(execRoot, pathname))
   177  		if err != nil {
   178  			return "", fmt.Errorf("stat %q: %v", pathname, err)
   179  		}
   180  		// find the same file and use the name on the disk.
   181  		for _, ent := range de.entries {
   182  			if os.SameFile(fi, ent) {
   183  				p.m[pathname] = ent.Name()
   184  				nsegs = append(nsegs, ent.Name())
   185  				continue loop
   186  			}
   187  		}
   188  		// not found on the filesystem? use given name as is.
   189  		nsegs = append(nsegs, seg)
   190  	}
   191  	return filepath.Join(nsegs...), nil
   192  }
   193  
   194  type pathNormalizerCross struct {
   195  	dirs map[string]string // normalized -> dir name to use
   196  }
   197  
   198  func (p pathNormalizerCross) normalize(execRoot, pathname string) (string, error) {
   199  	f := strings.TrimLeft(filepath.Clean(pathname), string(filepath.Separator))
   200  	_, err := os.Stat(filepath.Join(execRoot, f))
   201  	if err != nil {
   202  		return f, err
   203  	}
   204  	dir, base := filepath.Split(f)
   205  	// workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=1207754
   206  	key := strings.ToLower(dir)
   207  	d, ok := p.dirs[key]
   208  	if ok {
   209  		dir = d
   210  	} else {
   211  		p.dirs[key] = dir
   212  	}
   213  	return filepath.Join(dir, base), err
   214  }