diff --git a/core/src/main/java/google/registry/bsa/IdnChecker.java b/core/src/main/java/google/registry/bsa/IdnChecker.java
new file mode 100644
index 00000000000..d97a5345f24
--- /dev/null
+++ b/core/src/main/java/google/registry/bsa/IdnChecker.java
@@ -0,0 +1,108 @@
+// Copyright 2023 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.bsa;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.Maps.transformValues;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
+import google.registry.model.tld.Tld;
+import google.registry.model.tld.Tld.TldType;
+import google.registry.model.tld.Tlds;
+import google.registry.tldconfig.idn.IdnLabelValidator;
+import google.registry.tldconfig.idn.IdnTableEnum;
+import google.registry.util.Clock;
+import javax.inject.Inject;
+import org.joda.time.DateTime;
+
+/**
+ * Checks labels' validity wrt Idns in TLDs enrolled with BSA.
+ *
+ *
Each instance takes a snapshot of the TLDs at instantiation time, and should be limited to the
+ * Request scope.
+ */
+public class IdnChecker {
+ private static final IdnLabelValidator IDN_LABEL_VALIDATOR = new IdnLabelValidator();
+
+ private final ImmutableMap> idnToTlds;
+ private final ImmutableSet allTlds;
+
+ @Inject
+ IdnChecker(Clock clock) {
+ this.idnToTlds = getIdnToTldMap(clock.nowUtc());
+ allTlds = idnToTlds.values().stream().flatMap(ImmutableSet::stream).collect(toImmutableSet());
+ }
+
+ // TODO(11/30/2023): Remove below when new Tld schema is deployed and the `getBsaEnrollStartTime`
+ // method is no longer hardcoded.
+ @VisibleForTesting
+ IdnChecker(ImmutableMap> idnToTlds) {
+ this.idnToTlds = idnToTlds;
+ allTlds = idnToTlds.values().stream().flatMap(ImmutableSet::stream).collect(toImmutableSet());
+ }
+
+ /** Returns all IDNs in which the {@code label} is valid. */
+ ImmutableSet getAllValidIdns(String label) {
+ return idnToTlds.keySet().stream()
+ .filter(idnTable -> idnTable.getTable().isValidLabel(label))
+ .collect(toImmutableSet());
+ }
+
+ /**
+ * Returns the TLDs that support at least one IDN in the {@code idnTables}.
+ *
+ * @param idnTables String names of {@link IdnTableEnum} values
+ */
+ public ImmutableSet getSupportingTlds(ImmutableSet idnTables) {
+ return idnTables.stream()
+ .map(IdnTableEnum::valueOf)
+ .filter(idnToTlds::containsKey)
+ .map(idnToTlds::get)
+ .flatMap(ImmutableSet::stream)
+ .collect(toImmutableSet());
+ }
+
+ /**
+ * Returns the TLDs that do not support any IDN in the {@code idnTables}.
+ *
+ * @param idnTables String names of {@link IdnTableEnum} values
+ */
+ public SetView getForbiddingTlds(ImmutableSet idnTables) {
+ return Sets.difference(allTlds, getSupportingTlds(idnTables));
+ }
+
+ private static boolean isEnrolledWithBsa(Tld tld, DateTime now) {
+ DateTime enrollTime = tld.getBsaEnrollStartTime();
+ return enrollTime != null && enrollTime.isBefore(now);
+ }
+
+ private static ImmutableMap> getIdnToTldMap(DateTime now) {
+ ImmutableMultimap.Builder idnToTldMap = new ImmutableMultimap.Builder();
+ Tlds.getTldEntitiesOfType(TldType.REAL).stream()
+ .filter(tld -> isEnrolledWithBsa(tld, now))
+ .forEach(
+ tld -> {
+ for (IdnTableEnum idn : IDN_LABEL_VALIDATOR.getIdnTablesForTld(tld)) {
+ idnToTldMap.put(idn, tld);
+ }
+ });
+ return ImmutableMap.copyOf(transformValues(idnToTldMap.build().asMap(), ImmutableSet::copyOf));
+ }
+}
diff --git a/core/src/main/java/google/registry/tldconfig/idn/IdnLabelValidator.java b/core/src/main/java/google/registry/tldconfig/idn/IdnLabelValidator.java
index c0307cfda64..1d78a550c2c 100644
--- a/core/src/main/java/google/registry/tldconfig/idn/IdnLabelValidator.java
+++ b/core/src/main/java/google/registry/tldconfig/idn/IdnLabelValidator.java
@@ -38,9 +38,7 @@ public final class IdnLabelValidator {
public Optional findValidIdnTableForTld(String label, String tldStr) {
String unicodeString = Idn.toUnicode(label);
Tld tld = Tld.get(tldStr); // uses the cache
- ImmutableSet idnTablesForTld = tld.getIdnTables();
- ImmutableSet idnTables =
- idnTablesForTld.isEmpty() ? DEFAULT_IDN_TABLES : idnTablesForTld;
+ ImmutableSet idnTables = getIdnTablesForTld(tld);
for (IdnTableEnum idnTable : idnTables) {
if (idnTable.getTable().isValidLabel(unicodeString)) {
return Optional.of(idnTable.getTable().getName());
@@ -48,4 +46,10 @@ public Optional findValidIdnTableForTld(String label, String tldStr) {
}
return Optional.empty();
}
+
+ /** Returns the names of the IDN tables supported by a {@code tld}. */
+ public ImmutableSet getIdnTablesForTld(Tld tld) {
+ ImmutableSet idnTablesForTld = tld.getIdnTables();
+ return idnTablesForTld.isEmpty() ? DEFAULT_IDN_TABLES : idnTablesForTld;
+ }
}
diff --git a/core/src/main/java/google/registry/tldconfig/idn/IdnTable.java b/core/src/main/java/google/registry/tldconfig/idn/IdnTable.java
index b8587caccfd..1771576a15a 100644
--- a/core/src/main/java/google/registry/tldconfig/idn/IdnTable.java
+++ b/core/src/main/java/google/registry/tldconfig/idn/IdnTable.java
@@ -84,7 +84,7 @@ public URI getPolicy() {
* Returns true if the given label is valid for this IDN table. A label is considered valid if all
* of its codepoints are in the IDN table.
*/
- boolean isValidLabel(String label) {
+ public boolean isValidLabel(String label) {
final int length = label.length();
for (int i = 0; i < length; ) {
int codepoint = label.codePointAt(i);
diff --git a/core/src/test/java/google/registry/bsa/IdnCheckerTest.java b/core/src/test/java/google/registry/bsa/IdnCheckerTest.java
new file mode 100644
index 00000000000..04b204fdc39
--- /dev/null
+++ b/core/src/test/java/google/registry/bsa/IdnCheckerTest.java
@@ -0,0 +1,94 @@
+// Copyright 2023 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.bsa;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.tldconfig.idn.IdnTableEnum.EXTENDED_LATIN;
+import static google.registry.tldconfig.idn.IdnTableEnum.JA;
+import static google.registry.tldconfig.idn.IdnTableEnum.UNCONFUSABLE_LATIN;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import google.registry.model.tld.Tld;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class IdnCheckerTest {
+
+ @Mock Tld jaonly;
+ @Mock Tld jandelatin;
+ @Mock Tld strictlatin;
+ IdnChecker idnChecker;
+
+ @BeforeEach
+ void setup() {
+ idnChecker =
+ new IdnChecker(
+ ImmutableMap.of(
+ JA,
+ ImmutableSet.of(jandelatin, jaonly),
+ EXTENDED_LATIN,
+ ImmutableSet.of(jandelatin),
+ UNCONFUSABLE_LATIN,
+ ImmutableSet.of(strictlatin)));
+ }
+
+ @Test
+ void getAllValidIdns_allTlds() {
+ assertThat(idnChecker.getAllValidIdns("all"))
+ .containsExactly(EXTENDED_LATIN, JA, UNCONFUSABLE_LATIN);
+ }
+
+ @Test
+ void getAllValidIdns_notJa() {
+ assertThat(idnChecker.getAllValidIdns("à")).containsExactly(EXTENDED_LATIN, UNCONFUSABLE_LATIN);
+ }
+
+ @Test
+ void getAllValidIdns_extendedLatinOnly() {
+ assertThat(idnChecker.getAllValidIdns("á")).containsExactly(EXTENDED_LATIN);
+ }
+
+ @Test
+ void getAllValidIdns_jaOnly() {
+ assertThat(idnChecker.getAllValidIdns("っ")).containsExactly(JA);
+ }
+
+ @Test
+ void getAllValidIdns_none() {
+ assertThat(idnChecker.getAllValidIdns("д")).isEmpty();
+ }
+
+ @Test
+ void getSupportingTlds_singleTld_success() {
+ assertThat(idnChecker.getSupportingTlds(ImmutableSet.of("EXTENDED_LATIN")))
+ .containsExactly(jandelatin);
+ }
+
+ @Test
+ void getSupportingTlds_multiTld_success() {
+ assertThat(idnChecker.getSupportingTlds(ImmutableSet.of("JA")))
+ .containsExactly(jandelatin, jaonly);
+ }
+
+ @Test
+ void getForbiddingTlds_success() {
+ assertThat(idnChecker.getForbiddingTlds(ImmutableSet.of("JA"))).containsExactly(strictlatin);
+ }
+}
diff --git a/core/src/test/java/google/registry/tldconfig/idn/IdnLabelValidatorTest.java b/core/src/test/java/google/registry/tldconfig/idn/IdnLabelValidatorTest.java
index 37a087bfded..7270dbb2678 100644
--- a/core/src/test/java/google/registry/tldconfig/idn/IdnLabelValidatorTest.java
+++ b/core/src/test/java/google/registry/tldconfig/idn/IdnLabelValidatorTest.java
@@ -14,11 +14,14 @@
package google.registry.tldconfig.idn;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
+import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
import com.google.common.collect.ImmutableSet;
+import google.registry.model.tld.Tld;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import org.junit.jupiter.api.Test;
@@ -118,4 +121,24 @@ void testPerTldConfig() {
// Extended Latin shouldn't include Japanese characters
assertThat(idnLabelValidator.findValidIdnTableForTld("みんな", "tld")).isEmpty();
}
+
+ @Test
+ void testGetIdnTablesForTld_custom() {
+ persistResource(
+ createTld("tld")
+ .asBuilder()
+ .setIdnTables(ImmutableSet.of(IdnTableEnum.EXTENDED_LATIN))
+ .build());
+ Tld tld = tm().transact(() -> tm().loadByKey(Tld.createVKey("tld")));
+ assertThat(idnLabelValidator.getIdnTablesForTld(tld))
+ .containsExactly(IdnTableEnum.EXTENDED_LATIN);
+ }
+
+ @Test
+ void testGetIdnTablesForTld_default() {
+ persistResource(createTld("tld").asBuilder().build());
+ Tld tld = tm().transact(() -> tm().loadByKey(Tld.createVKey("tld")));
+ assertThat(idnLabelValidator.getIdnTablesForTld(tld))
+ .containsExactly(IdnTableEnum.EXTENDED_LATIN, IdnTableEnum.JA);
+ }
}