From f56f4b059a0f40ae981493ddd9c77b88c287b800 Mon Sep 17 00:00:00 2001 From: matevip Date: Sat, 16 May 2026 14:51:52 +0800 Subject: [PATCH] fix(workspace): reject impossible calendar dates in memory snapshot import --- .../WorkspaceMemoryArchiveService.java | 20 +++++++++++++- .../WorkspaceMemoryArchiveServiceTest.java | 27 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/document/WorkspaceMemoryArchiveService.java b/mateclaw-server/src/main/java/vip/mate/workspace/document/WorkspaceMemoryArchiveService.java index 8b13772b..10034311 100644 --- a/mateclaw-server/src/main/java/vip/mate/workspace/document/WorkspaceMemoryArchiveService.java +++ b/mateclaw-server/src/main/java/vip/mate/workspace/document/WorkspaceMemoryArchiveService.java @@ -17,6 +17,8 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; @@ -210,7 +212,23 @@ public class WorkspaceMemoryArchiveService { } private static boolean isAllowedFilename(String name) { - return TOP_LEVEL_WHITELIST.contains(name) || DAILY_FILENAME.matcher(name).matches(); + if (TOP_LEVEL_WHITELIST.contains(name)) { + return true; + } + if (!DAILY_FILENAME.matcher(name).matches()) { + return false; + } + // The regex only constrains digit shape, so structurally-valid but + // non-existent dates (memory/2026-13-99.md, memory/2026-02-30.md) + // would slip through. Parse the date to reject calendar dates that + // cannot occur, keeping the daily ledger namespace clean. + String date = name.substring("memory/".length(), name.length() - ".md".length()); + try { + LocalDate.parse(date); + return true; + } catch (DateTimeParseException e) { + return false; + } } /** diff --git a/mateclaw-server/src/test/java/vip/mate/workspace/document/WorkspaceMemoryArchiveServiceTest.java b/mateclaw-server/src/test/java/vip/mate/workspace/document/WorkspaceMemoryArchiveServiceTest.java index 94873a92..fd96472d 100644 --- a/mateclaw-server/src/test/java/vip/mate/workspace/document/WorkspaceMemoryArchiveServiceTest.java +++ b/mateclaw-server/src/test/java/vip/mate/workspace/document/WorkspaceMemoryArchiveServiceTest.java @@ -173,6 +173,33 @@ class WorkspaceMemoryArchiveServiceTest { org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString()); } + @Test + @DisplayName("Daily filename matching the digit shape but not a real date is rejected") + void impossibleCalendarDateRejected() throws Exception { + wireAgent(1L, 10L); + when(workspaceFileService.getFile(1L, "memory/2026-05-12.md")) + .thenReturn(null); + + byte[] zip = makeZip(Map.of( + "memory/2026-05-12.md", "real date → create", + "memory/2026-13-99.md", "month 13 / day 99 → reject", + "memory/2026-02-30.md", "feb 30 does not exist → reject")); + + WorkspaceMemoryArchiveService.ImportPreview preview = + service.previewImport(1L, 10L, zip); + + assertThat(preview.willCreate()).containsExactlyInAnyOrder("memory/2026-05-12.md"); + assertThat(preview.willSkip()) + .extracting(WorkspaceMemoryArchiveService.SkipEntry::filename, + WorkspaceMemoryArchiveService.SkipEntry::reason) + .contains( + org.assertj.core.groups.Tuple.tuple("memory/2026-13-99.md", "not in whitelist"), + org.assertj.core.groups.Tuple.tuple("memory/2026-02-30.md", "not in whitelist")); + + verify(workspaceFileService, never()).saveFile(org.mockito.ArgumentMatchers.anyLong(), + org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString()); + } + @Test @DisplayName("Preview surfaces old vs new hash + size for updated files") void previewExposesDiffMetadata() throws Exception {