1 // Copyright 2015 go-swagger maintainers 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 spec 16 17 import ( 18 "net/url" 19 "path" 20 "strings" 21 ) 22 23 const fileScheme = "file" 24 25 // normalizeURI ensures that all $ref paths used internally by the expander are canonicalized. 26 // 27 // NOTE(windows): there is a tolerance over the strict URI format on windows. 28 // 29 // The normalizer accepts relative file URLs like 'Path\File.JSON' as well as absolute file URLs like 30 // 'C:\Path\file.Yaml'. 31 // 32 // Both are canonicalized with a "file://" scheme, slashes and a lower-cased path: 33 // 'file:///c:/path/file.yaml' 34 // 35 // URLs can be specified with a file scheme, like in 'file:///folder/file.json' or 36 // 'file:///c:\folder\File.json'. 37 // 38 // URLs like file://C:\folder are considered invalid (i.e. there is no host 'c:\folder') and a "repair" 39 // is attempted. 40 // 41 // The base path argument is assumed to be canonicalized (e.g. using normalizeBase()). 42 func normalizeURI(refPath, base string) string { 43 refURL, err := parseURL(refPath) 44 if err != nil { 45 specLogger.Printf("warning: invalid URI in $ref %q: %v", refPath, err) 46 refURL, refPath = repairURI(refPath) 47 } 48 49 fixWindowsURI(refURL, refPath) // noop on non-windows OS 50 51 refURL.Path = path.Clean(refURL.Path) 52 if refURL.Path == "." { 53 refURL.Path = "" 54 } 55 56 r := MustCreateRef(refURL.String()) 57 if r.IsCanonical() { 58 return refURL.String() 59 } 60 61 baseURL, _ := parseURL(base) 62 if path.IsAbs(refURL.Path) { 63 baseURL.Path = refURL.Path 64 } else if refURL.Path != "" { 65 baseURL.Path = path.Join(path.Dir(baseURL.Path), refURL.Path) 66 } 67 // copying fragment from ref to base 68 baseURL.Fragment = refURL.Fragment 69 70 return baseURL.String() 71 } 72 73 // denormalizeRef returns the simplest notation for a normalized $ref, given the path of the original root document. 74 // 75 // When calling this, we assume that: 76 // * $ref is a canonical URI 77 // * originalRelativeBase is a canonical URI 78 // 79 // denormalizeRef is currently used when we rewrite a $ref after a circular $ref has been detected. 80 // In this case, expansion stops and normally renders the internal canonical $ref. 81 // 82 // This internal $ref is eventually rebased to the original RelativeBase used for the expansion. 83 // 84 // There is a special case for schemas that are anchored with an "id": 85 // in that case, the rebasing is performed // against the id only if this is an anchor for the initial root document. 86 // All other intermediate "id"'s found along the way are ignored for the purpose of rebasing. 87 func denormalizeRef(ref *Ref, originalRelativeBase, id string) Ref { 88 debugLog("denormalizeRef called:\n$ref: %q\noriginal: %s\nroot ID:%s", ref.String(), originalRelativeBase, id) 89 90 if ref.String() == "" || ref.IsRoot() || ref.HasFragmentOnly { 91 // short circuit: $ref to current doc 92 return *ref 93 } 94 95 if id != "" { 96 idBaseURL, err := parseURL(id) 97 if err == nil { // if the schema id is not usable as a URI, ignore it 98 if ref, ok := rebase(ref, idBaseURL, true); ok { // rebase, but keep references to root unchaged (do not want $ref: "") 99 // $ref relative to the ID of the schema in the root document 100 return ref 101 } 102 } 103 } 104 105 originalRelativeBaseURL, _ := parseURL(originalRelativeBase) 106 107 r, _ := rebase(ref, originalRelativeBaseURL, false) 108 109 return r 110 } 111 112 func rebase(ref *Ref, v *url.URL, notEqual bool) (Ref, bool) { 113 var newBase url.URL 114 115 u := ref.GetURL() 116 117 if u.Scheme != v.Scheme || u.Host != v.Host { 118 return *ref, false 119 } 120 121 docPath := v.Path 122 v.Path = path.Dir(v.Path) 123 124 if v.Path == "." { 125 v.Path = "" 126 } else if !strings.HasSuffix(v.Path, "/") { 127 v.Path += "/" 128 } 129 130 newBase.Fragment = u.Fragment 131 132 if strings.HasPrefix(u.Path, docPath) { 133 newBase.Path = strings.TrimPrefix(u.Path, docPath) 134 } else { 135 newBase.Path = strings.TrimPrefix(u.Path, v.Path) 136 } 137 138 if notEqual && newBase.Path == "" && newBase.Fragment == "" { 139 // do not want rebasing to end up in an empty $ref 140 return *ref, false 141 } 142 143 if path.IsAbs(newBase.Path) { 144 // whenever we end up with an absolute path, specify the scheme and host 145 newBase.Scheme = v.Scheme 146 newBase.Host = v.Host 147 } 148 149 return MustCreateRef(newBase.String()), true 150 } 151 152 // normalizeRef canonicalize a Ref, using a canonical relativeBase as its absolute anchor 153 func normalizeRef(ref *Ref, relativeBase string) *Ref { 154 r := MustCreateRef(normalizeURI(ref.String(), relativeBase)) 155 return &r 156 } 157 158 // normalizeBase performs a normalization of the input base path. 159 // 160 // This always yields a canonical URI (absolute), usable for the document cache. 161 // 162 // It ensures that all further internal work on basePath may safely assume 163 // a non-empty, cross-platform, canonical URI (i.e. absolute). 164 // 165 // This normalization tolerates windows paths (e.g. C:\x\y\File.dat) and transform this 166 // in a file:// URL with lower cased drive letter and path. 167 // 168 // See also: https://en.wikipedia.org/wiki/File_URI_scheme 169 func normalizeBase(in string) string { 170 u, err := parseURL(in) 171 if err != nil { 172 specLogger.Printf("warning: invalid URI in RelativeBase %q: %v", in, err) 173 u, in = repairURI(in) 174 } 175 176 u.Fragment = "" // any fragment in the base is irrelevant 177 178 fixWindowsURI(u, in) // noop on non-windows OS 179 180 u.Path = path.Clean(u.Path) 181 if u.Path == "." { // empty after Clean() 182 u.Path = "" 183 } 184 185 if u.Scheme != "" { 186 if path.IsAbs(u.Path) || u.Scheme != fileScheme { 187 // this is absolute or explicitly not a local file: we're good 188 return u.String() 189 } 190 } 191 192 // no scheme or file scheme with relative path: assume file and make it absolute 193 // enforce scheme file://... with absolute path. 194 // 195 // If the input path is relative, we anchor the path to the current working directory. 196 // NOTE: we may end up with a host component. Leave it unchanged: e.g. file://host/folder/file.json 197 198 u.Scheme = fileScheme 199 u.Path = absPath(u.Path) // platform-dependent 200 u.RawQuery = "" // any query component is irrelevant for a base 201 return u.String() 202 } 203