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.packages_file; | |
6 | |
7 import "package:charcode/ascii.dart"; | |
8 import "src/util.dart" show isValidPackageName; | |
9 | |
10 /// Parses a `.packages` file into a map from package name to base URI. | |
11 /// | |
12 /// The [source] is the byte content of a `.packages` file, assumed to be | |
13 /// UTF-8 encoded. In practice, all significant parts of the file must be ASCII, | |
14 /// so Latin-1 or Windows-1252 encoding will also work fine. | |
15 /// | |
16 /// If the file content is available as a string, its [String.codeUnits] can | |
17 /// be used as the `source` argument of this function. | |
18 /// | |
19 /// The [baseLocation] is used as a base URI to resolve all relative | |
20 /// URI references against. | |
21 /// If the content was read from a file, `baseLocation` should be the | |
22 /// location of that file. | |
23 /// | |
24 /// Returns a simple mapping from package name to package location. | |
25 Map<String, Uri> parse(List<int> source, Uri baseLocation) { | |
26 int index = 0; | |
27 Map<String, Uri> result = <String, Uri>{}; | |
28 while (index < source.length) { | |
29 bool isComment = false; | |
30 int start = index; | |
31 int separatorIndex = -1; | |
32 int end = source.length; | |
33 int char = source[index++]; | |
34 if (char == $cr || char == $lf) { | |
35 continue; | |
36 } | |
37 if (char == $colon) { | |
38 throw new FormatException("Missing package name", source, index - 1); | |
39 } | |
40 isComment = char == $hash; | |
41 while (index < source.length) { | |
42 char = source[index++]; | |
43 if (char == $colon && separatorIndex < 0) { | |
44 separatorIndex = index - 1; | |
45 } else if (char == $cr || char == $lf) { | |
46 end = index - 1; | |
47 break; | |
48 } | |
49 } | |
50 if (isComment) continue; | |
51 if (separatorIndex < 0) { | |
52 throw new FormatException("No ':' on line", source, index - 1); | |
53 } | |
54 var packageName = new String.fromCharCodes(source, start, separatorIndex); | |
55 if (!isValidPackageName(packageName)) { | |
56 throw new FormatException("Not a valid package name", packageName, 0); | |
57 } | |
58 var packageUri = new String.fromCharCodes(source, separatorIndex + 1, end); | |
59 var packageLocation = Uri.parse(packageUri); | |
60 packageLocation = baseLocation.resolveUri(packageLocation); | |
61 if (!packageLocation.path.endsWith('/')) { | |
62 packageLocation = | |
63 packageLocation.replace(path: packageLocation.path + "/"); | |
64 } | |
65 if (result.containsKey(packageName)) { | |
66 throw new FormatException( | |
67 "Same package name occured twice.", source, start); | |
68 } | |
69 result[packageName] = packageLocation; | |
70 } | |
71 return result; | |
72 } | |
73 | |
74 /// Writes the mapping to a [StringSink]. | |
75 /// | |
76 /// If [comment] is provided, the output will contain this comment | |
77 /// with `# ` in front of each line. | |
78 /// Lines are defined as ending in line feed (`'\n'`). If the final | |
79 /// line of the comment doesn't end in a line feed, one will be added. | |
80 /// | |
81 /// If [baseUri] is provided, package locations will be made relative | |
82 /// to the base URI, if possible, before writing. | |
83 /// | |
84 /// All the keys of [packageMapping] must be valid package names, | |
85 /// and the values must be URIs that do not have the `package:` scheme. | |
86 void write(StringSink output, Map<String, Uri> packageMapping, | |
87 {Uri baseUri, String comment}) { | |
88 if (baseUri != null && !baseUri.isAbsolute) { | |
89 throw new ArgumentError.value(baseUri, "baseUri", "Must be absolute"); | |
90 } | |
91 | |
92 if (comment != null) { | |
93 var lines = comment.split('\n'); | |
94 if (lines.last.isEmpty) lines.removeLast(); | |
95 for (var commentLine in lines) { | |
96 output.write('# '); | |
97 output.writeln(commentLine); | |
98 } | |
99 } else { | |
100 output.write("# generated by package:package_config at "); | |
101 output.write(new DateTime.now()); | |
102 output.writeln(); | |
103 } | |
104 | |
105 packageMapping.forEach((String packageName, Uri uri) { | |
106 // Validate packageName. | |
107 if (!isValidPackageName(packageName)) { | |
108 throw new ArgumentError('"$packageName" is not a valid package name'); | |
109 } | |
110 if (uri.scheme == "package") { | |
111 throw new ArgumentError.value( | |
112 "Package location must not be a package: URI", uri.toString()); | |
113 } | |
114 output.write(packageName); | |
115 output.write(':'); | |
116 // If baseUri provided, make uri relative. | |
117 if (baseUri != null) { | |
118 uri = _relativize(uri, baseUri); | |
119 } | |
120 output.write(uri); | |
121 if (!uri.path.endsWith('/')) { | |
122 output.write('/'); | |
123 } | |
124 output.writeln(); | |
125 }); | |
126 } | |
127 | |
128 /// Attempts to return a relative URI for [uri]. | |
129 /// | |
130 /// The result URI satisfies `baseUri.resolveUri(result) == uri`, | |
131 /// but may be relative. | |
132 /// The `baseUri` must be absolute. | |
133 Uri _relativize(Uri uri, Uri baseUri) { | |
134 assert(baseUri.isAbsolute); | |
135 if (uri.hasQuery || uri.hasFragment) { | |
136 uri = new Uri( | |
137 scheme: uri.scheme, | |
138 userInfo: uri.hasAuthority ? uri.userInfo : null, | |
139 host: uri.hasAuthority ? uri.host : null, | |
140 port: uri.hasAuthority ? uri.port : null, | |
141 path: uri.path); | |
142 } | |
143 | |
144 // Already relative. We assume the caller knows what they are doing. | |
145 if (!uri.isAbsolute) return uri; | |
146 | |
147 if (baseUri.scheme != uri.scheme) { | |
148 return uri; | |
149 } | |
150 | |
151 // If authority differs, we could remove the scheme, but it's not worth it. | |
152 if (uri.hasAuthority != baseUri.hasAuthority) return uri; | |
153 if (uri.hasAuthority) { | |
154 if (uri.userInfo != baseUri.userInfo || | |
155 uri.host.toLowerCase() != baseUri.host.toLowerCase() || | |
156 uri.port != baseUri.port) { | |
157 return uri; | |
158 } | |
159 } | |
160 | |
161 baseUri = _normalizePath(baseUri); | |
162 List<String> base = baseUri.pathSegments.toList(); | |
163 if (base.isNotEmpty) { | |
164 base = new List<String>.from(base)..removeLast(); | |
165 } | |
166 uri = _normalizePath(uri); | |
167 List<String> target = uri.pathSegments.toList(); | |
168 if (target.isNotEmpty && target.last.isEmpty) target.removeLast(); | |
169 int index = 0; | |
170 while (index < base.length && index < target.length) { | |
171 if (base[index] != target[index]) { | |
172 break; | |
173 } | |
174 index++; | |
175 } | |
176 if (index == base.length) { | |
177 if (index == target.length) { | |
178 return new Uri(path: "./"); | |
179 } | |
180 return new Uri(path: target.skip(index).join('/')); | |
181 } else if (index > 0) { | |
182 return new Uri( | |
183 path: '../' * (base.length - index) + target.skip(index).join('/')); | |
184 } else { | |
185 return uri; | |
186 } | |
187 } | |
188 | |
189 // TODO: inline to uri.normalizePath() when we move to 1.11 | |
190 Uri _normalizePath(Uri uri) => new Uri().resolveUri(uri); | |
OLD | NEW |