2424import java .nio .file .Files ;
2525import java .nio .file .Path ;
2626import java .util .Collections ;
27+ import java .util .Comparator ;
2728import java .util .HashMap ;
2829import java .util .LinkedList ;
2930import java .util .List ;
3031import java .util .Map ;
3132import java .util .Objects ;
33+ import java .util .Optional ;
34+ import land .oras .ContainerRef ;
3235import land .oras .exception .OrasException ;
3336import land .oras .utils .TomlUtils ;
3437import org .jspecify .annotations .NullMarked ;
@@ -94,12 +97,14 @@ public static RegistriesConf newConf() {
9497
9598 /**
9699 * The model of the registry configuration
100+ * @param prefix The prefix to match against container references.
97101 * @param location The registry location
98102 * @param blocked Whether the registry is blocked. If true, the registry is blocked and cannot be used for pulling or pushing images.
99103 * @param insecure Whether the registry is insecure. If true, the registry is considered insecure and may allow connections over HTTP or with invalid TLS certificates.
100104 */
101105 record RegistryConfig (
102- @ JsonProperty ("location" ) String location ,
106+ @ Nullable @ JsonProperty ("prefix" ) String prefix ,
107+ @ Nullable @ JsonProperty ("location" ) String location ,
103108 @ Nullable @ JsonProperty ("blocked" ) Boolean blocked ,
104109 @ Nullable @ JsonProperty ("insecure" ) Boolean insecure ) {
105110 public boolean isBlocked () {
@@ -112,8 +117,26 @@ public boolean isInsecure() {
112117 }
113118
114119 /**
115- * The model of the config file
116- *
120+ * The model of the parsed prefix, which contains the host and path components of the prefix.
121+ * @param host The host component of the prefix, which can be a specific hostname or a wildcard pattern (e.g., *.example.com).
122+ * @param path The path component of the prefix, which can be a specific path or a path prefix (e.g., namespace/repo).
123+ */
124+ record ParsedPrefix (String host , String path ) {
125+
126+ static ParsedPrefix parse (String prefix ) {
127+ int slash = prefix .indexOf ('/' );
128+ if (slash < 0 ) {
129+ return new ParsedPrefix (prefix , "" );
130+ }
131+ return new ParsedPrefix (prefix .substring (0 , slash ), prefix .substring (slash + 1 ));
132+ }
133+ }
134+
135+ /**
136+ * The model of the configuration file, which contains the list of registry configurations, aliases, and unqualified registries.
137+ * @param registries The list of registry configurations, each containing the registry location, whether it is blocked, and whether it is insecure.
138+ * @param aliases The map of registry aliases, where the key is the alias and the value is the actual registry URL.
139+ * @param unqualifiedRegistries The list of unqualified registries, which are registries that can be used without specifying a registry.
117140 */
118141 record ConfigFile (
119142 @ JsonProperty ("registry" ) @ Nullable List <RegistryConfig > registries ,
@@ -150,21 +173,95 @@ public boolean hasAlias(String alias) {
150173 * @param location the registry location to check for blocking.
151174 * @return true if the registry is marked as blocked, false otherwise.
152175 */
153- public boolean isBlocked (String location ) {
154- return config .registries .stream ()
155- .filter (registry -> registry .location .equals (location ))
156- .anyMatch (RegistryConfig ::isBlocked );
176+ public boolean isBlocked (ContainerRef location ) {
177+ return selectMatchingTable (location ).map (RegistryConfig ::isBlocked ).orElse (false );
157178 }
158179
159180 /**
160181 * Check if the given registry is marked as insecure in the configuration.
161182 * @param location the registry location to check for insecurity.
162183 * @return true if the registry is marked as insecure, false otherwise.
163184 */
164- public boolean isInsecure (String location ) {
185+ public boolean isInsecure (ContainerRef location ) {
186+ return selectMatchingTable (location ).map (RegistryConfig ::isInsecure ).orElse (false );
187+ }
188+
189+ /**
190+ * Rewrite the given container reference according to the matching registry configuration.
191+ * @param ref the container reference to rewrite.
192+ * @return the rewritten container reference.
193+ */
194+ public ContainerRef rewrite (ContainerRef ref ) {
195+ Optional <RegistryConfig > matchingConfig = selectMatchingTable (ref );
196+ if (matchingConfig .isEmpty ()) {
197+ return ref ;
198+ }
199+ // No rewrite possible if location and prefix are not set
200+ String registry = matchingConfig .get ().location ();
201+ String prefix = matchingConfig .get ().prefix ();
202+ if (registry == null || registry .isBlank () || prefix == null || prefix .isBlank ()) {
203+ return ref ;
204+ }
205+ String currentRefString = ref .toString ();
206+ String rewrittenRefString = currentRefString .replaceFirst (prefix , registry );
207+ LOG .debug (
208+ "Rewriting container reference from '{}' to '{}' using registry config with prefix '{}' and location '{}'" ,
209+ currentRefString ,
210+ rewrittenRefString ,
211+ prefix ,
212+ registry );
213+ return ContainerRef .parse (rewrittenRefString );
214+ }
215+
216+ /**
217+ * Select the matching registry configuration table for the container reference.
218+ * @param ref the container reference to find the matching registry configuration for.
219+ * @return an Optional containing the matching RegistryConfig if found, or an empty Optional if no matching configuration is found.
220+ */
221+ private Optional <RegistryConfig > selectMatchingTable (ContainerRef ref ) {
165222 return config .registries .stream ()
166- .filter (registry -> registry .location .equals (location ))
167- .anyMatch (RegistryConfig ::isInsecure );
223+ .filter (cfg -> matches (ref , effectivePrefix (cfg )))
224+ .max (Comparator .comparingInt (cfg -> effectivePrefix (cfg ).length ()));
225+ }
226+
227+ private @ Nullable String effectivePrefix (RegistryConfig cfg ) {
228+ return cfg .prefix () != null ? cfg .prefix () : cfg .location ();
229+ }
230+
231+ /**
232+ * Check if the given container reference matches the specified prefix.
233+ * @param ref the container reference to check for a match against the prefix.
234+ * @param prefix the prefix to match against the container reference, which can be a specific hostname or a wildcard pattern (e.g., *.example.com) and an optional path component (e.g., namespace/repo).
235+ * @return true if the container reference matches the prefix, false otherwise.
236+ */
237+ private boolean matches (ContainerRef ref , @ Nullable String prefix ) {
238+ if (prefix == null || prefix .isBlank ()) {
239+ return false ;
240+ }
241+
242+ ParsedPrefix p = ParsedPrefix .parse (prefix );
243+
244+ // Host match (supports *.example.com)
245+ if (!hostMatches (ref .getRegistry (), p .host ())) {
246+ return false ;
247+ }
248+
249+ // No path restriction → host-only match
250+ if (p .path ().isEmpty ()) {
251+ return true ;
252+ }
253+
254+ // Path prefix match (namespace/repo)
255+ String refPath = String .join ("/" , ref .getNamespace ()) + "/" + ref .getRepository ();
256+ return refPath .equals (p .path ()) || refPath .startsWith (p .path () + "/" );
257+ }
258+
259+ private boolean hostMatches (String host , String prefixHost ) {
260+ if (prefixHost .startsWith ("*." )) {
261+ String domain = prefixHost .substring (2 );
262+ return host .endsWith ("." + domain );
263+ }
264+ return host .equals (prefixHost );
168265 }
169266
170267 /**
@@ -177,6 +274,14 @@ static class Config {
177274 */
178275 private Config () {}
179276
277+ /**
278+ * Constructor for Config that takes a RegistryConfig and adds it to the list of registries.
279+ * @param registryConfigs The registry configuration to add to the list of registries.
280+ */
281+ Config (List <RegistryConfig > registryConfigs ) {
282+ this .registries .addAll (registryConfigs );
283+ }
284+
180285 /**
181286 * List of unqualified registries.
182287 */
0 commit comments