Refactor ParseImage and ParseRepository (#41154)

David Bennett committed Jul 24, 2026 at 13:28 UTC acb5fdc4c0cad222dfff2626346b06b8b9626d16
7 files changed +166 -97
src/windows/common/wslutil.cpp
+38 -38
@@ -1245,7 +1245,7 @@ std::vector<DWORD> wsl::windows::common::wslutil::ListRunningProcesses()
1245 return pids;
1246 }
1247
1248 -std::pair<std::string, std::string> wsl::windows::common::wslutil::NormalizeRepo(const std::string& Input)
1248 +wsl::windows::common::wslutil::RepositoryReference wsl::windows::common::wslutil::RepositoryReference::Parse(const std::string& input)
1249 {
1250 // See: https://github.com/distribution/reference/blob/ff14fafe2236e51c2894ac07d4bdfc778e96d682/normalize.go#L126
1251
@@ -1254,14 +1254,14 @@ std::pair<std::string, std::string> wsl::windows::common::wslutil::NormalizeRepo
1254 constexpr auto legacyDomain = "index.docker.io";
1255 constexpr auto localhost = "localhost";
1256
1257 - auto slash = Input.find('/');
1257 + auto slash = input.find('/');
1258 if (slash == std::string::npos)
1259 {
1260 - return {defaultDomain, officialPrefix + Input};
1260 + return RepositoryReference{input, defaultDomain, officialPrefix + input};
1261 }
1262
1263 - auto domain = Input.substr(0, slash);
1264 - auto path = Input.substr(slash + 1);
1263 + auto domain = input.substr(0, slash);
1264 + auto path = input.substr(slash + 1);
1265
1266 if (domain == legacyDomain)
1267 {
@@ -1272,7 +1272,7 @@ std::pair<std::string, std::string> wsl::windows::common::wslutil::NormalizeRepo
1272 }))
1273 {
1274 domain = defaultDomain;
1275 - path = Input;
1275 + path = input;
1276 }
1277
1278 if (domain == defaultDomain && path.find('/') == std::string::npos)
@@ -1280,7 +1280,12 @@ std::pair<std::string, std::string> wsl::windows::common::wslutil::NormalizeRepo
1280 path = "library/" + path;
1281 }
1282
1283 - return {domain, path};
1283 + return RepositoryReference{input, std::move(domain), std::move(path)};
1284 +}
1285 +
1286 +std::string wsl::windows::common::wslutil::RepositoryReference::GetCanonical() const
1287 +{
1288 + return std::format("{}/{}", Server, Path);
1289 }
1290
1291 std::pair<wil::unique_hfile, wil::unique_hfile> wsl::windows::common::wslutil::OpenAnonymousPipe(DWORD Size, bool ReadPipeOverlapped, bool WritePipeOverlapped)
@@ -1397,63 +1402,58 @@ std::tuple<uint32_t, uint32_t, uint32_t> wsl::windows::common::wslutil::ParseWsl
1402 }
1403 }
1404
1400 -std::pair<std::string, std::optional<std::string>> wsl::windows::common::wslutil::ParseImage(const std::string& Input, EnumReferenceFormat* Format)
1405 +wsl::windows::common::wslutil::ImageReference wsl::windows::common::wslutil::ImageReference::Parse(const std::string& input)
1406 {
1407 static const auto regex = BuildImageReferenceRegex();
1408 std::smatch match;
1404 - if (!std::regex_match(Input, match, regex))
1409 + if (!std::regex_match(input, match, regex))
1410 {
1406 - THROW_HR_WITH_USER_ERROR(E_INVALIDARG, wsl::shared::Localization::MessageWslcInvalidImage(Input.c_str()));
1411 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, wsl::shared::Localization::MessageWslcInvalidImage(input.c_str()));
1412 }
1413
1414 const auto& repo = match[1];
1415 const auto& tag = match[2];
1416 const auto& digest = match[3];
1417
1413 - THROW_HR_IF_MSG(E_UNEXPECTED, !repo.matched, "Unexpected regex match. Input: %hs", Input.c_str());
1418 + THROW_HR_IF_MSG(E_UNEXPECTED, !repo.matched, "Unexpected regex match. Input: %hs", input.c_str());
1419
1415 - EnumReferenceFormat referenceFormat = EnumReferenceFormatNone;
1416 - std::optional<std::string> tagOrDigest;
1417 - if (digest.matched) // <repo>:[tag]@<digest> (If both digest and tag are specified, digest takes precedence).
1420 + std::optional<std::string> tagValue;
1421 + if (tag.matched)
1422 {
1419 - tagOrDigest = digest.str();
1420 - referenceFormat = EnumReferenceFormatDigest;
1423 + tagValue = tag.str();
1424 }
1422 - else if (tag.matched) // <repo>:<tag>
1425 +
1426 + std::optional<std::string> digestValue;
1427 + if (digest.matched)
1428 {
1424 - tagOrDigest = tag.str();
1425 - referenceFormat = EnumReferenceFormatTag;
1429 + digestValue = digest.str();
1430 }
1431
1428 - if (Format)
1432 + // Classify the reference the way the Docker CLI does, where a digest takes precedence over a tag.
1433 + EnumReferenceFormat format = EnumReferenceFormatNone;
1434 + if (digestValue.has_value())
1435 + {
1436 + format = EnumReferenceFormatDigest;
1437 + }
1438 + else if (tagValue.has_value())
1439 {
1430 - *Format = referenceFormat;
1440 + format = EnumReferenceFormatTag;
1441 }
1442
1433 - return {repo.str(), std::move(tagOrDigest)};
1443 + return ImageReference{RepositoryReference::Parse(repo.str()), std::move(tagValue), std::move(digestValue), format};
1444 }
1445
1436 -std::string wsl::windows::common::wslutil::GetCanonicalImageReference(const std::string& input)
1446 +std::string wsl::windows::common::wslutil::ImageReference::GetCanonical() const
1447 {
1438 - // Mirror the Docker CLI's client-side reference normalization so the final line matches `docker pull` exactly.
1448 + // Mirror the Docker CLI's client-side reference normalization so the result matches `docker pull` exactly.
1449 // See github.com/distribution/reference (normalize.go, reference.go) and github.com/docker/cli
1440 - // (cli/command/image/pull.go). Unlike ParseImage -- which collapses to a single tag-or-digest field with digest
1441 - // precedence -- Docker's canonical string keeps both a tag and a digest when both are present, so compose the
1442 - // reference directly from the parsed name, tag and digest groups.
1443 - static const auto regex = BuildImageReferenceRegex();
1444 - std::smatch match;
1445 - if (!std::regex_match(input, match, regex))
1446 - {
1447 - THROW_HR_WITH_USER_ERROR(E_INVALIDARG, wsl::shared::Localization::MessageWslcInvalidImage(input.c_str()));
1448 - }
1449 -
1450 - auto [domain, path] = NormalizeRepo(match[1].str());
1450 + // (cli/command/image/pull.go). Docker's canonical string keeps both a tag and a digest when both are present.
1451
1452 // A tag joins with ':' and a digest with '@'. A name-only reference (no tag and no digest) defaults to ":latest";
1453 // a digest-only reference is not name-only, so it keeps no tag (matching Docker's TagNameOnly).
1454 - const std::string tag = match[2].matched ? std::format(":{}", match[2].str()) : (match[3].matched ? "" : ":latest");
1455 - const std::string digest = match[3].matched ? std::format("@{}", match[3].str()) : "";
1456 - return std::format("{}/{}{}{}", domain, path, tag, digest);
1454 + const std::string tag = Tag ? std::format(":{}", *Tag) : (Digest ? "" : ":latest");
1455 + const std::string digest = Digest ? std::format("@{}", *Digest) : "";
1456 + return std::format("{}{}{}", Repository.GetCanonical(), tag, digest);
1457 }
1458
1459 void wsl::windows::common::wslutil::PrintSystemError(_In_ HRESULT result, _Inout_ FILE* const stream)
src/windows/common/wslutil.h
+39 -6
@@ -222,10 +222,6 @@ ErrorStrings ErrorToString(const Error& error);
222
223 std::filesystem::path GetBasePath();
224
225 -// Returns the fully-qualified canonical image reference for the given input, matching the string
226 -// printed by `docker pull` (e.g. "ubuntu" -> "docker.io/library/ubuntu:latest").
227 -std::string GetCanonicalImageReference(const std::string& input);
228 -
225 std::optional<COMErrorInfo> GetCOMErrorInfo();
226
227 DWORD GetDefaultVersion(void);
@@ -268,7 +264,22 @@ bool IsVirtualMachinePlatformInstalled();
264
265 std::vector<DWORD> ListRunningProcesses();
266
271 -std::pair<std::string, std::string> NormalizeRepo(const std::string& Input);
267 +// An immutable container repository reference. Holds the original repository token together with its normalized
268 +// registry server and path, following Docker's client-side normalization (e.g. "ubuntu" -> {"docker.io",
269 +// "library/ubuntu"}). Construct one with Parse(). Normalization is lossy, so the original Name is retained for
270 +// callers that must echo the repository exactly as it was written.
271 +struct RepositoryReference
272 +{
273 + const std::string Name;
274 + const std::string Server;
275 + const std::string Path;
276 +
277 + // Split and normalize a repository string into its registry server and path.
278 + static RepositoryReference Parse(const std::string& repository);
279 +
280 + // The fully-qualified "server/path" form (e.g. "docker.io/library/ubuntu").
281 + std::string GetCanonical() const;
282 +};
283
284 std::pair<wil::unique_hfile, wil::unique_hfile> OpenAnonymousPipe(DWORD Size, bool ReadPipeOverlapped, bool WritePipeOverlapped);
285
@@ -280,7 +291,29 @@ void ParseIpv6Address(const char* Address, in_addr6& Result);
291
292 std::tuple<uint32_t, uint32_t, uint32_t> ParseWslPackageVersion(_In_ const std::wstring& Version);
293
283 -std::pair<std::string, std::optional<std::string>> ParseImage(const std::string& Input, EnumReferenceFormat* Format = nullptr);
294 +// A parsed, immutable image reference such as "ubuntu:22.04@sha256:...". Construct one with Parse().
295 +struct ImageReference
296 +{
297 + const RepositoryReference Repository;
298 + const std::optional<std::string> Tag;
299 + const std::optional<std::string> Digest;
300 + const EnumReferenceFormat Format;
301 +
302 + // Parse an image reference string into its components. Throws E_INVALIDARG (with a user-facing error) when the
303 + // reference is malformed.
304 + static ImageReference Parse(const std::string& input);
305 +
306 + // Collapse the reference to a single tag-or-digest field, where a digest takes precedence over a tag. This matches
307 + // how callers that resolve, pull or push a single reference treat the two.
308 + std::optional<std::string> TagOrDigest() const
309 + {
310 + return Digest.has_value() ? Digest : Tag;
311 + }
312 +
313 + // The fully-qualified canonical reference, matching the string printed by `docker pull`
314 + // (e.g. "ubuntu" -> "docker.io/library/ubuntu:latest").
315 + std::string GetCanonical() const;
316 +};
317
318 void PrintSystemError(_In_ HRESULT result, _Inout_ FILE* stream = stdout);
319
src/windows/wslc/services/ImageService.cpp
+8 -11
@@ -68,9 +68,7 @@ wil::unique_hfile ResolveBuildFile(const std::filesystem::path& contextPath)
68
69 std::string GetServerFromImage(const std::string& image)
70 {
71 - auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(image);
72 - auto [server, path] = wsl::windows::common::wslutil::NormalizeRepo(repo);
73 - return server;
71 + return wsl::windows::common::wslutil::ImageReference::Parse(image).Repository.Server;
72 }
73
74 struct InputSource
@@ -217,9 +215,9 @@ std::vector<ImageInformation> ImageService::List(
215 std::string imageRef = image.Image;
216 if (imageRef != "<none>:<none>")
217 {
220 - auto parsed = wsl::windows::common::wslutil::ParseImage(imageRef);
221 - info.Repository = parsed.first;
222 - info.Tag = parsed.second;
218 + auto parsed = wsl::windows::common::wslutil::ImageReference::Parse(imageRef);
219 + info.Repository = parsed.Repository.Name;
220 + info.Tag = parsed.TagOrDigest();
221 }
222
223 info.Id = image.Hash;
@@ -277,17 +275,16 @@ void ImageService::Pull(Reporter& reporter, wsl::windows::wslc::models::Session&
275
276 void ImageService::Tag(wsl::windows::wslc::models::Session& session, const std::string& sourceImage, const std::string& targetImage)
277 {
280 - EnumReferenceFormat format;
281 - auto [repo, tag] = ParseImage(targetImage, &format);
282 - if (format == EnumReferenceFormatDigest)
278 + auto reference = ImageReference::Parse(targetImage);
279 + if (reference.Format == EnumReferenceFormatDigest)
280 {
281 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcTagImageInvalidFormat(targetImage.c_str()));
282 }
283
284 WSLCTagImageOptions options{};
285 options.Image = sourceImage.c_str();
289 - options.Repo = repo.c_str();
290 - options.Tag = tag ? tag->c_str() : "";
286 + options.Repo = reference.Repository.Name.c_str();
287 + options.Tag = reference.Tag ? reference.Tag->c_str() : "";
288
289 THROW_IF_FAILED(session.Get()->TagImage(&options));
290 }
src/windows/wslc/tasks/ImageTasks.cpp
+10 -6
@@ -228,21 +228,25 @@ void PullImage(CLIExecutionContext& context)
228
229 // Match `docker pull`: for a name-only reference (no tag or digest) the tag defaults to "latest". Unless quiet,
230 // the client reports this on stdout before contacting the registry.
231 - EnumReferenceFormat format = EnumReferenceFormatNone;
232 - ParseImage(image, &format);
233 - if (!quiet && format == EnumReferenceFormatNone)
231 + const auto reference = ImageReference::Parse(image);
232 + if (!quiet && reference.Format == EnumReferenceFormatNone)
233 {
234 context.Reporter.Output(L"{}\n", Localization::WSLCCLI_PullUsingDefaultTag(L"latest"));
235 }
236
237 // Match `docker pull`: in quiet mode, suppress progress output by passing no progress callback. Warnings are
238 // unaffected because the warning callback is built internally by ImageService::Pull from the Reporter.
240 - ImageProgressCallback callback(context.Reporter, Reporter::Level::Output);
241 - IProgressCallback* progress = quiet ? nullptr : &callback;
239 + std::optional<ImageProgressCallback> callback;
240 + if (!quiet)
241 + {
242 + callback.emplace(context.Reporter, Reporter::Level::Output);
243 + }
244 +
245 + IProgressCallback* progress = callback ? &*callback : nullptr;
246 services::ImageService::Pull(context.Reporter, session, image, progress);
247
248 // Match `docker pull`: always print the resolved canonical image reference as the final line.
245 - context.Reporter.Output(L"{}\n", MultiByteToWide(GetCanonicalImageReference(image)));
249 + context.Reporter.Output(L"{}\n", MultiByteToWide(reference.GetCanonical()));
250 }
251
252 void PushImage(CLIExecutionContext& context)
src/windows/wslcsession/DockerHTTPClient.cpp
+1 -2
@@ -125,8 +125,7 @@ std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::PullImag
125 auto url = URL::Create("/images/create");
126
127 // Normalize the repo server & path
128 - auto [server, path] = wslutil::NormalizeRepo(Repo);
129 - url.SetParameter("fromImage", std::format("{}/{}", server, path));
128 + url.SetParameter("fromImage", wslutil::RepositoryReference::Parse(Repo).GetCanonical());
129
130 if (tagOrDigest.has_value())
131 {
src/windows/wslcsession/WSLCSession.cpp
+16 -12
@@ -48,13 +48,12 @@ namespace {
48 // Group policy: WSLContainerRegistryAllowlist restricts which container-image
49 // registries can be pulled from or pushed to. The check is enforced here at the
50 // service boundary so it covers ALL callers (wslc.exe CLI, the WslcSDK C API, and
51 -// any other COM client). The repo argument must be the parsed repo from
52 -// wslutil::ParseImage so callers don't pay the regex cost twice.
53 -void EnforceRegistryAllowlist(const std::string& Repo)
51 +// any other COM client). Callers pass the parsed repository so no reference is
52 +// parsed twice.
53 +void EnforceRegistryAllowlist(const wslutil::RepositoryReference& Repository)
54 {
55 const auto policiesKey = wsl::windows::policies::OpenPoliciesKey();
56 - auto [server, path] = wsl::windows::common::wslutil::NormalizeRepo(Repo);
57 - const auto serverWide = wsl::shared::string::MultiByteToWide(server);
56 + const auto serverWide = wsl::shared::string::MultiByteToWide(Repository.Server);
57
58 if (wsl::windows::policies::IsRegistryAllowed(policiesKey.get(), serverWide))
59 {
@@ -842,7 +841,9 @@ try
841
842 RETURN_HR_IF_NULL(E_POINTER, Image);
843
845 - auto [repo, tagOrDigest] = wslutil::ParseImage(Image);
844 + const auto reference = wslutil::ImageReference::Parse(Image);
845 + const auto& repo = reference.Repository;
846 + auto tagOrDigest = reference.TagOrDigest();
847 EnforceRegistryAllowlist(repo);
848
849 auto lock = m_lock.lock_shared();
@@ -860,7 +861,7 @@ try
861 registryAuth = std::string(RegistryAuthenticationInformation);
862 }
863
863 - auto requestContext = m_dockerClient->PullImage(repo, tagOrDigest, registryAuth);
864 + auto requestContext = m_dockerClient->PullImage(repo.Name, tagOrDigest, registryAuth);
865 StreamImageOperation(*requestContext, Image, "Pull", ProgressCallback);
866
867 OnImageCreated(Image);
@@ -1259,9 +1260,10 @@ try
1260 {
1261 RETURN_HR_IF(E_INVALIDARG, strlen(ImageName) > WSLC_MAX_IMAGE_NAME_LENGTH);
1262
1262 - auto [parsedRepo, tagOrDigest] = wslutil::ParseImage(ImageName);
1263 + auto reference = wslutil::ImageReference::Parse(ImageName);
1264 + auto tagOrDigest = reference.TagOrDigest();
1265 THROW_HR_IF_MSG(E_INVALIDARG, !tagOrDigest.has_value(), "Expected tag for image import: %hs", ImageName);
1264 - repo = parsedRepo;
1266 + repo = reference.Repository.Name;
1267 tag = tagOrDigest.value();
1268 }
1269
@@ -1607,7 +1609,7 @@ try
1609
1610 // Extract repo name from tag (format: "repo:tag")
1611 // and lookup corresponding digest from the map
1610 - auto repoName = wslutil::ParseImage(tag).first;
1612 + auto repoName = wslutil::ImageReference::Parse(tag).Repository.Name;
1613 auto it = repoToDigest.find(repoName);
1614 if (it != repoToDigest.end())
1615 {
@@ -1761,13 +1763,15 @@ try
1763 RETURN_HR_IF_NULL(E_POINTER, Image);
1764 RETURN_HR_IF_NULL(E_POINTER, RegistryAuthenticationInformation);
1765
1764 - auto [repo, tagOrDigest] = wslutil::ParseImage(Image);
1766 + const auto reference = wslutil::ImageReference::Parse(Image);
1767 + const auto& repo = reference.Repository;
1768 + auto tagOrDigest = reference.TagOrDigest();
1769 EnforceRegistryAllowlist(repo);
1770
1771 auto lock = m_lock.lock_shared();
1772 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1773
1770 - auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, RegistryAuthenticationInformation);
1774 + auto requestContext = m_dockerClient->PushImage(repo.Name, tagOrDigest, RegistryAuthenticationInformation);
1775 StreamImageOperation(*requestContext, Image, "Push", ProgressCallback);
1776
1777 return S_OK;
test/windows/WSLCTests.cpp
+54 -22
@@ -199,7 +199,9 @@ class WSLCTests
199
200 std::string PushImageToRegistry(const std::string& imageName, const std::string& registryAddress, const std::string& registryAuth)
201 {
202 - auto [repo, tag] = ParseImage(imageName);
202 + auto reference = ImageReference::Parse(imageName);
203 + const auto& repo = reference.Repository.Name;
204 + auto tag = reference.TagOrDigest();
205 auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag.value_or("latest"));
206 auto registryRepo = std::format("{}/{}", registryAddress, repo);
207 auto registryTag = tag.value_or("latest");
@@ -11869,12 +11871,37 @@ class WSLCTests
11871
11872 TEST_METHOD(ImageParsing)
11873 {
11872 - using wsl::windows::common::wslutil::ParseImage;
11874 + auto ValidateImageParsing = [](const std::string& input,
11875 + const std::string& expectedRepo,
11876 + const std::optional<std::string>& expectedTag,
11877 + const std::optional<std::string>& expectedDigest = std::nullopt) {
11878 + auto reference = ImageReference::Parse(input);
11879
11874 - auto ValidateImageParsing = [](const std::string& input, const std::string& expectedRepo, const std::optional<std::string>& expectedTag) {
11875 - auto [repo, tag] = ParseImage(input);
11876 - VERIFY_ARE_EQUAL(repo, expectedRepo);
11877 - VERIFY_ARE_EQUAL(tag.value_or("<empty>"), expectedTag.value_or("<empty>"));
11880 + // The repository is parsed into a RepositoryReference; Name preserves the original token while Server and
11881 + // Path hold its normalized form.
11882 + const auto expectedRepository = RepositoryReference::Parse(expectedRepo);
11883 + VERIFY_ARE_EQUAL(reference.Repository.Name, expectedRepository.Name);
11884 + VERIFY_ARE_EQUAL(reference.Repository.Server, expectedRepository.Server);
11885 + VERIFY_ARE_EQUAL(reference.Repository.Path, expectedRepository.Path);
11886 + VERIFY_ARE_EQUAL(reference.Tag.value_or("<empty>"), expectedTag.value_or("<empty>"));
11887 + VERIFY_ARE_EQUAL(reference.Digest.value_or("<empty>"), expectedDigest.value_or("<empty>"));
11888 +
11889 + // TagOrDigest() collapses to a single field where a digest takes precedence over a tag.
11890 + const std::optional<std::string> expectedTagOrDigest = expectedDigest.has_value() ? expectedDigest : expectedTag;
11891 + VERIFY_ARE_EQUAL(reference.TagOrDigest().value_or("<empty>"), expectedTagOrDigest.value_or("<empty>"));
11892 +
11893 + // Format mirrors that same classification.
11894 + EnumReferenceFormat expectedFormat = EnumReferenceFormatNone;
11895 + if (expectedDigest.has_value())
11896 + {
11897 + expectedFormat = EnumReferenceFormatDigest;
11898 + }
11899 + else if (expectedTag.has_value())
11900 + {
11901 + expectedFormat = EnumReferenceFormatTag;
11902 + }
11903 +
11904 + VERIFY_ARE_EQUAL(reference.Format, expectedFormat);
11905 };
11906
11907 ValidateImageParsing("ubuntu:22.04", "ubuntu", "22.04");
@@ -11889,47 +11916,54 @@ class WSLCTests
11916 ValidateImageParsing("localhost:5000/myimage:latest", "localhost:5000/myimage", "latest");
11917 ValidateImageParsing("ghcr.io/owner/repo:sha-abc123", "ghcr.io/owner/repo", "sha-abc123");
11918
11919 + // A digest-only reference populates the digest field and leaves the tag empty.
11920 ValidateImageParsing(
11921 "ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30",
11922 "ubuntu",
11923 + {},
11924 "sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30");
11925
11897 - // Validate that the digest takes precedence over the tag.
11926 + // A reference with both a tag and a digest captures each in its own field.
11927 ValidateImageParsing(
11928 "ubuntu:latest@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30",
11929 "ubuntu",
11930 + "latest",
11931 "sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30");
11932
11933 ValidateImageParsing(
11934 "myregistry.io:5000/myimage@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30",
11935 "myregistry.io:5000/myimage",
11936 + {},
11937 "sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30");
11938
11939 ValidateImageParsing(
11940 "ubuntu:22.04@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30",
11941 "ubuntu",
11942 + "22.04",
11943 "sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30");
11944
11945 ValidateImageParsing("pytorch/pytorch", "pytorch/pytorch", {});
11946
11947 // Invalid inputs
11916 - VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage(""); }), E_INVALIDARG);
11917 - VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage(":debian:latest"); }), E_INVALIDARG);
11918 - VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage("debian:latest@"); }), E_INVALIDARG);
11919 - VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage(""); }), E_INVALIDARG);
11920 - VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage(":"); }), E_INVALIDARG);
11921 - VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage("a:"); }), E_INVALIDARG);
11922 - VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage(":b"); }), E_INVALIDARG);
11948 + VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ImageReference::Parse(""); }), E_INVALIDARG);
11949 + VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ImageReference::Parse(":debian:latest"); }), E_INVALIDARG);
11950 + VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ImageReference::Parse("debian:latest@"); }), E_INVALIDARG);
11951 + VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ImageReference::Parse(""); }), E_INVALIDARG);
11952 + VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ImageReference::Parse(":"); }), E_INVALIDARG);
11953 + VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ImageReference::Parse("a:"); }), E_INVALIDARG);
11954 + VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ImageReference::Parse(":b"); }), E_INVALIDARG);
11955 }
11956
11957 TEST_METHOD(RepoParsing)
11958 {
11927 - using wsl::windows::common::wslutil::NormalizeRepo;
11928 -
11959 auto ValidateRepoParsing = [](const std::string& input, const std::string& expectedServer, const std::string& expectedPath) {
11930 - auto [server, path] = NormalizeRepo(input);
11931 - VERIFY_ARE_EQUAL(server, expectedServer);
11932 - VERIFY_ARE_EQUAL(path, expectedPath);
11960 + auto repository = RepositoryReference::Parse(input);
11961 + VERIFY_ARE_EQUAL(repository.Name, input);
11962 + VERIFY_ARE_EQUAL(repository.Server, expectedServer);
11963 + VERIFY_ARE_EQUAL(repository.Path, expectedPath);
11964 +
11965 + // GetCanonical() rejoins the normalized server and path.
11966 + VERIFY_ARE_EQUAL(repository.GetCanonical(), std::format("{}/{}", expectedServer, expectedPath));
11967 };
11968
11969 ValidateRepoParsing("ubuntu", "docker.io", "library/ubuntu");
@@ -11949,10 +11983,8 @@ class WSLCTests
11983
11984 TEST_METHOD(CanonicalImageReference)
11985 {
11952 - using wsl::windows::common::wslutil::GetCanonicalImageReference;
11953 -
11986 auto Validate = [](const std::string& input, const std::string& expected) {
11955 - VERIFY_ARE_EQUAL(GetCanonicalImageReference(input), expected);
11987 + VERIFY_ARE_EQUAL(ImageReference::Parse(input).GetCanonical(), expected);
11988 };
11989
11990 // Name-only references default to ":latest" and the docker.io/library prefix (matches `docker pull` output).