OLD | NEW |
| (Empty) |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library package_config.packagemap; | |
6 | |
7 class Packages { | |
8 static const int _EQUALS = 0x3d; | |
9 static const int _CR = 0x0d; | |
10 static const int _NL = 0x0a; | |
11 static const int _NUMBER_SIGN = 0x23; | |
12 | |
13 final Map<String, Uri> packageMapping; | |
14 | |
15 Packages(this.packageMapping); | |
16 | |
17 /// Resolves a URI to a non-package URI. | |
18 /// | |
19 /// If [uri] is a `package:` URI, the location is resolved wrt. the | |
20 /// [packageMapping]. | |
21 /// Otherwise the original URI is returned. | |
22 Uri resolve(Uri uri) { | |
23 if (uri.scheme.toLowerCase() != "package") { | |
24 return uri; | |
25 } | |
26 if (uri.hasAuthority) { | |
27 throw new ArgumentError.value(uri, "uri", "Must not have authority"); | |
28 } | |
29 if (uri.path.startsWith("/")) { | |
30 throw new ArgumentError.value( | |
31 uri, "uri", "Path must not start with '/'."); | |
32 } | |
33 // Normalizes the path by removing '.' and '..' segments. | |
34 uri = uri.normalizePath(); | |
35 String path = uri.path; | |
36 var slashIndex = path.indexOf('/'); | |
37 String packageName; | |
38 String rest; | |
39 if (slashIndex < 0) { | |
40 packageName = path; | |
41 rest = ""; | |
42 } else { | |
43 packageName = path.substring(0, slashIndex); | |
44 rest = path.substring(slashIndex + 1); | |
45 } | |
46 Uri packageLocation = packageMapping[packageName]; | |
47 if (packageLocation == null) { | |
48 throw new ArgumentError.value( | |
49 uri, "uri", "Unknown package name: $packageName"); | |
50 } | |
51 return packageLocation.resolveUri(new Uri(path: rest)); | |
52 } | |
53 | |
54 /// Parses a `packages.cfg` file into a `Packages` object. | |
55 /// | |
56 /// The [baseLocation] is used as a base URI to resolve all relative | |
57 /// URI references against. | |
58 /// | |
59 /// The `Packages` object allows resolving package: URIs and writing | |
60 /// the mapping back to a file or string. | |
61 /// The [packageMapping] will contain a simple mapping from package name | |
62 /// to package location. | |
63 static Packages parse(String source, Uri baseLocation) { | |
64 int index = 0; | |
65 Map<String, Uri> result = <String, Uri>{}; | |
66 while (index < source.length) { | |
67 bool isComment = false; | |
68 int start = index; | |
69 int eqIndex = -1; | |
70 int end = source.length; | |
71 int char = source.codeUnitAt(index++); | |
72 if (char == _CR || char == _NL) { | |
73 continue; | |
74 } | |
75 if (char == _EQUALS) { | |
76 throw new FormatException("Missing package name", source, index - 1); | |
77 } | |
78 isComment = char == _NUMBER_SIGN; | |
79 while (index < source.length) { | |
80 char = source.codeUnitAt(index++); | |
81 if (char == _EQUALS && eqIndex < 0) { | |
82 eqIndex = index - 1; | |
83 } else if (char == _NL || char == _CR) { | |
84 end = index - 1; | |
85 break; | |
86 } | |
87 } | |
88 if (isComment) continue; | |
89 if (eqIndex < 0) { | |
90 throw new FormatException("No '=' on line", source, index - 1); | |
91 } | |
92 _checkIdentifier(source, start, eqIndex); | |
93 var packageName = source.substring(start, eqIndex); | |
94 | |
95 var packageLocation = Uri.parse(source, eqIndex + 1, end); | |
96 if (!packageLocation.path.endsWith('/')) { | |
97 packageLocation = | |
98 packageLocation.replace(path: packageLocation.path + "/"); | |
99 } | |
100 packageLocation = baseLocation.resolveUri(packageLocation); | |
101 if (result.containsKey(packageName)) { | |
102 throw new FormatException( | |
103 "Same package name occured twice.", source, start); | |
104 } | |
105 result[packageName] = packageLocation; | |
106 } | |
107 return new Packages(result); | |
108 } | |
109 | |
110 /** | |
111 * Writes the mapping to a [StringSink]. | |
112 * | |
113 * If [comment] is provided, the output will contain this comment | |
114 * with `#` in front of each line. | |
115 * | |
116 * If [baseUri] is provided, package locations will be made relative | |
117 * to the base URI, if possible, before writing. | |
118 */ | |
119 void write(StringSink output, {Uri baseUri, String comment}) { | |
120 if (baseUri != null && !baseUri.isAbsolute) { | |
121 throw new ArgumentError.value(baseUri, "baseUri", "Must be absolute"); | |
122 } | |
123 | |
124 if (comment != null) { | |
125 for (var commentLine in comment.split('\n')) { | |
126 output.write('#'); | |
127 output.writeln(commentLine); | |
128 } | |
129 } else { | |
130 output.write("# generated by package:packagecfg at "); | |
131 output.write(new DateTime.now()); | |
132 output.writeln(); | |
133 } | |
134 | |
135 packageMapping.forEach((String packageName, Uri uri) { | |
136 // Validate packageName. | |
137 _checkIdentifier(packageName, 0, packageName.length); | |
138 output.write(packageName); | |
139 | |
140 output.write('='); | |
141 | |
142 // If baseUri provided, make uri relative. | |
143 if (baseUri != null) { | |
144 uri = relativize(uri, baseUri); | |
145 } | |
146 output.write(uri); | |
147 if (!uri.path.endsWith('/')) { | |
148 output.write('/'); | |
149 } | |
150 output.writeln(); | |
151 }); | |
152 } | |
153 | |
154 String toString() { | |
155 StringBuffer buffer = new StringBuffer(); | |
156 write(buffer); | |
157 return buffer.toString(); | |
158 } | |
159 | |
160 static Uri relativize(Uri uri, Uri baseUri) { | |
161 if (uri.hasQuery || uri.hasFragment) { | |
162 uri = new Uri( | |
163 scheme: uri.scheme, | |
164 userInfo: uri.hasAuthority ? uri.userInfo : null, | |
165 host: uri.hasAuthority ? uri.host : null, | |
166 port: uri.hasAuthority ? uri.port : null, | |
167 path: uri.path); | |
168 } | |
169 if (!baseUri.isAbsolute) { | |
170 throw new ArgumentError("Base uri '$baseUri' must be absolute."); | |
171 } | |
172 // Already relative. | |
173 if (!uri.isAbsolute) return uri; | |
174 | |
175 if (baseUri.scheme.toLowerCase() != uri.scheme.toLowerCase()) { | |
176 return uri; | |
177 } | |
178 // If authority differs, we could remove the scheme, but it's not worth it. | |
179 if (uri.hasAuthority != baseUri.hasAuthority) return uri; | |
180 if (uri.hasAuthority) { | |
181 if (uri.userInfo != baseUri.userInfo || | |
182 uri.host.toLowerCase() != baseUri.host.toLowerCase() || | |
183 uri.port != baseUri.port) { | |
184 return uri; | |
185 } | |
186 } | |
187 | |
188 baseUri = baseUri.normalizePath(); | |
189 List<String> base = baseUri.pathSegments.toList(); | |
190 if (base.isNotEmpty) { | |
191 base = new List<String>.from(base)..removeLast(); | |
192 } | |
193 uri = uri.normalizePath(); | |
194 List<String> target = uri.pathSegments.toList(); | |
195 int index = 0; | |
196 while (index < base.length && index < target.length) { | |
197 if (base[index] != target[index]) { | |
198 break; | |
199 } | |
200 index++; | |
201 } | |
202 if (index == base.length) { | |
203 return new Uri(path: target.skip(index).join('/')); | |
204 } else if (index > 0) { | |
205 return new Uri( | |
206 path: '../' * (base.length - index) + target.skip(index).join('/')); | |
207 } else { | |
208 return uri; | |
209 } | |
210 } | |
211 | |
212 static bool _checkIdentifier(String string, int start, int end) { | |
213 const int a = 0x61; | |
214 const int z = 0x7a; | |
215 const int _ = 0x5f; | |
216 const int $ = 0x24; | |
217 if (start == end) return false; | |
218 for (int i = start; i < end; i++) { | |
219 var char = string.codeUnitAt(i); | |
220 if (char == _ || char == $) continue; | |
221 if ((char ^ 0x30) <= 9 && i > 0) continue; | |
222 char |= 0x20; // Lower-case letters. | |
223 if (char >= a && char <= z) continue; | |
224 throw new FormatException("Not an identifier", string, i); | |
225 } | |
226 return true; | |
227 } | |
228 } | |
OLD | NEW |