Dependency services: preserve pub.dartlang.org in lockfile (#3846)

diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
index 00cbe2e..c56f413 100644
--- a/lib/src/command/dependency_services.dart
+++ b/lib/src/command/dependency_services.dart
@@ -381,6 +381,7 @@
     final lockFileYaml = lockFile == null ? null : loadYaml(lockFile);
     final lockFileEditor = lockFile == null ? null : YamlEditor(lockFile);
     final hasContentHashes = _lockFileHasContentHashes(lockFileYaml);
+    final usesPubDev = _lockFileUsesPubDev(lockFileYaml);
     for (final p in toApply) {
       final targetPackage = p.name;
       final targetVersion = p.version;
@@ -513,7 +514,6 @@
           for (var package in solveResult.packages) {
             if (package.isRoot) continue;
             final description = package.description;
-
             // Handle content-hashes of hosted dependencies.
             if (description is ResolvedHostedDescription) {
               // Ensure we get content-hashes if the original lock-file had
@@ -547,6 +547,23 @@
                   description.withSha256(null),
                 );
               }
+              // Keep using https://pub.dartlang.org if the original lockfile
+              // used it. This is to support lockfiles from old sdks.
+              if (!usesPubDev &&
+                  HostedSource.isPubDevUrl(description.description.url)) {
+                package = PackageId(
+                  package.name,
+                  package.version,
+                  ResolvedHostedDescription(
+                    HostedDescription.raw(
+                      package.name,
+                      HostedSource.pubDartlangUrl,
+                    ),
+                    sha256: (package.description as ResolvedHostedDescription)
+                        .sha256,
+                  ),
+                );
+              }
             }
             updatedPackages.add(package);
           }
@@ -695,3 +712,28 @@
   }
   return false;
 }
+
+/// `true` iff any of the packages described by the [lockfile] uses
+/// `https://pub.dev` as url.
+///
+/// Undefined for invalid lock files, but mostly `true`.
+bool _lockFileUsesPubDev(dynamic lockfile) {
+  if (lockfile is! Map) return true;
+  final packages = lockfile['packages'];
+  if (packages is! Map) return true;
+
+  /// We consider an empty lockfile ready to get content-hashes.
+  if (packages.isEmpty) return true;
+  for (final package in packages.values) {
+    if (package is! Map) return true;
+    if (package['source'] != 'hosted') continue;
+    final descriptor = package['description'];
+    if (descriptor is! Map) return true;
+    final url = descriptor['url'];
+    if (url is! String) return true;
+    if (HostedSource.isPubDevUrl(url) && url != HostedSource.pubDartlangUrl) {
+      return true;
+    }
+  }
+  return false;
+}
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index 2086ed0..6f6632c 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -128,7 +128,16 @@
   static String pubDartlangUrl = 'https://pub.dartlang.org';
 
   static bool isPubDevUrl(String url) {
-    final origin = Uri.parse(url).origin;
+    final parsedUrl = Uri.parse(url);
+    if (parsedUrl.scheme != 'http' && parsedUrl.scheme != 'https') {
+      // A non http(s) url is not pub.dev.
+      return false;
+    }
+    if (parsedUrl.host.isEmpty) {
+      // The empty host is not pub.dev.
+      return false;
+    }
+    final origin = parsedUrl.origin;
     // Allow the defaultHostedUrl to be overriden when running from tests
     if (runningFromTest &&
         io.Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'] != null) {
@@ -1380,6 +1389,11 @@
   final String url;
 
   HostedDescription._(this.packageName, this.url);
+
+  // This can be used to construct a description with any specific url.
+  factory HostedDescription.raw(String packageName, String url) =>
+      HostedDescription._(packageName, url);
+
   factory HostedDescription(String packageName, String url) =>
       HostedDescription._(
         packageName,
@@ -1437,16 +1451,10 @@
 
   @override
   Object? serializeForLockfile({required String? containingDir}) {
-    late final String url;
-    try {
-      url = validateAndNormalizeHostedUrl(description.url).toString();
-    } on FormatException catch (e) {
-      throw ArgumentError.value(url, 'url', 'url must be normalized: $e');
-    }
     final hash = sha256;
     return {
       'name': description.packageName,
-      'url': url.toString(),
+      'url': description.url,
       if (hash != null) 'sha256': hexEncode(hash),
     };
   }
diff --git a/test/dependency_services/dependency_services_test.dart b/test/dependency_services/dependency_services_test.dart
index cd77694..412be30 100644
--- a/test/dependency_services/dependency_services_test.dart
+++ b/test/dependency_services/dependency_services_test.dart
@@ -52,11 +52,15 @@
     final process = await Process.start(
       Platform.resolvedExecutable,
       [
+        '--enable-asserts',
         snapshot,
         '--verbose',
         ...args,
       ],
-      environment: getPubTestEnvironment(),
+      environment: {
+        ...getPubTestEnvironment(),
+        '_PUB_TEST_DEFAULT_HOSTED_URL': globalServer.url,
+      },
       workingDirectory: p.join(d.sandbox, appPath),
     );
     if (stdin != null) {
@@ -265,6 +269,45 @@
     ]);
   });
 
