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  }