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 }