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).