github.com/openconfig/goyang@v1.4.5/pkg/yang/file.go (about) 1 // Copyright 2015 Google Inc. 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 package yang 16 17 import ( 18 "fmt" 19 "io/ioutil" 20 "os" 21 "path/filepath" 22 "regexp" 23 "sort" 24 "strings" 25 ) 26 27 var ( 28 // revisionDateSuffixRegex matches on the revision-date portion of a YANG 29 // file's name. 30 revisionDateSuffixRegex = regexp.MustCompile(`^@\d{4}-\d{2}-\d{2}\.yang$`) 31 ) 32 33 // PathsWithModules returns all paths under and including the 34 // root containing files with a ".yang" extension, as well as 35 // any error encountered 36 func PathsWithModules(root string) (paths []string, err error) { 37 pm := map[string]bool{} 38 filepath.Walk(root, func(p string, info os.FileInfo, e error) error { 39 err = e 40 if err == nil { 41 if info == nil { 42 return nil 43 } 44 if !info.IsDir() && strings.HasSuffix(p, ".yang") { 45 dir := filepath.Dir(p) 46 if !pm[dir] { 47 pm[dir] = true 48 paths = append(paths, dir) 49 } 50 } 51 return nil 52 } 53 return err 54 }) 55 return 56 } 57 58 // AddPath adds the directories specified in p, a colon separated list 59 // of directory names, to Path, if they are not already in Path. Using 60 // multiple arguments is also supported. 61 func (ms *Modules) AddPath(paths ...string) { 62 for _, path := range paths { 63 for _, p := range strings.Split(path, ":") { 64 if !ms.pathMap[p] { 65 ms.pathMap[p] = true 66 ms.Path = append(ms.Path, p) 67 } 68 } 69 } 70 } 71 72 // readFile makes testing of findFile easier. 73 var readFile = ioutil.ReadFile 74 75 // scanDir makes testing of findFile easier. 76 var scanDir = findInDir 77 78 // findFile returns the name and contents of the .yang file associated with 79 // name, or an error. If name is a module name rather than a file name (it does 80 // not have a .yang extension and there is no / in name), .yang is appended to 81 // the the name. The directory that the .yang file is found in is added to Path 82 // if not already in Path. If a file is not found by exact match, directories 83 // are scanned for "name@revision-date.yang" files, the latest (sorted by 84 // YYYY-MM-DD revision-date) of these will be selected. 85 // 86 // If a path has the form dir/... then dir and all direct or indirect 87 // subdirectories of dir are searched. 88 // 89 // The current directory (.) is always checked first, no matter the value of 90 // Path. 91 func (ms *Modules) findFile(name string) (string, string, error) { 92 slash := strings.Index(name, "/") 93 if slash < 0 && !strings.HasSuffix(name, ".yang") { 94 name += ".yang" 95 if best := scanDir(".", name, false); best != "" { 96 // we found a matching candidate in the local directory 97 name = best 98 } 99 } 100 101 switch data, err := readFile(name); true { 102 case err == nil: 103 ms.AddPath(filepath.Dir(name)) 104 return name, string(data), nil 105 case slash >= 0: 106 // If there are any /'s in the name then don't search Path. 107 return "", "", fmt.Errorf("no such file: %s", name) 108 } 109 110 for _, dir := range ms.Path { 111 var n string 112 if filepath.Base(dir) == "..." { 113 n = scanDir(filepath.Dir(dir), name, true) 114 } else { 115 n = scanDir(dir, name, false) 116 } 117 if n == "" { 118 continue 119 } 120 if data, err := readFile(n); err == nil { 121 return n, string(data), nil 122 } 123 } 124 return "", "", fmt.Errorf("no such file: %s", name) 125 } 126 127 // findInDir looks for a file named name in dir or any of its subdirectories if 128 // recurse is true. if recurse is false, scan only the directory dir. 129 // If no matching file is found, an empty string is returned. 130 // 131 // The file SHOULD have the following name, per 132 // https://tools.ietf.org/html/rfc7950#section-5.2: 133 // module-or-submodule-name ['@' revision-date] '.yang' 134 // where revision-date = 4DIGIT "-" 2DIGIT "-" 2DIGIT 135 // 136 // If a perfect name match is found, then that file's path is returned. 137 // Else if file(s) with otherwise matching names but which contain a 138 // revision-date pattern exactly matching the above are found, then path of the 139 // one with the latest date is returned. 140 func findInDir(dir, name string, recurse bool) string { 141 fis, err := ioutil.ReadDir(dir) 142 if err != nil { 143 return "" 144 } 145 146 var revisions []string 147 mname := strings.TrimSuffix(name, ".yang") 148 for _, fi := range fis { 149 switch { 150 case !fi.IsDir(): 151 if fn := fi.Name(); fn == name { 152 return filepath.Join(dir, name) 153 } else if strings.HasPrefix(fn, mname) && revisionDateSuffixRegex.MatchString(strings.TrimPrefix(fn, mname)) { 154 revisions = append(revisions, fn) 155 } 156 case recurse: 157 if n := findInDir(filepath.Join(dir, fi.Name()), name, recurse); n != "" { 158 return n 159 } 160 } 161 } 162 if len(revisions) == 0 { 163 return "" 164 } 165 sort.Strings(revisions) 166 return filepath.Join(dir, revisions[len(revisions)-1]) 167 }