diff --git a/core/src/main/java/google/registry/reporting/billing/CopyDetailReportsAction.java b/core/src/main/java/google/registry/reporting/billing/CopyDetailReportsAction.java index e5a152ca1..ac9f26309 100644 --- a/core/src/main/java/google/registry/reporting/billing/CopyDetailReportsAction.java +++ b/core/src/main/java/google/registry/reporting/billing/CopyDetailReportsAction.java @@ -21,10 +21,8 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK; import static java.util.stream.Collectors.joining; import com.google.cloud.storage.BlobId; -import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.Iterables; import com.google.common.flogger.FluentLogger; import com.google.common.io.ByteStreams; import com.google.common.net.MediaType; @@ -41,6 +39,8 @@ import jakarta.inject.Inject; import java.io.IOException; import java.io.InputStream; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** Copy all registrar detail reports in a given bucket's subdirectory from GCS to Drive. */ @Action( @@ -54,6 +54,9 @@ public final class CopyDetailReportsAction implements Runnable { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final Pattern FILENAME_PATTERN = + Pattern.compile("^invoice_details_[0-9]{4}-[0-9]{2}_(.+)_.+\\.csv$"); + private final String billingBucket; private final String invoiceDirectoryPrefix; private final DriveConnection driveConnection; @@ -101,8 +104,13 @@ public final class CopyDetailReportsAction implements Runnable { new ImmutableMultimap.Builder<>(); for (String detailReportName : detailReportObjectNames) { // The standard report format is "invoice_details_yyyy-MM_registrarId_tld.csv - // TODO(larryruili): Determine a safer way of enforcing this. - String registrarId = Iterables.get(Splitter.on('_').split(detailReportName), 3); + Matcher matcher = FILENAME_PATTERN.matcher(detailReportName); + if (!matcher.matches()) { + logger.atWarning().log( + "Detail report filename '%s' does not match the expected pattern.", detailReportName); + continue; + } + String registrarId = matcher.group(1); Optional registrar = Registrar.loadByRegistrarId(registrarId); if (registrar.isEmpty()) { logger.atWarning().log( diff --git a/core/src/test/java/google/registry/reporting/billing/CopyDetailReportsActionTest.java b/core/src/test/java/google/registry/reporting/billing/CopyDetailReportsActionTest.java index b0ca522d4..f7c040613 100644 --- a/core/src/test/java/google/registry/reporting/billing/CopyDetailReportsActionTest.java +++ b/core/src/test/java/google/registry/reporting/billing/CopyDetailReportsActionTest.java @@ -248,4 +248,38 @@ class CopyDetailReportsActionTest { action.run(); verifyNoInteractions(driveConnection); } + + @Test + void testSuccess_registrarIdWithUnderscores() throws IOException { + persistResource( + loadRegistrar("TheRegistrar") + .asBuilder() + .setRegistrarId("The_Registrar") + .setDriveFolderId("0B-99999") + .build()); + + gcsUtils.createFromBytes( + BlobId.of("test-bucket", "results/invoice_details_2017-10_The_Registrar_test.csv"), + "hello,world\n1,2".getBytes(UTF_8)); + + action.run(); + verify(driveConnection) + .createOrUpdateFile( + "invoice_details_2017-10_The_Registrar_test.csv", + MediaType.CSV_UTF_8, + "0B-99999", + "hello,world\n1,2".getBytes(UTF_8)); + assertThat(response.getStatus()).isEqualTo(SC_OK); + } + + @Test + void testSuccess_invalidFilenamePattern_skipped() throws IOException { + gcsUtils.createFromBytes( + BlobId.of("test-bucket", "results/invoice_details_2017-10.csv"), + "hello,world\n1,2".getBytes(UTF_8)); + + action.run(); + verifyNoInteractions(driveConnection); + assertThat(response.getStatus()).isEqualTo(SC_OK); + } }