Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(130)

Side by Side Diff: lib/src/source/hosted.dart

Issue 2044253003: Refactor Source and SourceRegistry. (Closed) Base URL: git@github.com:dart-lang/pub.git@master
Patch Set: Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 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. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 import 'dart:async'; 5 import 'dart:async';
6 import 'dart:io' as io; 6 import 'dart:io' as io;
7 import "dart:convert"; 7 import "dart:convert";
8 8
9 import 'package:http/http.dart' as http; 9 import 'package:http/http.dart' as http;
10 import 'package:path/path.dart' as p; 10 import 'package:path/path.dart' as p;
11 import 'package:pub_semver/pub_semver.dart'; 11 import 'package:pub_semver/pub_semver.dart';
12 12
13 import '../exceptions.dart'; 13 import '../exceptions.dart';
14 import '../http.dart'; 14 import '../http.dart';
15 import '../io.dart'; 15 import '../io.dart';
16 import '../log.dart' as log; 16 import '../log.dart' as log;
17 import '../package.dart'; 17 import '../package.dart';
18 import '../pubspec.dart'; 18 import '../pubspec.dart';
19 import '../source.dart';
20 import '../system_cache.dart';
19 import '../utils.dart'; 21 import '../utils.dart';
20 import 'cached.dart'; 22 import 'cached.dart';
21 23
22 /// A package source that gets packages from a package hosting site that uses 24 /// A package source that gets packages from a package hosting site that uses
23 /// the same API as pub.dartlang.org. 25 /// the same API as pub.dartlang.org.
24 class HostedSource extends CachedSource { 26 class HostedSource extends Source {
27 final name = "hosted";
28 final hasMultipleVersions = true;
29
30 LiveHostedSource bind(SystemCache systemCache, {bool isOffline: false}) =>
31 isOffline
32 ? new _OfflineHostedSource(this, systemCache)
33 : new LiveHostedSource(this, systemCache);
34
35 /// Gets the default URL for the package server for hosted dependencies.
36 String get defaultUrl {
37 var url = io.Platform.environment["PUB_HOSTED_URL"];
38 if (url != null) return url;
39
40 return "https://pub.dartlang.org";
41 }
42
25 /// Returns a reference to a hosted package named [name]. 43 /// Returns a reference to a hosted package named [name].
26 /// 44 ///
27 /// If [url] is passed, it's the URL of the pub server from which the package 45 /// If [url] is passed, it's the URL of the pub server from which the package
28 /// should be downloaded. It can be a [Uri] or a [String]. 46 /// should be downloaded. It can be a [Uri] or a [String].
29 static PackageRef refFor(String name, {url}) => 47 PackageRef refFor(String name, {url}) =>
30 new PackageRef(name, 'hosted', _descriptionFor(name, url)); 48 new PackageRef(name, 'hosted', _descriptionFor(name, url));
31 49
32 /// Returns an ID for a hosted package named [name] at [version]. 50 /// Returns an ID for a hosted package named [name] at [version].
33 /// 51 ///
34 /// If [url] is passed, it's the URL of the pub server from which the package 52 /// If [url] is passed, it's the URL of the pub server from which the package
35 /// should be downloaded. It can be a [Uri] or a [String]. 53 /// should be downloaded. It can be a [Uri] or a [String].
36 static PackageId idFor(String name, Version version, {url}) => 54 PackageId idFor(String name, Version version, {url}) =>
37 new PackageId(name, 'hosted', version, _descriptionFor(name, url)); 55 new PackageId(name, 'hosted', version, _descriptionFor(name, url));
38 56
39 /// Returns the description for a hosted package named [name] with the 57 /// Returns the description for a hosted package named [name] with the
40 /// given package server [url]. 58 /// given package server [url].
41 static _descriptionFor(String name, [url]) { 59 _descriptionFor(String name, [url]) {
42 if (url == null) return name; 60 if (url == null) return name;
43 61
44 if (url is! String && url is! Uri) { 62 if (url is! String && url is! Uri) {
45 throw new ArgumentError.value(url, 'url', 'must be a Uri or a String.'); 63 throw new ArgumentError.value(url, 'url', 'must be a Uri or a String.');
46 } 64 }
47 65
48 return {'name': name, 'url': url.toString()}; 66 return {'name': name, 'url': url.toString()};
49 } 67 }
50 68
51 final name = "hosted"; 69 bool descriptionsEqual(description1, description2) =>
52 final hasMultipleVersions = true; 70 _parseDescription(description1) == _parseDescription(description2);
53 71
54 /// Gets the default URL for the package server for hosted dependencies. 72 /// Ensures that [description] is a valid hosted package description.
55 static String get defaultUrl { 73 ///
56 var url = io.Platform.environment["PUB_HOSTED_URL"]; 74 /// There are two valid formats. A plain string refers to a package with the
57 if (url != null) return url; 75 /// given name from the default host, while a map with keys "name" and "url"
76 /// refers to a package with the given name from the host at the given URL.
77 PackageRef parseRef(String name, description, {String containingPath}) {
78 _parseDescription(description);
79 return new PackageRef(name, this.name, description);
80 }
58 81
59 return "https://pub.dartlang.org"; 82 PackageId parseId(String name, Version version, description) {
83 _parseDescription(description);
84 return new PackageId(name, this.name, version, description);
60 } 85 }
61 86
87 /// Parses the description for a package.
88 ///
89 /// If the package parses correctly, this returns a (name, url) pair. If not,
90 /// this throws a descriptive FormatException.
91 Pair<String, String> _parseDescription(description) {
92 if (description is String) {
93 return new Pair<String, String>(description, defaultUrl);
94 }
95
96 if (description is! Map) {
97 throw new FormatException(
98 "The description must be a package name or map.");
99 }
100
101 if (!description.containsKey("name")) {
102 throw new FormatException(
103 "The description map must contain a 'name' key.");
104 }
105
106 var name = description["name"];
107 if (name is! String) {
108 throw new FormatException("The 'name' key must have a string value.");
109 }
110
111 return new Pair<String, String>(name, description["url"] ?? defaultUrl);
112 }
113 }
114
115 /// The bound version of [HostedSource].
Bob Nystrom 2016/06/14 23:21:55 "Bound" isn't used elsewhere to describe live sour
nweiz 2016/06/20 20:46:08 I don't like leaving classes undocumented :-/. Wha
116 class LiveHostedSource extends CachedSource {
117 final HostedSource source;
118
119 final SystemCache systemCache;
120
121 LiveHostedSource(this.source, this.systemCache);
122
62 /// Downloads a list of all versions of a package that are available from the 123 /// Downloads a list of all versions of a package that are available from the
63 /// site. 124 /// site.
64 Future<List<PackageId>> doGetVersions(PackageRef ref) async { 125 Future<List<PackageId>> doGetVersions(PackageRef ref) async {
65 var url = _makeUrl(ref.description, 126 var url = _makeUrl(ref.description,
66 (server, package) => "$server/api/packages/$package"); 127 (server, package) => "$server/api/packages/$package");
67 128
68 log.io("Get versions from $url."); 129 log.io("Get versions from $url.");
69 130
70 var body; 131 var body;
71 try { 132 try {
72 body = await httpClient.read(url, headers: PUB_API_HEADERS); 133 body = await httpClient.read(url, headers: PUB_API_HEADERS);
73 } catch (error, stackTrace) { 134 } catch (error, stackTrace) {
74 var parsed = _parseDescription(ref.description); 135 var parsed = source._parseDescription(ref.description);
75 _throwFriendlyError(error, stackTrace, parsed.first, parsed.last); 136 _throwFriendlyError(error, stackTrace, parsed.first, parsed.last);
76 } 137 }
77 138
78 var doc = JSON.decode(body); 139 var doc = JSON.decode(body);
79 return doc['versions'].map((map) { 140 return doc['versions'].map((map) {
80 var pubspec = new Pubspec.fromMap( 141 var pubspec = new Pubspec.fromMap(
81 map['pubspec'], systemCache.sources, 142 map['pubspec'], systemCache.sources,
82 expectedName: ref.name, location: url); 143 expectedName: ref.name, location: url);
83 var id = idFor(ref.name, pubspec.version, 144 var id = source.idFor(ref.name, pubspec.version,
84 url: _serverFor(ref.description)); 145 url: _serverFor(ref.description));
85 memoizePubspec(id, pubspec); 146 memoizePubspec(id, pubspec);
86 147
87 return id; 148 return id;
88 }).toList(); 149 }).toList();
89 } 150 }
90 151
152 /// Parses [description] into its server and package name components, then
153 /// converts that to a Uri given [pattern].
154 ///
155 /// Ensures the package name is properly URL encoded.
156 Uri _makeUrl(description, String pattern(String server, String package)) {
157 var parsed = source._parseDescription(description);
158 var server = parsed.last;
159 var package = Uri.encodeComponent(parsed.first);
160 return Uri.parse(pattern(server, package));
161 }
162
91 /// Downloads and parses the pubspec for a specific version of a package that 163 /// Downloads and parses the pubspec for a specific version of a package that
92 /// is available from the site. 164 /// is available from the site.
93 Future<Pubspec> describeUncached(PackageId id) async { 165 Future<Pubspec> describeUncached(PackageId id) async {
94 // Request it from the server. 166 // Request it from the server.
95 var url = _makeVersionUrl(id, (server, package, version) => 167 var url = _makeVersionUrl(id, (server, package, version) =>
96 "$server/api/packages/$package/versions/$version"); 168 "$server/api/packages/$package/versions/$version");
97 169
98 log.io("Describe package at $url."); 170 log.io("Describe package at $url.");
99 var version; 171 var version;
100 try { 172 try {
101 version = JSON.decode( 173 version = JSON.decode(
102 await httpClient.read(url, headers: PUB_API_HEADERS)); 174 await httpClient.read(url, headers: PUB_API_HEADERS));
103 } catch (error, stackTrace) { 175 } catch (error, stackTrace) {
104 var parsed = _parseDescription(id.description); 176 var parsed = source._parseDescription(id.description);
105 _throwFriendlyError(error, stackTrace, id.name, parsed.last); 177 _throwFriendlyError(error, stackTrace, id.name, parsed.last);
106 } 178 }
107 179
108 return new Pubspec.fromMap( 180 return new Pubspec.fromMap(
109 version['pubspec'], systemCache.sources, 181 version['pubspec'], systemCache.sources,
110 expectedName: id.name, location: url); 182 expectedName: id.name, location: url);
111 } 183 }
112 184
113 /// Downloads the package identified by [id] to the system cache. 185 /// Downloads the package identified by [id] to the system cache.
114 Future<Package> downloadToSystemCache(PackageId id) async { 186 Future<Package> downloadToSystemCache(PackageId id) async {
115 if (!isInSystemCache(id)) { 187 if (!isInSystemCache(id)) {
116 var packageDir = getDirectory(id); 188 var packageDir = getDirectory(id);
117 ensureDir(p.dirname(packageDir)); 189 ensureDir(p.dirname(packageDir));
118 var parsed = _parseDescription(id.description); 190 var parsed = source._parseDescription(id.description);
119 await _download(parsed.last, parsed.first, id.version, packageDir); 191 await _download(parsed.last, parsed.first, id.version, packageDir);
120 } 192 }
121 193
122 return new Package.load(id.name, getDirectory(id), systemCache.sources); 194 return new Package.load(id.name, getDirectory(id), systemCache.sources);
123 } 195 }
124 196
125 /// The system cache directory for the hosted source contains subdirectories 197 /// The system cache directory for the hosted source contains subdirectories
126 /// for each separate repository URL that's used on the system. 198 /// for each separate repository URL that's used on the system.
127 /// 199 ///
128 /// Each of these subdirectories then contains a subdirectory for each 200 /// Each of these subdirectories then contains a subdirectory for each
129 /// package downloaded from that site. 201 /// package downloaded from that site.
130 String getDirectory(PackageId id) { 202 String getDirectory(PackageId id) {
131 var parsed = _parseDescription(id.description); 203 var parsed = source._parseDescription(id.description);
132 var dir = _urlToDirectory(parsed.last); 204 var dir = _urlToDirectory(parsed.last);
133 return p.join(systemCacheRoot, dir, "${parsed.first}-${id.version}"); 205 return p.join(systemCacheRoot, dir, "${parsed.first}-${id.version}");
134 } 206 }
135 207
136 String packageName(description) => _parseDescription(description).first;
137
138 bool descriptionsEqual(description1, description2) =>
139 _parseDescription(description1) == _parseDescription(description2);
140
141 /// Ensures that [description] is a valid hosted package description.
142 ///
143 /// There are two valid formats. A plain string refers to a package with the
144 /// given name from the default host, while a map with keys "name" and "url"
145 /// refers to a package with the given name from the host at the given URL.
146 PackageRef parseRef(String name, description, {String containingPath}) {
147 _parseDescription(description);
148 return new PackageRef(name, this.name, description);
149 }
150
151 PackageId parseId(String name, Version version, description) {
152 _parseDescription(description);
153 return new PackageId(name, this.name, version, description);
154 }
155
156 /// Re-downloads all packages that have been previously downloaded into the 208 /// Re-downloads all packages that have been previously downloaded into the
157 /// system cache from any server. 209 /// system cache from any server.
158 Future<Pair<List<PackageId>, List<PackageId>>> repairCachedPackages() async { 210 Future<Pair<List<PackageId>, List<PackageId>>> repairCachedPackages() async {
159 if (!dirExists(systemCacheRoot)) return new Pair([], []); 211 if (!dirExists(systemCacheRoot)) return new Pair([], []);
160 212
161 var successes = []; 213 var successes = [];
162 var failures = []; 214 var failures = [];
163 215
164 for (var serverDir in listDir(systemCacheRoot)) { 216 for (var serverDir in listDir(systemCacheRoot)) {
165 var url = _directoryToUrl(p.basename(serverDir)); 217 var url = _directoryToUrl(p.basename(serverDir));
166 var packages = _getCachedPackagesInDirectory(p.basename(serverDir)); 218 var packages = _getCachedPackagesInDirectory(p.basename(serverDir));
167 packages.sort(Package.orderByNameAndVersion); 219 packages.sort(Package.orderByNameAndVersion);
168 220
169 for (var package in packages) { 221 for (var package in packages) {
170 var id = idFor(package.name, package.version, url: url); 222 var id = source.idFor(package.name, package.version, url: url);
171 223
172 try { 224 try {
173 await _download(url, package.name, package.version, package.dir); 225 await _download(url, package.name, package.version, package.dir);
174 successes.add(id); 226 successes.add(id);
175 } catch (error, stackTrace) { 227 } catch (error, stackTrace) {
176 failures.add(id); 228 failures.add(id);
177 var message = "Failed to repair ${log.bold(package.name)} " 229 var message = "Failed to repair ${log.bold(package.name)} "
178 "${package.version}"; 230 "${package.version}";
179 if (url != defaultUrl) message += " from $url"; 231 if (url != source.defaultUrl) message += " from $url";
180 log.error("$message. Error:\n$error"); 232 log.error("$message. Error:\n$error");
181 log.fine(stackTrace); 233 log.fine(stackTrace);
182 234
183 tryDeleteEntry(package.dir); 235 tryDeleteEntry(package.dir);
184 } 236 }
185 } 237 }
186 } 238 }
187 239
188 return new Pair(successes, failures); 240 return new Pair(successes, failures);
189 } 241 }
190 242
191 /// Gets all of the packages that have been downloaded into the system cache 243 /// Gets all of the packages that have been downloaded into the system cache
192 /// from the default server. 244 /// from the default server.
193 List<Package> getCachedPackages() { 245 List<Package> getCachedPackages() =>
194 return _getCachedPackagesInDirectory(_urlToDirectory(defaultUrl)); 246 _getCachedPackagesInDirectory(_urlToDirectory(source.defaultUrl));
195 }
196 247
197 /// Gets all of the packages that have been downloaded into the system cache 248 /// Gets all of the packages that have been downloaded into the system cache
198 /// into [dir]. 249 /// into [dir].
199 List<Package> _getCachedPackagesInDirectory(String dir) { 250 List<Package> _getCachedPackagesInDirectory(String dir) {
200 var cacheDir = p.join(systemCacheRoot, dir); 251 var cacheDir = p.join(systemCacheRoot, dir);
201 if (!dirExists(cacheDir)) return []; 252 if (!dirExists(cacheDir)) return [];
202 253
203 return listDir(cacheDir) 254 return listDir(cacheDir)
204 .map((entry) => new Package.load(null, entry, systemCache.sources)) 255 .map((entry) => new Package.load(null, entry, systemCache.sources))
205 .toList(); 256 .toList();
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
241 } 292 }
242 293
243 if (error is io.SocketException) { 294 if (error is io.SocketException) {
244 fail("Got socket error trying to find package $package at $url.", 295 fail("Got socket error trying to find package $package at $url.",
245 error, stackTrace); 296 error, stackTrace);
246 } 297 }
247 298
248 // Otherwise re-throw the original exception. 299 // Otherwise re-throw the original exception.
249 throw error; 300 throw error;
250 } 301 }
302
303 /// Given a URL, returns a "normalized" string to be used as a directory name
304 /// for packages downloaded from the server at that URL.
305 ///
306 /// This normalization strips off the scheme (which is presumed to be HTTP or
307 /// HTTPS) and *sort of* URL-encodes it. I say "sort of" because it does it
308 /// incorrectly: it uses the character's *decimal* ASCII value instead of hex.
309 ///
310 /// This could cause an ambiguity since some characters get encoded as three
311 /// digits and others two. It's possible for one to be a prefix of the other.
312 /// In practice, the set of characters that are encoded don't happen to have
313 /// any collisions, so the encoding is reversible.
314 ///
315 /// This behavior is a bug, but is being preserved for compatibility.
316 String _urlToDirectory(String url) {
317 // Normalize all loopback URLs to "localhost".
318 url = url.replaceAllMapped(
319 new RegExp(r"^(https?://)(127\.0\.0\.1|\[::1\]|localhost)?"),
320 (match) {
321 // Don't include the scheme for HTTPS URLs. This makes the directory names
322 // nice for the default and most recommended scheme. We also don't include
323 // it for localhost URLs, since they're always known to be HTTP.
324 var localhost = match[2] == null ? '' : 'localhost';
325 var scheme = match[1] == 'https://' || localhost.isNotEmpty ? '' : match[1 ];
Bob Nystrom 2016/06/14 23:21:55 Long line.
nweiz 2016/06/20 20:46:08 Done.
326 return "$scheme$localhost";
327 });
328 return replace(url, new RegExp(r'[<>:"\\/|?*%]'),
329 (match) => '%${match[0].codeUnitAt(0)}');
330 }
331
332 /// Given a directory name in the system cache, returns the URL of the server
333 /// whose packages it contains.
334 ///
335 /// See [_urlToDirectory] for details on the mapping. Note that because the
336 /// directory name does not preserve the scheme, this has to guess at it. It
337 /// chooses "http" for loopback URLs (mainly to support the pub tests) and
338 /// "https" for all others.
339 String _directoryToUrl(String url) {
340 // Decode the pseudo-URL-encoded characters.
341 var chars = '<>:"\\/|?*%';
342 for (var i = 0; i < chars.length; i++) {
343 var c = chars.substring(i, i + 1);
344 url = url.replaceAll("%${c.codeUnitAt(0)}", c);
345 }
346
347 // If the URL has an explicit scheme, use that.
348 if (url.contains("://")) return url;
349
350 // Otherwise, default to http for localhost and https for everything else.
351 var scheme =
352 isLoopback(url.replaceAll(new RegExp(":.*"), "")) ? "http" : "https";
353 return "$scheme://$url";
354 }
355
356 /// Returns the server URL for [description].
357 Uri _serverFor(description) =>
358 Uri.parse(source._parseDescription(description).last);
359
360 /// Parses [id] into its server, package name, and version components, then
361 /// converts that to a Uri given [pattern].
362 ///
363 /// Ensures the package name is properly URL encoded.
364 Uri _makeVersionUrl(PackageId id,
365 String pattern(String server, String package, String version)) {
366 var parsed = source._parseDescription(id.description);
367 var server = parsed.last;
368 var package = Uri.encodeComponent(parsed.first);
369 var version = Uri.encodeComponent(id.version.toString());
370 return Uri.parse(pattern(server, package, version));
371 }
251 } 372 }
252 373
253 /// This is the modified hosted source used when pub get or upgrade are run 374 /// This is the modified hosted source used when pub get or upgrade are run
254 /// with "--offline". 375 /// with "--offline".
255 /// 376 ///
256 /// This uses the system cache to get the list of available packages and does 377 /// This uses the system cache to get the list of available packages and does
257 /// no network access. 378 /// no network access.
258 class OfflineHostedSource extends HostedSource { 379 class _OfflineHostedSource extends LiveHostedSource {
380 _OfflineHostedSource(HostedSource source, SystemCache systemCache)
381 : super(source, systemCache);
382
259 /// Gets the list of all versions of [ref] that are in the system cache. 383 /// Gets the list of all versions of [ref] that are in the system cache.
260 Future<List<PackageId>> doGetVersions(PackageRef ref) async { 384 Future<List<PackageId>> doGetVersions(PackageRef ref) async {
261 var parsed = _parseDescription(ref.description); 385 var parsed = source._parseDescription(ref.description);
262 var server = parsed.last; 386 var server = parsed.last;
263 log.io("Finding versions of ${ref.name} in " 387 log.io("Finding versions of ${ref.name} in "
264 "$systemCacheRoot/${_urlToDirectory(server)}"); 388 "$systemCacheRoot/${_urlToDirectory(server)}");
265 389
266 var dir = p.join(systemCacheRoot, _urlToDirectory(server)); 390 var dir = p.join(systemCacheRoot, _urlToDirectory(server));
267 391
268 var versions; 392 var versions;
269 if (dirExists(dir)) { 393 if (dirExists(dir)) {
270 versions = await listDir(dir).map((entry) { 394 versions = await listDir(dir).map((entry) {
271 var components = p.basename(entry).split("-"); 395 var components = p.basename(entry).split("-");
272 if (components.first != ref.name) return null; 396 if (components.first != ref.name) return null;
273 return HostedSource.idFor( 397 return source.idFor(
274 ref.name, new Version.parse(components.skip(1).join("-")), 398 ref.name, new Version.parse(components.skip(1).join("-")),
275 url: _serverFor(ref.description)); 399 url: _serverFor(ref.description));
276 }).where((id) => id != null).toList(); 400 }).where((id) => id != null).toList();
277 } else { 401 } else {
278 versions = []; 402 versions = [];
279 } 403 }
280 404
281 // If there are no versions in the cache, report a clearer error. 405 // If there are no versions in the cache, report a clearer error.
282 if (versions.isEmpty) { 406 if (versions.isEmpty) {
283 throw new PackageNotFoundException( 407 throw new PackageNotFoundException(
284 "Could not find package ${ref.name} in cache."); 408 "Could not find package ${ref.name} in cache.");
285 } 409 }
286 410
287 return versions; 411 return versions;
288 } 412 }
289 413
290 Future _download(String server, String package, Version version, 414 Future _download(String server, String package, Version version,
291 String destPath) { 415 String destPath) {
292 // Since HostedSource is cached, this will only be called for uncached 416 // Since HostedSource is cached, this will only be called for uncached
293 // packages. 417 // packages.
294 throw new UnsupportedError("Cannot download packages when offline."); 418 throw new UnsupportedError("Cannot download packages when offline.");
295 } 419 }
296 420
297 Future<Pubspec> describeUncached(PackageId id) { 421 Future<Pubspec> describeUncached(PackageId id) {
298 throw new PackageNotFoundException( 422 throw new PackageNotFoundException(
299 "${id.name} ${id.version} is not available in your system cache."); 423 "${id.name} ${id.version} is not available in your system cache.");
300 } 424 }
301 } 425 }
302
303 /// Given a URL, returns a "normalized" string to be used as a directory name
304 /// for packages downloaded from the server at that URL.
305 ///
306 /// This normalization strips off the scheme (which is presumed to be HTTP or
307 /// HTTPS) and *sort of* URL-encodes it. I say "sort of" because it does it
308 /// incorrectly: it uses the character's *decimal* ASCII value instead of hex.
309 ///
310 /// This could cause an ambiguity since some characters get encoded as three
311 /// digits and others two. It's possible for one to be a prefix of the other.
312 /// In practice, the set of characters that are encoded don't happen to have
313 /// any collisions, so the encoding is reversible.
314 ///
315 /// This behavior is a bug, but is being preserved for compatibility.
316 String _urlToDirectory(String url) {
317 // Normalize all loopback URLs to "localhost".
318 url = url.replaceAllMapped(
319 new RegExp(r"^(https?://)(127\.0\.0\.1|\[::1\]|localhost)?"),
320 (match) {
321 // Don't include the scheme for HTTPS URLs. This makes the directory names
322 // nice for the default and most recommended scheme. We also don't include
323 // it for localhost URLs, since they're always known to be HTTP.
324 var localhost = match[2] == null ? '' : 'localhost';
325 var scheme = match[1] == 'https://' || localhost.isNotEmpty ? '' : match[1];
326 return "$scheme$localhost";
327 });
328 return replace(url, new RegExp(r'[<>:"\\/|?*%]'),
329 (match) => '%${match[0].codeUnitAt(0)}');
330 }
331
332 /// Given a directory name in the system cache, returns the URL of the server
333 /// whose packages it contains.
334 ///
335 /// See [_urlToDirectory] for details on the mapping. Note that because the
336 /// directory name does not preserve the scheme, this has to guess at it. It
337 /// chooses "http" for loopback URLs (mainly to support the pub tests) and
338 /// "https" for all others.
339 String _directoryToUrl(String url) {
340 // Decode the pseudo-URL-encoded characters.
341 var chars = '<>:"\\/|?*%';
342 for (var i = 0; i < chars.length; i++) {
343 var c = chars.substring(i, i + 1);
344 url = url.replaceAll("%${c.codeUnitAt(0)}", c);
345 }
346
347 // If the URL has an explicit scheme, use that.
348 if (url.contains("://")) return url;
349
350 // Otherwise, default to http for localhost and https for everything else.
351 var scheme =
352 isLoopback(url.replaceAll(new RegExp(":.*"), "")) ? "http" : "https";
353 return "$scheme://$url";
354 }
355
356 /// Parses [description] into its server and package name components, then
357 /// converts that to a Uri given [pattern].
358 ///
359 /// Ensures the package name is properly URL encoded.
360 Uri _makeUrl(description, String pattern(String server, String package)) {
361 var parsed = _parseDescription(description);
362 var server = parsed.last;
363 var package = Uri.encodeComponent(parsed.first);
364 return Uri.parse(pattern(server, package));
365 }
366
367 /// Returns the server URL for [description].
368 Uri _serverFor(description) => Uri.parse(_parseDescription(description).last);
369
370 /// Parses [id] into its server, package name, and version components, then
371 /// converts that to a Uri given [pattern].
372 ///
373 /// Ensures the package name is properly URL encoded.
374 Uri _makeVersionUrl(PackageId id,
375 String pattern(String server, String package, String version)) {
376 var parsed = _parseDescription(id.description);
377 var server = parsed.last;
378 var package = Uri.encodeComponent(parsed.first);
379 var version = Uri.encodeComponent(id.version.toString());
380 return Uri.parse(pattern(server, package, version));
381 }
382
383 /// Parses the description for a package.
384 ///
385 /// If the package parses correctly, this returns a (name, url) pair. If not,
386 /// this throws a descriptive FormatException.
387 Pair<String, String> _parseDescription(description) {
388 if (description is String) {
389 return new Pair<String, String>(description, HostedSource.defaultUrl);
390 }
391
392 if (description is! Map) {
393 throw new FormatException(
394 "The description must be a package name or map.");
395 }
396
397 if (!description.containsKey("name")) {
398 throw new FormatException(
399 "The description map must contain a 'name' key.");
400 }
401
402 var name = description["name"];
403 if (name is! String) {
404 throw new FormatException("The 'name' key must have a string value.");
405 }
406
407 var url = description["url"];
408 if (url == null) url = HostedSource.defaultUrl;
409
410 return new Pair<String, String>(name, url);
411 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698