+  testWithGolden('Preserves pub.dartlang.org as hosted url', (context) async {
+    final server = (await servePackages())
+      ..serve('foo', '1.2.3')
+      ..serve('bar', '1.2.3')
+      ..serveContentHashes = true;
+
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'app',
+        'dependencies': {
+          'foo': '^1.0.0',
+          'bar': '^1.0.0',
+        },
+      })
+    ]).create();
+    await pubGet();
+    final lockFile = File(path(p.join(appPath, 'pubspec.lock')));
+    final lockFileYaml = YamlEditor(
+      lockFile.readAsStringSync(),
+    );
+    for (final p in lockFileYaml.parseAt(['packages']).value.entries) {
+      lockFileYaml.update(
+        ['packages', p.key, 'description', 'url'],
+        'https://pub.dartlang.org',
+      );
+    }
+    lockFile.writeAsStringSync(lockFileYaml.toString());
+
+    server.serve('foo', '1.2.4');
+    server.serve('boo', '1.2.4');
+
+    await _listReportApply(
+      context,
+      [
+        _PackageVersion('foo', '1.2.4'),
+      ],
+    );
+  });
+
   testWithGolden('Adding transitive', (context) async {
     final server = (await servePackages())
       ..serve('foo', '1.2.3')
@@ -383,6 +426,7 @@
   });
 
   testWithGolden('Can update a git package', (context) async {
+    await servePackages();
     await d.git('foo.git', [d.libPubspec('foo', '1.0.0')]).create();
     await d.git('bar.git', [d.libPubspec('bar', '1.0.0')]).create();
 
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Preserves pub.dartlang.org as hosted url.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Preserves pub.dartlang.org as hosted url.txt
new file mode 100644
index 0000000..5a95570
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Preserves pub.dartlang.org as hosted url.txt
@@ -0,0 +1,352 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0","bar":"^1.0.0"},"environment":{"sdk":"^3.0.2"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  bar:
+    dependency: "direct main"
+    description:
+      name: bar
+      sha256: "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+      url: https://pub.dartlang.org
+    source: hosted
+    version: "1.2.3"
+  foo:
+    dependency: "direct main"
+    description:
+      name: foo
+      sha256: "0a2eec28c63c8ee616670da9bae0dbfe9294f5b04b264cc4a9be24876ea23d3b"
+      url: https://pub.dartlang.org
+    source: hosted
+    version: "1.2.3"
+sdks:
+  dart: ">=3.0.2 <4.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section list
+$ dependency_services list
+{
+  "dependencies": [
+    {
+      "name": "bar",
+      "version": "1.2.3",
+      "kind": "direct",
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "bar",
+          "url": "https://pub.dev",
+          "sha256": "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+        }
+      }
+    },
+    {
+      "name": "foo",
+      "version": "1.2.3",
+      "kind": "direct",
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "https://pub.dev",
+          "sha256": "0a2eec28c63c8ee616670da9bae0dbfe9294f5b04b264cc4a9be24876ea23d3b"
+        }
+      }
+    }
+  ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ dependency_services report
+{
+  "dependencies": [
+    {
+      "name": "bar",
+      "version": "1.2.3",
+      "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "bar",
+          "url": "https://pub.dev",
+          "sha256": "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+        }
+      },
+      "latest": null,
+      "constraint": "^1.0.0",
+      "compatible": [
+        {
+          "name": "bar",
+          "version": "1.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT",
+              "sha256": "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+            }
+          },
+          "constraintBumped": "^1.0.0",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "https://pub.dev",
+              "sha256": "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+            }
+          }
+        },
+        {
+          "name": "foo",
+          "version": "1.2.4",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "98405f2be818d57d0c01a1252e6bc50c1db9bb69b90557909740083019f214cd"
+            }
+          },
+          "constraintBumped": "^1.0.0",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "https://pub.dev",
+              "sha256": "0a2eec28c63c8ee616670da9bae0dbfe9294f5b04b264cc4a9be24876ea23d3b"
+            }
+          }
+        }
+      ],
+      "singleBreaking": [],
+      "multiBreaking": [
+        {
+          "name": "bar",
+          "version": "1.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT",
+              "sha256": "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+            }
+          },
+          "constraintBumped": "^1.2.3",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "https://pub.dev",
+              "sha256": "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+            }
+          }
+        },
+        {
+          "name": "foo",
+          "version": "1.2.4",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "98405f2be818d57d0c01a1252e6bc50c1db9bb69b90557909740083019f214cd"
+            }
+          },
+          "constraintBumped": "^1.2.4",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "https://pub.dev",
+              "sha256": "0a2eec28c63c8ee616670da9bae0dbfe9294f5b04b264cc4a9be24876ea23d3b"
+            }
+          }
+        }
+      ]
+    },
+    {
+      "name": "foo",
+      "version": "1.2.3",
+      "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "https://pub.dev",
+          "sha256": "0a2eec28c63c8ee616670da9bae0dbfe9294f5b04b264cc4a9be24876ea23d3b"
+        }
+      },
+      "latest": "0.0.1",
+      "constraint": "^1.0.0",
+      "compatible": [
+        {
+          "name": "bar",
+          "version": "1.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT",
+              "sha256": "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+            }
+          },
+          "constraintBumped": "^1.0.0",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "https://pub.dev",
+              "sha256": "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+            }
+          }
+        },
+        {
+          "name": "foo",
+          "version": "1.2.4",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "98405f2be818d57d0c01a1252e6bc50c1db9bb69b90557909740083019f214cd"
+            }
+          },
+          "constraintBumped": "^1.0.0",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "https://pub.dev",
+              "sha256": "0a2eec28c63c8ee616670da9bae0dbfe9294f5b04b264cc4a9be24876ea23d3b"
+            }
+          }
+        }
+      ],
+      "singleBreaking": [],
+      "multiBreaking": [
+        {
+          "name": "bar",
+          "version": "1.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT",
+              "sha256": "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+            }
+          },
+          "constraintBumped": "^1.2.3",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "https://pub.dev",
+              "sha256": "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+            }
+          }
+        },
+        {
+          "name": "foo",
+          "version": "1.2.4",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "98405f2be818d57d0c01a1252e6bc50c1db9bb69b90557909740083019f214cd"
+            }
+          },
+          "constraintBumped": "^1.2.4",
+          "constraintWidened": "^1.0.0",
+          "constraintBumpedIfNeeded": "^1.0.0",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "https://pub.dev",
+              "sha256": "0a2eec28c63c8ee616670da9bae0dbfe9294f5b04b264cc4a9be24876ea23d3b"
+            }
+          }
+        }
+      ]
+    }
+  ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section apply
+$ echo '{"dependencyChanges":[{"name":"foo","version":"1.2.4"}]}' | dependency_services apply
+{"dependencies":[]}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0","bar":"^1.0.0"},"environment":{"sdk":"^3.0.2"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  bar:
+    dependency: "direct main"
+    description:
+      name: bar
+      sha256: "0a2afc2a042366f06a89e07dfe845d3c77c07534fe489d5c4d8e8a8bdcbf0c22"
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.3"
+  foo:
+    dependency: "direct main"
+    description:
+      name: foo
+      sha256: "98405f2be818d57d0c01a1252e6bc50c1db9bb69b90557909740083019f214cd"
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.4"
+sdks:
+  dart: ">=3.0.2 <4.0.0"