// Copyright 2015 Google Inc. All rights reserved // // 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 // // http://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 kati import ( "bytes" "errors" "fmt" "io" "os" "path/filepath" "strconv" "strings" "sync" "syscall" "github.com/golang/glog" ) type fileid struct { dev, ino uint64 } var ( unknownFileid = fileid{} invalidFileid = fileid{dev: 1<<64 - 1, ino: 1<<64 - 1} ) type dirent struct { id fileid name string lmode os.FileMode mode os.FileMode // add other fields to support more find commands? } type fsCacheT struct { mu sync.Mutex ids map[string]fileid dirents map[fileid][]dirent } var fsCache = &fsCacheT{ ids: make(map[string]fileid), dirents: map[fileid][]dirent{ invalidFileid: nil, }, } func init() { fsCache.readdir(".", unknownFileid) } func (c *fsCacheT) dirs() int { c.mu.Lock() defer c.mu.Unlock() return len(c.dirents) } func (c *fsCacheT) files() int { c.mu.Lock() defer c.mu.Unlock() n := 0 for _, ents := range c.dirents { n += len(ents) } return n } func hasWildcardMeta(pat string) bool { return strings.IndexAny(pat, "*?[") >= 0 } func hasWildcardMetaByte(pat []byte) bool { return bytes.IndexAny(pat, "*?[") >= 0 } func wildcardUnescape(pat string) string { var buf bytes.Buffer for i := 0; i < len(pat); i++ { if pat[i] == '\\' && i+1 < len(pat) { switch pat[i+1] { case '*', '?', '[', '\\': buf.WriteByte(pat[i]) } continue } buf.WriteByte(pat[i]) } return buf.String() } func filepathJoin(names ...string) string { var dir string for i, n := range names { dir += n if i != len(names)-1 && n != "" && n[len(n)-1] != '/' { dir += "/" } } return dir } func filepathClean(path string) string { var names []string if filepath.IsAbs(path) { names = append(names, "") } paths := strings.Split(path, string(filepath.Separator)) Loop: for _, n := range paths { if n == "" || n == "." { continue Loop } if n == ".." && len(names) > 0 { dir, last := names[:len(names)-1], names[len(names)-1] parent := strings.Join(dir, string(filepath.Separator)) if parent == "" { parent = "." } _, ents := fsCache.readdir(parent, unknownFileid) for _, e := range ents { if e.name != last { continue } if e.lmode&os.ModeSymlink == os.ModeSymlink && e.mode&os.ModeDir == os.ModeDir { // preserve .. if last is symlink dir. names = append(names, "..") continue Loop } // last is not symlink, maybe safe to clean. names = names[:len(names)-1] continue Loop } // parent doesn't exists? preserve .. names = append(names, "..") continue Loop } names = append(names, n) } if len(names) == 0 { return "." } return strings.Join(names, string(filepath.Separator)) } func (c *fsCacheT) fileid(dir string) fileid { c.mu.Lock() id := c.ids[dir] c.mu.Unlock() return id } func (c *fsCacheT) readdir(dir string, id fileid) (fileid, []dirent) { glog.V(3).Infof("readdir: %s [%v]", dir, id) c.mu.Lock() if id == unknownFileid { id = c.ids[dir] } ents, ok := c.dirents[id] c.mu.Unlock() if ok { return id, ents } glog.V(3).Infof("opendir: %s", dir) d, err := os.Open(dir) if err != nil { c.mu.Lock() c.ids[dir] = invalidFileid c.mu.Unlock() return invalidFileid, nil } defer d.Close() fi, err := d.Stat() if err != nil { c.mu.Lock() c.ids[dir] = invalidFileid c.mu.Unlock() return invalidFileid, nil } if stat, ok := fi.Sys().(*syscall.Stat_t); ok { id = fileid{dev: uint64(stat.Dev), ino: stat.Ino} } names, _ := d.Readdirnames(-1) // need sort? ents = nil var path string for _, name := range names { path = filepath.Join(dir, name) fi, err := os.Lstat(path) if err != nil { glog.Warningf("readdir %s: %v", name, err) ents = append(ents, dirent{name: name}) continue } lmode := fi.Mode() mode := lmode var id fileid if stat, ok := fi.Sys().(*syscall.Stat_t); ok { id = fileid{dev: uint64(stat.Dev), ino: stat.Ino} } if lmode&os.ModeSymlink == os.ModeSymlink { fi, err = os.Stat(path) if err != nil { glog.Warningf("readdir %s: %v", name, err) } else { mode = fi.Mode() if stat, ok := fi.Sys().(*syscall.Stat_t); ok { id = fileid{dev: uint64(stat.Dev), ino: stat.Ino} } } } ents = append(ents, dirent{id: id, name: name, lmode: lmode, mode: mode}) } glog.V(3).Infof("readdir:%s => %v: %v", dir, id, ents) c.mu.Lock() c.ids[dir] = id c.dirents[id] = ents c.mu.Unlock() return id, ents } // glob searches for files matching pattern in the directory dir // and appends them to matches. ignore I/O errors. func (c *fsCacheT) glob(dir, pattern string, matches []string) ([]string, error) { _, ents := c.readdir(filepathClean(dir), unknownFileid) switch dir { case "", string(filepath.Separator): // nothing default: dir += string(filepath.Separator) // add trailing separator back } for _, ent := range ents { matched, err := filepath.Match(pattern, ent.name) if err != nil { return nil, err } if matched { matches = append(matches, dir+ent.name) } } return matches, nil } func (c *fsCacheT) Glob(pat string) ([]string, error) { // TODO(ukai): expand ~ to user's home directory. // TODO(ukai): use find cache for glob if exists // or use wildcardCache for find cache. pat = wildcardUnescape(pat) dir, file := filepath.Split(pat) switch dir { case "", string(filepath.Separator): // nothing default: dir = dir[:len(dir)-1] // chop off trailing separator } if !hasWildcardMeta(dir) { return c.glob(dir, file, nil) } m, err := c.Glob(dir) if err != nil { return nil, err } var matches []string for _, d := range m { matches, err = c.glob(d, file, matches) if err != nil { return nil, err } } return matches, nil } func wildcard(w evalWriter, pat string) error { files, err := fsCache.Glob(pat) if err != nil { return err } for _, file := range files { w.writeWordString(file) } return nil } type findOp interface { apply(evalWriter, string, dirent) (test bool, prune bool) } type findOpName string func (op findOpName) apply(w evalWriter, path string, ent dirent) (bool, bool) { matched, err := filepath.Match(string(op), ent.name) if err != nil { glog.Warningf("find -name %q: %v", string(op), err) return false, false } return matched, false } type findOpType struct { mode os.FileMode followSymlinks bool } func (op findOpType) apply(w evalWriter, path string, ent dirent) (bool, bool) { mode := ent.lmode if op.followSymlinks && ent.mode != 0 { mode = ent.mode } return op.mode&mode == op.mode, false } type findOpRegular struct { followSymlinks bool } func (op findOpRegular) apply(w evalWriter, path string, ent dirent) (bool, bool) { mode := ent.lmode if op.followSymlinks && ent.mode != 0 { mode = ent.mode } return mode.IsRegular(), false } type findOpNot struct { op findOp } func (op findOpNot) apply(w evalWriter, path string, ent dirent) (bool, bool) { test, prune := op.op.apply(w, path, ent) return !test, prune } type findOpAnd []findOp func (op findOpAnd) apply(w evalWriter, path string, ent dirent) (bool, bool) { var prune bool for _, o := range op { test, p := o.apply(w, path, ent) if p { prune = true } if !test { return test, prune } } return true, prune } type findOpOr struct { op1, op2 findOp } func (op findOpOr) apply(w evalWriter, path string, ent dirent) (bool, bool) { test, prune := op.op1.apply(w, path, ent) if test { return test, prune } return op.op2.apply(w, path, ent) } type findOpPrune struct{} func (op findOpPrune) apply(w evalWriter, path string, ent dirent) (bool, bool) { return true, true } type findOpPrint struct{} func (op findOpPrint) apply(w evalWriter, path string, ent dirent) (bool, bool) { var name string if path == "" { name = ent.name } else if ent.name == "." { name = path } else { name = filepathJoin(path, ent.name) } glog.V(3).Infof("find print: %s", name) w.writeWordString(name) return true, false } func (c *fsCacheT) find(w evalWriter, fc findCommand, path string, id fileid, depth int, seen map[fileid]string) { glog.V(2).Infof("find: path:%s id:%v depth:%d", path, id, depth) id, ents := c.readdir(filepathClean(filepathJoin(fc.chdir, path)), id) if ents == nil { glog.V(1).Infof("find: %s %s not found", fc.chdir, path) return } for _, ent := range ents { glog.V(3).Infof("find: path:%s ent:%s depth:%d", path, ent.name, depth) _, prune := fc.apply(w, path, ent) mode := ent.lmode if fc.followSymlinks { if mode&os.ModeSymlink == os.ModeSymlink { lpath := filepathJoin(path, ent.name) if p, ok := seen[ent.id]; ok { // stderr? glog.Errorf("find: File system loop detected; `%s' is part of the same file system loop as `%s'.", lpath, p) return } seen[ent.id] = lpath } mode = ent.mode } if !mode.IsDir() { glog.V(3).Infof("find: not dir: %s/%s", path, ent.name) continue } if prune { glog.V(3).Infof("find: prune: %s", path) continue } if depth >= fc.depth { glog.V(3).Infof("find: depth: %d >= %d", depth, fc.depth) continue } c.find(w, fc, filepathJoin(path, ent.name), ent.id, depth+1, seen) } } type findCommand struct { testdir string // before chdir chdir string finddirs []string // after chdir followSymlinks bool ops []findOp depth int } func parseFindCommand(cmd string) (findCommand, error) { if !strings.Contains(cmd, "find") { return findCommand{}, errNotFind } fcp := findCommandParser{ shellParser: shellParser{ cmd: cmd, }, } err := fcp.parse() if err != nil { return fcp.fc, err } if len(fcp.fc.finddirs) == 0 { fcp.fc.finddirs = append(fcp.fc.finddirs, ".") } if fcp.fc.chdir != "" { fcp.fc.chdir = filepathClean(fcp.fc.chdir) } if filepath.IsAbs(fcp.fc.chdir) { return fcp.fc, errFindAbspath } for _, dir := range fcp.fc.finddirs { if filepath.IsAbs(dir) { return fcp.fc, errFindAbspath } } glog.V(3).Infof("find command: %#v", fcp.fc) // TODO(ukai): handle this in run() instead of fallback shell. _, ents := fsCache.readdir(filepathClean(fcp.fc.testdir), unknownFileid) if ents == nil { glog.V(1).Infof("find: testdir %s - not dir", fcp.fc.testdir) return fcp.fc, errFindNoSuchDir } _, ents = fsCache.readdir(filepathClean(fcp.fc.chdir), unknownFileid) if ents == nil { glog.V(1).Infof("find: cd %s: No such file or directory", fcp.fc.chdir) return fcp.fc, errFindNoSuchDir } return fcp.fc, nil } func (fc findCommand) run(w evalWriter) { glog.V(3).Infof("find: %#v", fc) for _, dir := range fc.finddirs { seen := make(map[fileid]string) id, _ := fsCache.readdir(filepathClean(filepathJoin(fc.chdir, dir)), unknownFileid) _, prune := fc.apply(w, dir, dirent{id: id, name: ".", mode: os.ModeDir, lmode: os.ModeDir}) if prune { glog.V(3).Infof("find: prune: %s", dir) continue } if 0 >= fc.depth { glog.V(3).Infof("find: depth: 0 >= %d", fc.depth) continue } fsCache.find(w, fc, dir, id, 1, seen) } } func (fc findCommand) apply(w evalWriter, path string, ent dirent) (test, prune bool) { var p bool for _, op := range fc.ops { test, p = op.apply(w, path, ent) if p { prune = true } if !test { break } } glog.V(2).Infof("apply path:%s ent:%v => test=%t, prune=%t", path, ent, test, prune) return test, prune } var ( errNotFind = errors.New("not find command") errFindBackground = errors.New("find command: background") errFindUnbalancedQuote = errors.New("find command: unbalanced quote") errFindDupChdir = errors.New("find command: dup chdir") errFindDupTestdir = errors.New("find command: dup testdir") errFindExtra = errors.New("find command: extra") errFindUnexpectedEnd = errors.New("find command: unexpected end") errFindAbspath = errors.New("find command: abs path") errFindNoSuchDir = errors.New("find command: no such dir") ) type findCommandParser struct { fc findCommand shellParser } func (p *findCommandParser) parse() error { p.fc.depth = 1<<31 - 1 // max int32 var hasIf bool var hasFind bool for { tok, err := p.token() if err == io.EOF || tok == "" { if !hasFind { return errNotFind } return nil } if err != nil { return err } switch tok { case "cd": if p.fc.chdir != "" { return errFindDupChdir } p.fc.chdir, err = p.token() if err != nil { return err } err = p.expect(";", "&&") if err != nil { return err } case "if": err = p.expect("[") if err != nil { return err } if hasIf { return errFindDupTestdir } err = p.parseTest() if err != nil { return err } err = p.expectSeq("]", ";", "then") if err != nil { return err } hasIf = true case "test": if hasIf { return errFindDupTestdir } err = p.parseTest() if err != nil { return err } err = p.expect("&&") if err != nil { return err } case "find": err = p.parseFind() if err != nil { return err } if hasIf { err = p.expect("fi") if err != nil { return err } } tok, err = p.token() if err != io.EOF || tok != "" { return errFindExtra } hasFind = true return nil } } } func (p *findCommandParser) parseTest() error { if p.fc.testdir != "" { return errFindDupTestdir } err := p.expect("-d") if err != nil { return err } p.fc.testdir, err = p.token() return err } func (p *findCommandParser) parseFind() error { for { tok, err := p.token() if err == io.EOF || tok == "" || tok == ";" { var print findOpPrint if len(p.fc.ops) == 0 || p.fc.ops[len(p.fc.ops)-1] != print { p.fc.ops = append(p.fc.ops, print) } return nil } if err != nil { return err } if tok != "" && (tok[0] == '-' || tok == "\\(") { p.unget(tok) op, err := p.parseFindCond() if err != nil { return err } if op != nil { p.fc.ops = append(p.fc.ops, op) } continue } p.fc.finddirs = append(p.fc.finddirs, tok) } } func (p *findCommandParser) parseFindCond() (findOp, error) { return p.parseExpr() } func (p *findCommandParser) parseExpr() (findOp, error) { op, err := p.parseTerm() if err != nil { return nil, err } if op == nil { return nil, nil } for { tok, err := p.token() if err == io.EOF || tok == "" { return op, nil } if err != nil { return nil, err } if tok != "-or" && tok != "-o" { p.unget(tok) return op, nil } op2, err := p.parseTerm() if err != nil { return nil, err } op = findOpOr{op, op2} } } func (p *findCommandParser) parseTerm() (findOp, error) { op, err := p.parseFact() if err != nil { return nil, err } if op == nil { return nil, nil } var ops []findOp ops = append(ops, op) for { tok, err := p.token() if err == io.EOF || tok == "" { if len(ops) == 1 { return ops[0], nil } return findOpAnd(ops), nil } if err != nil { return nil, err } if tok != "-and" && tok != "-a" { p.unget(tok) } op, err = p.parseFact() if err != nil { return nil, err } if op == nil { if len(ops) == 1 { return ops[0], nil } return findOpAnd(ops), nil } ops = append(ops, op) // findAndOp? } } func (p *findCommandParser) parseFact() (findOp, error) { tok, err := p.token() if err != nil { return nil, err } switch tok { case "-L": p.fc.followSymlinks = true return nil, nil case "-prune": return findOpPrune{}, nil case "-print": return findOpPrint{}, nil case "-maxdepth": tok, err = p.token() if err != nil { return nil, err } i, err := strconv.ParseInt(tok, 10, 32) if err != nil { return nil, err } if i < 0 { return nil, fmt.Errorf("find commnad: -maxdepth negative: %d", i) } p.fc.depth = int(i) return nil, nil case "-not", "\\!": op, err := p.parseFact() if err != nil { return nil, err } return findOpNot{op}, nil case "\\(": op, err := p.parseExpr() if err != nil { return nil, err } err = p.expect("\\)") if err != nil { return nil, err } return op, nil case "-name": tok, err = p.token() if err != nil { return nil, err } return findOpName(tok), nil case "-type": tok, err = p.token() if err != nil { return nil, err } var m os.FileMode switch tok { case "b": m = os.ModeDevice case "c": m = os.ModeDevice | os.ModeCharDevice case "d": m = os.ModeDir case "p": m = os.ModeNamedPipe case "l": m = os.ModeSymlink case "f": return findOpRegular{p.fc.followSymlinks}, nil case "s": m = os.ModeSocket default: return nil, fmt.Errorf("find command: unsupported -type %s", tok) } return findOpType{m, p.fc.followSymlinks}, nil case "-o", "-or", "-a", "-and": p.unget(tok) return nil, nil default: if tok != "" && tok[0] == '-' { return nil, fmt.Errorf("find command: unsupported %s", tok) } p.unget(tok) return nil, nil } } type findleavesCommand struct { name string dirs []string prunes []string mindepth int } func parseFindleavesCommand(cmd string) (findleavesCommand, error) { if !strings.Contains(cmd, "build/tools/findleaves.py") { return findleavesCommand{}, errNotFindleaves } fcp := findleavesCommandParser{ shellParser: shellParser{ cmd: cmd, }, } err := fcp.parse() if err != nil { return fcp.fc, err } glog.V(3).Infof("findleaves command: %#v", fcp.fc) return fcp.fc, nil } func (fc findleavesCommand) run(w evalWriter) { glog.V(3).Infof("findleaves: %#v", fc) for _, dir := range fc.dirs { seen := make(map[fileid]string) id, _ := fsCache.readdir(filepathClean(dir), unknownFileid) fc.walk(w, dir, id, 1, seen) } } func (fc findleavesCommand) walk(w evalWriter, dir string, id fileid, depth int, seen map[fileid]string) { glog.V(3).Infof("findleaves walk: dir:%d id:%v depth:%d", dir, id, depth) id, ents := fsCache.readdir(filepathClean(dir), id) var subdirs []dirent for _, ent := range ents { if ent.mode.IsDir() { if fc.isPrune(ent.name) { glog.V(3).Infof("findleaves prune %s in %s", ent.name, dir) continue } subdirs = append(subdirs, ent) continue } if depth < fc.mindepth { glog.V(3).Infof("findleaves depth=%d mindepth=%d", depth, fc.mindepth) continue } if ent.name == fc.name { glog.V(2).Infof("findleaves %s in %s", ent.name, dir) w.writeWordString(filepathJoin(dir, ent.name)) // no recurse subdirs return } } for _, subdir := range subdirs { if subdir.lmode&os.ModeSymlink == os.ModeSymlink { lpath := filepathJoin(dir, subdir.name) if p, ok := seen[subdir.id]; ok { // symlink loop detected. glog.Errorf("findleaves: loop detected %q was %q", lpath, p) continue } seen[subdir.id] = lpath } fc.walk(w, filepathJoin(dir, subdir.name), subdir.id, depth+1, seen) } } func (fc findleavesCommand) isPrune(name string) bool { for _, p := range fc.prunes { if p == name { return true } } return false } var ( errNotFindleaves = errors.New("not findleaves command") errFindleavesEmptyPrune = errors.New("findleaves: empty prune") errFindleavesNoFilename = errors.New("findleaves: no filename") ) type findleavesCommandParser struct { fc findleavesCommand shellParser } func (p *findleavesCommandParser) parse() error { var args []string p.fc.mindepth = -1 tok, err := p.token() if err != nil { return err } if tok != "build/tools/findleaves.py" { return errNotFindleaves } for { tok, err := p.token() if err == io.EOF || tok == "" { break } if err != nil { return err } switch { case strings.HasPrefix(tok, "--prune="): prune := filepath.Base(strings.TrimPrefix(tok, "--prune=")) if prune == "" { return errFindleavesEmptyPrune } p.fc.prunes = append(p.fc.prunes, prune) case strings.HasPrefix(tok, "--mindepth="): md := strings.TrimPrefix(tok, "--mindepth=") i, err := strconv.ParseInt(md, 10, 32) if err != nil { return err } p.fc.mindepth = int(i) default: args = append(args, tok) } } if len(args) < 2 { return errFindleavesNoFilename } p.fc.dirs, p.fc.name = args[:len(args)-1], args[len(args)-1] return nil }