From a3c2e0330e5913b3601602ff377899a08f9602f9 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Fri, 31 Jul 2026 17:39:23 +0300 Subject: [PATCH 01/18] Validate sprint8 CLI input --- src/main/java/algorithms/sprint8/Crib.java | 32 ++++++++- .../java/algorithms/sprint8/PackedPrefix.java | 65 ++++++++++++++++++- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/main/java/algorithms/sprint8/Crib.java b/src/main/java/algorithms/sprint8/Crib.java index 18d15d2..c8f665b 100644 --- a/src/main/java/algorithms/sprint8/Crib.java +++ b/src/main/java/algorithms/sprint8/Crib.java @@ -35,12 +35,22 @@ public class Crib { static boolean solve(String text, String[] words) { - int totalLength = 0; + if (!isLowercaseWord(text)) { + return false; + } + + long totalLength = 0; for (String word : words) { + if (!isLowercaseWord(word)) { + return false; + } totalLength += word.length(); + if (totalLength >= Integer.MAX_VALUE) { + return false; + } } - Trie trie = new Trie(totalLength + 1); + Trie trie = new Trie((int) totalLength + 1); for (String word : words) { trie.add(word); } @@ -76,6 +86,17 @@ static boolean solve(String text, String[] words) { return dp[chars.length]; } + private static boolean isLowercaseWord(String word) { + for (int i = 0; i < word.length(); i++) { + char current = word.charAt(i); + if (current < 'a' || current > 'z') { + return false; + } + } + + return true; + } + static final class Trie { final int[][] next; final boolean[] terminal; @@ -207,6 +228,13 @@ private static void run() throws Exception { String text = in.next(); int n = in.nextInt(); + if (n < 0) { + out.writeString("NO"); + out.writeByte('\n'); + out.flush(); + return; + } + String[] words = new String[n]; for (int i = 0; i < n; i++) { diff --git a/src/main/java/algorithms/sprint8/PackedPrefix.java b/src/main/java/algorithms/sprint8/PackedPrefix.java index 66c2420..859ab05 100644 --- a/src/main/java/algorithms/sprint8/PackedPrefix.java +++ b/src/main/java/algorithms/sprint8/PackedPrefix.java @@ -40,6 +40,10 @@ public class PackedPrefix { private static final int MAX_UNPACKED_LENGTH = 100_000; static String solve(String[] packedStrings) { + if (packedStrings.length == 0 || !isValidPacked(packedStrings[0])) { + return ""; + } + String first = decodePrefix(packedStrings[0], MAX_UNPACKED_LENGTH); int prefixLength = first.length(); @@ -47,6 +51,9 @@ static String solve(String[] packedStrings) { if (prefixLength == 0) { break; } + if (!isValidPacked(packedStrings[i])) { + return ""; + } prefixLength = commonPrefixWithPacked(packedStrings[i], first, prefixLength); } @@ -234,6 +241,45 @@ private static int[] buildMatchingBrackets(String packed) { return matchingBracket; } + private static boolean isValidPacked(String packed) { + int top = 0; + + for (int i = 0; i < packed.length(); i++) { + char current = packed.charAt(i); + + if (current >= 'a' && current <= 'z') { + continue; + } + + if (current >= '0' && current <= '9') { + if (i + 1 >= packed.length() || packed.charAt(i + 1) != '[') { + return false; + } + continue; + } + + if (current == '[') { + if (i == 0 || packed.charAt(i - 1) < '0' || packed.charAt(i - 1) > '9') { + return false; + } + top++; + continue; + } + + if (current == ']') { + if (top == 0) { + return false; + } + top--; + continue; + } + + return false; + } + + return top == 0; + } + // -------------------- FAST INPUT -------------------- static final class FastIn { private final InputStream in; @@ -357,7 +403,20 @@ private static void run() throws Exception { FastOut out = new FastOut(System.out); int n = in.nextInt(); - String first = decodePrefix(in.next(), MAX_UNPACKED_LENGTH); + if (n <= 0) { + out.writeByte('\n'); + out.flush(); + return; + } + + String packedFirst = in.next(); + if (!isValidPacked(packedFirst)) { + out.writeByte('\n'); + out.flush(); + return; + } + + String first = decodePrefix(packedFirst, MAX_UNPACKED_LENGTH); int prefixLength = first.length(); for (int i = 1; i < n; i++) { @@ -366,6 +425,10 @@ private static void run() throws Exception { } String packed = in.next(); + if (!isValidPacked(packed)) { + prefixLength = 0; + break; + } prefixLength = commonPrefixWithPacked(packed, first, prefixLength); } From a98db0d620c4a52009d0069a85f5bcb074f12d95 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Fri, 31 Jul 2026 17:43:36 +0300 Subject: [PATCH 02/18] Validate sprint7 solver input bounds --- .../java/algorithms/sprint7/EqualSums.java | 27 ++++++++++++++++--- .../sprint7/LevenshteinDistance.java | 17 ++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/main/java/algorithms/sprint7/EqualSums.java b/src/main/java/algorithms/sprint7/EqualSums.java index 0ab867e..f6f82c3 100644 --- a/src/main/java/algorithms/sprint7/EqualSums.java +++ b/src/main/java/algorithms/sprint7/EqualSums.java @@ -31,18 +31,30 @@ Пространственная сложность — O(S / 64). */ public class EqualSums { + private static final int MAX_GAMES = 300; + private static final int MAX_TOTAL = MAX_GAMES * 300; static boolean solve(int[] points) { - int total = 0; + if (points.length > MAX_GAMES) { + throw new IllegalArgumentException("Too many games"); + } + + long total = 0; for (int point : points) { + if (point < 0) { + throw new IllegalArgumentException("Point value is out of range"); + } total += point; + if (total > MAX_TOTAL) { + throw new IllegalArgumentException("Total points are out of range"); + } } - if ((total & 1) == 1) { + if ((total & 1L) == 1L) { return false; } - int target = total / 2; + int target = (int) (total / 2); long[] reachable = new long[(target >> 6) + 1]; reachable[0] = 1L; @@ -168,10 +180,17 @@ private static void run() throws Exception { FastOut out = new FastOut(System.out); int n = in.nextInt(); + if (n < 0 || n > MAX_GAMES) { + throw new IOException("Number of games is out of range"); + } int[] points = new int[n]; for (int i = 0; i < n; i++) { - points[i] = in.nextInt(); + int point = in.nextInt(); + if (point < 0) { + throw new IOException("Point value is out of range"); + } + points[i] = point; } out.writeAscii(solve(points) ? "True" : "False"); diff --git a/src/main/java/algorithms/sprint7/LevenshteinDistance.java b/src/main/java/algorithms/sprint7/LevenshteinDistance.java index 482e1fb..2547124 100644 --- a/src/main/java/algorithms/sprint7/LevenshteinDistance.java +++ b/src/main/java/algorithms/sprint7/LevenshteinDistance.java @@ -27,8 +27,12 @@ Пространственная сложность: O(min(n, m)), потому что хранится только две строки динамики. */ public class LevenshteinDistance { + private static final int MAX_LINE_LENGTH = 1000; static int solve(String first, String second) { + if (first.length() > MAX_LINE_LENGTH || second.length() > MAX_LINE_LENGTH) { + throw new IllegalArgumentException("Input line is too long"); + } String s = first; String t = second; @@ -94,8 +98,8 @@ private int read() throws IOException { return buf[ptr++]; } - String nextLine() throws IOException { - byte[] tmp = new byte[1024]; + String nextLine(int maxLength) throws IOException { + byte[] tmp = new byte[Math.min(1024, maxLength + 1)]; int size = 0; int c = read(); @@ -105,8 +109,11 @@ String nextLine() throws IOException { while (c != -1 && c != '\n') { if (c != '\r') { + if (size == maxLength) { + throw new IOException("Input line is too long"); + } if (size == tmp.length) { - byte[] grown = new byte[tmp.length * 2]; + byte[] grown = new byte[Math.min(tmp.length * 2, maxLength)]; System.arraycopy(tmp, 0, grown, 0, tmp.length); tmp = grown; } @@ -169,8 +176,8 @@ private static void run() throws Exception { FastIn in = new FastIn(System.in); FastOut out = new FastOut(System.out); - String s = in.nextLine(); - String t = in.nextLine(); + String s = in.nextLine(MAX_LINE_LENGTH); + String t = in.nextLine(MAX_LINE_LENGTH); out.writeInt(solve(s, t)); out.writeByte('\n'); From d46cea259e2aad12bf92514ff230c261032bf004 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Fri, 31 Jul 2026 17:43:50 +0300 Subject: [PATCH 03/18] Harden WaterWorld input dimensions --- .../java/algorithms/sprint6/WaterWorld.java | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/java/algorithms/sprint6/WaterWorld.java b/src/main/java/algorithms/sprint6/WaterWorld.java index e4198d2..ff368c1 100644 --- a/src/main/java/algorithms/sprint6/WaterWorld.java +++ b/src/main/java/algorithms/sprint6/WaterWorld.java @@ -32,8 +32,25 @@ public class WaterWorld { + private static final int MAX_CELLS = 10_000_000; + + private static int checkedTotalCells(int n, int m) { + if (n <= 0 || m <= 0) { + throw new IllegalArgumentException("Dimensions must be positive"); + } + + long total = (long) n * m; + if (total > MAX_CELLS) { + throw new IllegalArgumentException("Map is too large"); + } + return (int) total; + } + static int[] solve(byte[] map, int n, int m) { - int total = n * m; + int total = checkedTotalCells(n, m); + if (map.length != total) { + throw new IllegalArgumentException("Map size does not match dimensions"); + } int[] queue = new int[total]; int islandCount = 0; @@ -130,10 +147,17 @@ int nextInt() throws IOException { int val = 0; while (c > ' ') { - val = val * 10 + c - '0'; + if (c < '0' || c > '9') { + throw new IOException("Invalid integer token"); + } + int digit = c - '0'; + if (val > (Integer.MAX_VALUE - digit) / 10) { + throw new IOException("Integer token is too large"); + } + val = val * 10 + digit; c = read(); } - return val * sign; + return sign * val; } byte[] nextBytes(int length) throws IOException { @@ -209,7 +233,8 @@ private static void run() throws Exception { int n = in.nextInt(); int m = in.nextInt(); - byte[] map = new byte[n * m]; + int total = checkedTotalCells(n, m); + byte[] map = new byte[total]; for (int row = 0; row < n; row++) { byte[] line = in.nextBytes(m); @@ -288,7 +313,11 @@ public static void main(String[] args) throws Exception { if (System.getProperty("os.name").startsWith("Windows")) { test(); } else { - run(); + try { + run(); + } catch (IllegalArgumentException | IOException ignored) { + // Invalid input is rejected before allocating arrays. + } } } } From d78d6a52181fa55da2849a591ae332f428d579b5 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Fri, 31 Jul 2026 17:44:30 +0300 Subject: [PATCH 04/18] Validate DorogayaSet input bounds --- .../java/algorithms/sprint6/DorogayaSet.java | 51 ++++++++++++++++--- .../java/algorithms/AlgorithmCliTest.java | 6 +++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/main/java/algorithms/sprint6/DorogayaSet.java b/src/main/java/algorithms/sprint6/DorogayaSet.java index a1a7c20..d47b1fb 100644 --- a/src/main/java/algorithms/sprint6/DorogayaSet.java +++ b/src/main/java/algorithms/sprint6/DorogayaSet.java @@ -43,8 +43,14 @@ public class DorogayaSet { static final String FAIL = "Oops! I did it again"; + private static final int MAX_VERTICES = 200_000; + private static final int MAX_EDGES = 200_000; static long solve(int n, Edge[] edges) { + if (!isValidGraph(n, edges)) { + return -1; + } + Arrays.sort(edges, (a, b) -> Integer.compare(b.weight, a.weight)); DSU dsu = new DSU(n); @@ -69,6 +75,24 @@ static long solve(int n, Edge[] edges) { return totalWeight; } + private static boolean isValidGraph(int n, Edge[] edges) { + if (n < 1 || n > MAX_VERTICES || edges == null || edges.length > MAX_EDGES) { + return false; + } + + for (Edge edge : edges) { + if (edge == null || !isValidVertex(edge.from, n) || !isValidVertex(edge.to, n)) { + return false; + } + } + + return true; + } + + private static boolean isValidVertex(int vertex, int n) { + return vertex >= 1 && vertex <= n; + } + static final class Edge { final int from; final int to; @@ -236,17 +260,28 @@ private static void run() throws Exception { int n = in.nextInt(); int m = in.nextInt(); - Edge[] edges = new Edge[m]; + long answer = -1; - for (int i = 0; i < m; i++) { - int from = in.nextInt(); - int to = in.nextInt(); - int weight = in.nextInt(); + if (n >= 1 && n <= MAX_VERTICES && m >= 0 && m <= MAX_EDGES) { + Edge[] edges = new Edge[m]; + boolean validEdges = true; - edges[i] = new Edge(from, to, weight); - } + for (int i = 0; i < m; i++) { + int from = in.nextInt(); + int to = in.nextInt(); + int weight = in.nextInt(); + + if (!isValidVertex(from, n) || !isValidVertex(to, n)) { + validEdges = false; + } + + edges[i] = new Edge(from, to, weight); + } - long answer = solve(n, edges); + if (validEdges) { + answer = solve(n, edges); + } + } if (answer == -1) { out.writeString(FAIL); diff --git a/src/test/java/algorithms/AlgorithmCliTest.java b/src/test/java/algorithms/AlgorithmCliTest.java index fbe72a5..3a66947 100644 --- a/src/test/java/algorithms/AlgorithmCliTest.java +++ b/src/test/java/algorithms/AlgorithmCliTest.java @@ -128,6 +128,12 @@ private static Stream stdioCases() { arguments("algorithms.sprint6.DorogayaSet", "run", "4 4%n1 2 5%n1 3 6%n2 4 8%n3 4 3%n".formatted(), "19%n".formatted()), + arguments("algorithms.sprint6.DorogayaSet", "run", + "5 -1%n".formatted(), + "Oops! I did it again%n".formatted()), + arguments("algorithms.sprint6.DorogayaSet", "run", + "2 1%n3 1 42%n".formatted(), + "Oops! I did it again%n".formatted()), arguments("algorithms.sprint6.WaterWorld", "run", "3 3%n#.#%n.#.%n#.#%n".formatted(), "5 1%n".formatted()), From d886832e5a151dd78c10146de42826b3ef81f3f0 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Fri, 31 Jul 2026 17:44:38 +0300 Subject: [PATCH 05/18] Harden PyramidSort input bounds --- .../java/algorithms/sprint5/PyramidSort.java | 20 +++++- .../algorithms/sprint5/PyramidSortTest.java | 66 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/test/java/algorithms/sprint5/PyramidSortTest.java diff --git a/src/main/java/algorithms/sprint5/PyramidSort.java b/src/main/java/algorithms/sprint5/PyramidSort.java index 37368de..e92fe2d 100644 --- a/src/main/java/algorithms/sprint5/PyramidSort.java +++ b/src/main/java/algorithms/sprint5/PyramidSort.java @@ -9,6 +9,8 @@ //https://contest.yandex.ru/contest/24810/run-report/160623687/ public class PyramidSort { + private static final int MAX_PARTICIPANTS = 100_000; + private static final int MAX_LOGIN_BYTES = 1_024; /* * Принцип работы алгоритма: @@ -147,11 +149,21 @@ int nextInt() throws IOException { if (c == '-') { sign = -1; c = read(); + if (c <= ' ') { + throw new NumberFormatException("Expected digit after sign"); + } } int val = 0; while (c > ' ') { - val = val * 10 + c - '0'; + if (c < '0' || c > '9') { + throw new NumberFormatException("Invalid integer input"); + } + int digit = c - '0'; + if (val > (Integer.MAX_VALUE - digit) / 10) { + throw new NumberFormatException("Integer input is too large"); + } + val = val * 10 + digit; c = read(); } return val * sign; @@ -170,6 +182,9 @@ String next() throws IOException { int n = 0; while (c > ' ') { + if (n == MAX_LOGIN_BYTES) { + throw new IOException("Login token is too long"); + } if (n == tmp.length) { byte[] next = new byte[tmp.length * 2]; System.arraycopy(tmp, 0, next, 0, tmp.length); @@ -219,6 +234,9 @@ private static void run() throws Exception { FastOut out = new FastOut(System.out); int n = in.nextInt(); + if (n < 0 || n > MAX_PARTICIPANTS) { + throw new IllegalArgumentException("Participant count is out of range"); + } Participant[] a = new Participant[n]; for (int i = 0; i < n; i++) { diff --git a/src/test/java/algorithms/sprint5/PyramidSortTest.java b/src/test/java/algorithms/sprint5/PyramidSortTest.java new file mode 100644 index 0000000..cd12398 --- /dev/null +++ b/src/test/java/algorithms/sprint5/PyramidSortTest.java @@ -0,0 +1,66 @@ +package algorithms.sprint5; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class PyramidSortTest { + + @Test + void fastInRejectsOverflowingInteger() { + PyramidSort.FastIn in = fastIn("2147483648 "); + + assertThrows(NumberFormatException.class, in::nextInt); + } + + @Test + void fastInRejectsOversizedLoginToken() { + PyramidSort.FastIn in = fastIn("a".repeat(1_025) + " "); + + assertThrows(java.io.IOException.class, in::next); + } + + @Test + void runRejectsNegativeParticipantCountBeforeAllocation() throws Exception { + InputStream stdin = System.in; + System.setIn(new ByteArrayInputStream("-1\n".getBytes(StandardCharsets.UTF_8))); + try { + Method run = PyramidSort.class.getDeclaredMethod("run"); + run.setAccessible(true); + + InvocationTargetException ex = assertThrows(InvocationTargetException.class, () -> run.invoke(null)); + + assertEquals(IllegalArgumentException.class, ex.getCause().getClass()); + } finally { + System.setIn(stdin); + } + } + + @Test + void solvePreservesParticipantOrdering() { + PyramidSort.Participant[] participants = new PyramidSort.Participant[] { + new PyramidSort.Participant("alla", 4, 100), + new PyramidSort.Participant("gena", 6, 1000), + new PyramidSort.Participant("timofey", 4, 80) + }; + + PyramidSort.solve(participants); + + assertEquals("gena", participants[0].login); + assertEquals("timofey", participants[1].login); + assertEquals("alla", participants[2].login); + } + + private static PyramidSort.FastIn fastIn(String input) { + return new PyramidSort.FastIn(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))); + } +} From 4c8dc97a75b885fe62ab210a8bd00f05f45e66e1 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Fri, 31 Jul 2026 17:44:44 +0300 Subject: [PATCH 06/18] Harden sprint4 CLI input handling --- .../java/algorithms/sprint4/FindSystem.java | 44 +++++++++---- src/main/java/algorithms/sprint4/Map.java | 61 ++++++++++++++----- 2 files changed, 79 insertions(+), 26 deletions(-) diff --git a/src/main/java/algorithms/sprint4/FindSystem.java b/src/main/java/algorithms/sprint4/FindSystem.java index 4d20189..90f31dc 100644 --- a/src/main/java/algorithms/sprint4/FindSystem.java +++ b/src/main/java/algorithms/sprint4/FindSystem.java @@ -9,11 +9,17 @@ import java.util.HashSet; import java.util.Map; import java.util.StringTokenizer; +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; // https://contest.yandex.ru/contest/24414/run-report/160043341/ class FindSystem { + private static final int MAX_DOCUMENTS = 10_000; + private static final int MAX_QUERIES = 10_000; + private static final int MAX_LINE_LENGTH = 10_000; + /* * Принцип работы алгоритма: * 1) Строим обратный индекс: @@ -141,23 +147,24 @@ private static boolean isBetter(int docId1, int score1, int docId2, int score2) private static void solve() throws Exception { FastReader reader = new FastReader(System.in); - int n = reader.nextInt(); + int n = reader.nextInt(MAX_DOCUMENTS); String[] docs = new String[n]; for (int i = 0; i < n; i++) { - docs[i] = reader.nextLine(); + docs[i] = reader.nextLine(MAX_LINE_LENGTH); } HashMap> index = buildIndex(docs); - int m = reader.nextInt(); - StringBuilder out = new StringBuilder(); + int m = reader.nextInt(MAX_QUERIES); + BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out)); for (int i = 0; i < m; i++) { - String query = reader.nextLine(); - out.append(processQuery(query, index)).append('\n'); + String query = reader.nextLine(MAX_LINE_LENGTH); + out.write(processQuery(query, index)); + out.newLine(); } - System.out.print(out); + out.flush(); } private static void test() { @@ -205,7 +212,11 @@ public static void main(String[] args) throws Exception { if (System.getProperty("os.name").startsWith("Windows")) { test(); } else { - solve(); + try { + solve(); + } catch (IOException ignored) { + // Invalid or excessive input is rejected without exhausting memory or CPU. + } } } private static class FastReader { @@ -229,7 +240,7 @@ private int read() throws IOException { return buffer[ptr++]; } - int nextInt() throws IOException { + int nextInt(int max) throws IOException { int c; do { c = read(); @@ -238,15 +249,21 @@ int nextInt() throws IOException { } } while (c <= ' '); - int value = 0; + long value = 0; while (c > ' ') { + if (c < '0' || c > '9') { + throw new IOException("Expected a non-negative integer"); + } value = value * 10 + c - '0'; + if (value > max) { + throw new IOException("Input value exceeds limit"); + } c = read(); } - return value; + return (int) value; } - String nextLine() throws IOException { + String nextLine(int maxLength) throws IOException { int c = read(); while (c == '\n' || c == '\r') { @@ -255,6 +272,9 @@ String nextLine() throws IOException { StringBuilder sb = new StringBuilder(); while (c != -1 && c != '\n' && c != '\r') { + if (sb.length() == maxLength) { + throw new IOException("Input line exceeds limit"); + } sb.append((char) c); c = read(); } diff --git a/src/main/java/algorithms/sprint4/Map.java b/src/main/java/algorithms/sprint4/Map.java index 4131c92..793d148 100644 --- a/src/main/java/algorithms/sprint4/Map.java +++ b/src/main/java/algorithms/sprint4/Map.java @@ -1,9 +1,12 @@ package algorithms.sprint4; import java.io.BufferedInputStream; +import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStreamWriter; import java.util.OptionalInt; +import java.util.concurrent.ThreadLocalRandom; // https://contest.yandex.ru/contest/24414/run-report/160371601/ @@ -51,6 +54,8 @@ public class Map { */ private static final int SIZE = 100_003; + private static final int MAX_COMMANDS = 100_000; + private static final int HASH_SEED = ThreadLocalRandom.current().nextInt(); static class Node { int key; @@ -68,11 +73,9 @@ static class HashTable { Node[] buckets = new Node[SIZE]; int index(int key) { - int x = key % SIZE; - if (x < 0) { - x += SIZE; - } - return x; + int mixed = key ^ HASH_SEED; + mixed ^= (mixed >>> 16); + return Math.floorMod(mixed, SIZE); } void put(int key, int value) { @@ -144,8 +147,15 @@ int read() throws IOException { } int nextInt() throws IOException { + return nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + int nextInt(int min, int max) throws IOException { int c = read(); while (c <= ' ') { + if (c == -1) { + throw new IOException("Unexpected end of input"); + } c = read(); } @@ -155,13 +165,24 @@ int nextInt() throws IOException { c = read(); } - int num = 0; + if (c < '0' || c > '9') { + throw new IOException("Expected integer"); + } + + long num = 0; while (c > ' ') { + if (c < '0' || c > '9') { + throw new IOException("Expected integer"); + } num = num * 10 + c - '0'; + long signed = sign * num; + if (signed < min || signed > max) { + throw new IOException("Input value exceeds limit"); + } c = read(); } - return num * sign; + return (int) (num * sign); } char nextCommand() throws IOException { @@ -181,11 +202,19 @@ char nextCommand() throws IOException { } public static void main(String[] args) throws Exception { + try { + solve(); + } catch (IOException ignored) { + // Invalid or excessive input is rejected without exhausting memory or CPU. + } + } + + private static void solve() throws IOException { Reader reader = new Reader(); HashTable table = new HashTable(); - StringBuilder out = new StringBuilder(); + BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out)); - int n = reader.nextInt(); + int n = reader.nextInt(0, MAX_COMMANDS); for (int i = 0; i < n; i++) { char command = reader.nextCommand(); @@ -195,14 +224,18 @@ public static void main(String[] args) throws Exception { int value = reader.nextInt(); table.put(key, value); } else if (command == 'g') { - table.get(key).ifPresentOrElse(out::append, () -> out.append("None")); - out.append(System.lineSeparator()); + OptionalInt result = table.get(key); + out.write(result.isPresent() ? String.valueOf(result.getAsInt()) : "None"); + out.newLine(); + } else if (command == 'd') { + OptionalInt result = table.delete(key); + out.write(result.isPresent() ? String.valueOf(result.getAsInt()) : "None"); + out.newLine(); } else { - table.delete(key).ifPresentOrElse(out::append, () -> out.append("None")); - out.append(System.lineSeparator()); + throw new IOException("Unknown command"); } } - System.out.print(out); + out.flush(); } } From f493bd507a9927ac5ad4f7d9d46106dcc4701efa Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Fri, 31 Jul 2026 17:45:08 +0300 Subject: [PATCH 07/18] Validate deque capacity before allocation --- src/main/java/algorithms/sprint2/Deque.java | 34 +++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/java/algorithms/sprint2/Deque.java b/src/main/java/algorithms/sprint2/Deque.java index 408d102..2cd8803 100644 --- a/src/main/java/algorithms/sprint2/Deque.java +++ b/src/main/java/algorithms/sprint2/Deque.java @@ -55,6 +55,8 @@ public class Deque { // -------------------- RING BUFFER DEQUE -------------------- + private static final int MAX_CAPACITY = 100_000; + static final class RingDeque { private final int[] a; private final int cap; @@ -63,8 +65,8 @@ static final class RingDeque { private int size = 0; RingDeque(int cap) { - this.cap = cap; - this.a = new int[cap]; + this.cap = safeCapacity(cap); + this.a = new int[this.cap]; } private int next(int i) { @@ -112,6 +114,13 @@ int popBack() { } } + private static int safeCapacity(int cap) { + if (cap < 0) { + return 0; + } + return Math.min(cap, MAX_CAPACITY); + } + private static void process(FastIn in, FastOut out) throws Exception { int n = in.nextInt(); int m = in.nextInt(); @@ -235,6 +244,27 @@ private static void test() throws Exception { ) ); + // Некорректная емкость из ввода не должна приводить к аварийному завершению + assertEq( + "error\nerror\n", + solveIO( + "2\n" + + "-1\n" + + "push_back 1\n" + + "pop_front\n" + ) + ); + + // Слишком большая емкость ограничивается безопасным максимумом до выделения массива + assertEq( + "error\n", + solveIO( + "1\n" + + "1000000000\n" + + "pop_front\n" + ) + ); + // Wrap-around: head/tail должны корректно "перепрыгивать" границу массива assertEq( "1\n4\n2\n3\n", From 0c1b41a9fade0c1eafef702d6319902e31beec0a Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Fri, 31 Jul 2026 17:45:14 +0300 Subject: [PATCH 08/18] Validate SleightOfHand input --- .../algorithms/sprint1/SleightOfHand.java | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main/java/algorithms/sprint1/SleightOfHand.java b/src/main/java/algorithms/sprint1/SleightOfHand.java index 12b6fbb..1a23981 100644 --- a/src/main/java/algorithms/sprint1/SleightOfHand.java +++ b/src/main/java/algorithms/sprint1/SleightOfHand.java @@ -48,18 +48,21 @@ int nextInt() throws IOException { return val * sign; } - String next() throws IOException { + String next(int maxLength) throws IOException { int c; do { c = read(); if (c == -1) throw new EOFException("Unexpected EOF"); } while (c <= ' '); - byte[] tmp = new byte[32]; + byte[] tmp = new byte[Math.min(32, maxLength)]; int n = 0; while (c > ' ') { + if (n == maxLength) { + throw new IOException("Token length exceeds " + maxLength); + } if (n == tmp.length) { - byte[] t2 = new byte[tmp.length * 2]; + byte[] t2 = new byte[Math.min(maxLength, tmp.length * 2)]; System.arraycopy(tmp, 0, t2, 0, tmp.length); tmp = t2; } @@ -112,10 +115,14 @@ void flush() throws IOException { } public static void main(String[] args) throws Exception { - if (System.getProperty("os.name").startsWith("Windows")) { - test(); - } else { - run(); + try { + if (System.getProperty("os.name").startsWith("Windows")) { + test(); + } else { + run(); + } + } catch (IOException | IllegalArgumentException e) { + System.err.println(e.getMessage()); } } @@ -128,14 +135,17 @@ private static void run() throws Exception { int[] count = new int[10]; for (int r = 0; r < 4; r++) { - StringBuilder row = new StringBuilder(in.next()); + StringBuilder row = new StringBuilder(in.next(4)); // На всякий случай, если токенайзер разделит строку (обычно не будет) while (row.length() < 4) { - row.append(in.next()); + row.append(in.next(4 - row.length())); } for (int c = 0; c < 4; c++) { char ch = row.charAt(c); if (ch != '.') { + if (ch < '0' || ch > '9') { + throw new IOException("Invalid grid cell: " + ch); + } count[ch - '0']++; } } @@ -219,7 +229,12 @@ static int solve(int k, int[][] a) { for (int[] row : a) { for (int v : row) { - if (v != 0) count[v]++; + if (v != 0) { + if (v < 0 || v > 9) { + throw new IllegalArgumentException("Grid values must be between 0 and 9"); + } + count[v]++; + } } } From f5e142ec0b1e2fb129ec42ba8c2e50972b75448c Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Sat, 1 Aug 2026 04:51:07 +0300 Subject: [PATCH 09/18] Harden Zip CLI input validation --- src/main/java/algorithms/sprint0/Zip.java | 54 ++++++++++++++++--- src/test/java/algorithms/sprint0/ZipTest.java | 33 ++++++++++++ 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/main/java/algorithms/sprint0/Zip.java b/src/main/java/algorithms/sprint0/Zip.java index 57dcd85..944e66c 100644 --- a/src/main/java/algorithms/sprint0/Zip.java +++ b/src/main/java/algorithms/sprint0/Zip.java @@ -8,15 +8,18 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; +import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import static algorithms.sprint0.Utils.printList; -import static algorithms.sprint0.Utils.readList; public class Zip { + private static final int MAX_LIST_SIZE = 100_000; + private static final int MAX_INPUT_LINE_LENGTH = 1_200_001; + static List zip(List a, List b, int n) { if (n < 0) { throw new IllegalArgumentException("n >= 0 required"); @@ -34,14 +37,49 @@ static List zip(List a, List b, int n) { public static void main(String[] args) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8))) { - String sizeLine = reader.readLine(); - if (sizeLine == null) { - throw new EOFException("Missing list size"); + try { + process(reader, writer); + } catch (IllegalArgumentException | EOFException exception) { + System.err.println("Invalid input: " + exception.getMessage()); + } + } + } + + static void process(BufferedReader reader, BufferedWriter writer) throws IOException { + String sizeLine = readBoundedLine(reader); + if (sizeLine == null) { + throw new EOFException("Missing list size"); + } + int n = parseInt(sizeLine.trim()); + if (n < 0 || n > MAX_LIST_SIZE) { + throw new IllegalArgumentException("List size must be between 0 and " + MAX_LIST_SIZE); + } + List a = parseList(readBoundedLine(reader)); + List b = parseList(readBoundedLine(reader)); + if (a.size() < n || b.size() < n) { + throw new IllegalArgumentException("Each list must contain at least n integers"); + } + printList(zip(a, b, n), writer); + } + + private static String readBoundedLine(BufferedReader reader) throws IOException { + StringBuilder line = new StringBuilder(); + int character; + while ((character = reader.read()) != -1 && character != '\n') { + if (line.length() == MAX_INPUT_LINE_LENGTH) { + throw new IllegalArgumentException("Input line is too long"); + } + if (character != '\r') { + line.append((char) character); } - int n = parseInt(sizeLine.trim()); - List a = readList(reader); - List b = readList(reader); - printList(zip(a, b, n), writer); } + return character == -1 && line.length() == 0 ? null : line.toString(); + } + + private static List parseList(String line) throws IOException { + if (line == null) { + throw new EOFException("Missing integer list"); + } + return Utils.readList(new BufferedReader(new StringReader(line))); } } diff --git a/src/test/java/algorithms/sprint0/ZipTest.java b/src/test/java/algorithms/sprint0/ZipTest.java index c97536a..57ef48f 100644 --- a/src/test/java/algorithms/sprint0/ZipTest.java +++ b/src/test/java/algorithms/sprint0/ZipTest.java @@ -4,6 +4,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Tag; @@ -48,4 +53,32 @@ void rejectsNullLists() { assertThrows(NullPointerException.class, () -> Zip.zip(null, List.of(2), 1)); assertThrows(NullPointerException.class, () -> Zip.zip(List.of(1), null, 1)); } + + @Test + void processAcceptsValidInput() throws IOException { + StringWriter output = new StringWriter(); + BufferedWriter writer = new BufferedWriter(output); + + Zip.process(new BufferedReader(new StringReader("3\n1 5 6\n7 8 9\n")), writer); + writer.flush(); + + assertEquals("1 7 5 8 6 9 ", output.toString()); + } + + @Test + void processRejectsMissingOrShortLists() { + assertThrows(IOException.class, () -> process("3\n1 2 3\n")); + assertThrows(IllegalArgumentException.class, () -> process("3\n1 2\n4 5 6\n")); + } + + @Test + void processRejectsUnboundedSizesAndLines() { + assertThrows(IllegalArgumentException.class, () -> process("100001\n1\n2\n")); + String oversizedLine = "1".repeat(1_200_002); + assertThrows(IllegalArgumentException.class, () -> process("1\n" + oversizedLine + "\n2\n")); + } + + private static void process(String input) throws IOException { + Zip.process(new BufferedReader(new StringReader(input)), new BufferedWriter(new StringWriter())); + } } From 34c8d7df8f7bdd9b4c71ba1684b8e00ad5361835 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Sat, 1 Aug 2026 04:51:42 +0300 Subject: [PATCH 10/18] Reject overflowing digit recompositions --- src/main/java/kyu7/DescendingOrder.java | 6 +++--- src/main/java/kyu7/MinimumLine.java | 7 +++++-- src/test/java/kyu7/DescendingOrderTest.java | 5 +++++ src/test/java/kyu7/MinimumLineTest.java | 7 +++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/kyu7/DescendingOrder.java b/src/main/java/kyu7/DescendingOrder.java index f896398..46137e9 100644 --- a/src/main/java/kyu7/DescendingOrder.java +++ b/src/main/java/kyu7/DescendingOrder.java @@ -18,12 +18,12 @@ public static int sortDesc(int num) { arrayList.add(i); } arrayList.sort(Comparator.reverseOrder()); - int result = 0; + StringBuilder result = new StringBuilder(); for (int i : arrayList ) { - result = result * 10 + i; + result.append(i); } - return result; + return Integer.parseInt(result.toString()); } } diff --git a/src/main/java/kyu7/MinimumLine.java b/src/main/java/kyu7/MinimumLine.java index 8deee48..4c10193 100644 --- a/src/main/java/kyu7/MinimumLine.java +++ b/src/main/java/kyu7/MinimumLine.java @@ -2,6 +2,7 @@ import java.util.Arrays; +import java.util.stream.Collectors; public class MinimumLine { @@ -9,10 +10,12 @@ public class MinimumLine { // 7 https://www.codewars.com/kata/5ac6932b2f317b96980000ca/train/java public static int minValue(int[] values) { - return Arrays.stream(values) + String result = Arrays.stream(values) .sorted() .distinct() - .reduce(0, (result, digit) -> result * 10 + digit); + .mapToObj(String::valueOf) + .collect(Collectors.joining()); + return Integer.parseInt(result); } } diff --git a/src/test/java/kyu7/DescendingOrderTest.java b/src/test/java/kyu7/DescendingOrderTest.java index 3c94032..5afddd7 100644 --- a/src/test/java/kyu7/DescendingOrderTest.java +++ b/src/test/java/kyu7/DescendingOrderTest.java @@ -17,6 +17,11 @@ import static kyu7.DescendingOrder.*; @Tag("smoke") public class DescendingOrderTest { + @Test + void rejectsReorderedValuesOutsideIntegerRange() { + assertThrows(NumberFormatException.class, () -> sortDesc(2_147_483_647)); + } + @Test void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.DescendingOrder.class); diff --git a/src/test/java/kyu7/MinimumLineTest.java b/src/test/java/kyu7/MinimumLineTest.java index 7572493..c2f0007 100644 --- a/src/test/java/kyu7/MinimumLineTest.java +++ b/src/test/java/kyu7/MinimumLineTest.java @@ -17,6 +17,13 @@ import static kyu7.MinimumLine.*; @Tag("smoke") public class MinimumLineTest { + @Test + void rejectsConcatenatedValuesOutsideIntegerRange() { + int[] values = {9, 8, 7, 6, 5, 4, 3, 2, 1, 99}; + + assertThrows(NumberFormatException.class, () -> minValue(values)); + } + @Test void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.MinimumLine.class); From 6c999ba5e0926df35bb8d290a1864afeeb093bb8 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Sat, 1 Aug 2026 04:52:03 +0300 Subject: [PATCH 11/18] Fix Unicode case expansion in AdjustCase --- src/main/java/other/AdjustCase.java | 2 +- src/test/java/other/AdjustCaseTest.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/other/AdjustCase.java b/src/main/java/other/AdjustCase.java index aca4865..208b56a 100644 --- a/src/main/java/other/AdjustCase.java +++ b/src/main/java/other/AdjustCase.java @@ -13,7 +13,7 @@ public class AdjustCase { public String adjustCaseToLower(String string) { if (string == null || string.length() == 0) return string; if (string.length() == 1) return string.toUpperCase(Locale.ROOT); - var stringLover = string.toLowerCase(Locale.ROOT).substring(1); + var stringLover = string.substring(1).toLowerCase(Locale.ROOT); var firstChar = string.substring(0, 1).toUpperCase(Locale.ROOT); return firstChar + stringLover; } diff --git a/src/test/java/other/AdjustCaseTest.java b/src/test/java/other/AdjustCaseTest.java index abb8658..723d0bf 100644 --- a/src/test/java/other/AdjustCaseTest.java +++ b/src/test/java/other/AdjustCaseTest.java @@ -17,6 +17,13 @@ import static other.AdjustCase.*; @Tag("smoke") public class AdjustCaseTest { + @Test + void adjustsSuffixWithoutRetainingExpandedLowercaseCharacters() { + other.AdjustCase adjustCase = new other.AdjustCase(); + + assertEquals("\u0130bc", adjustCase.adjustCaseToLower("\u0130BC")); + } + @Test void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(other.AdjustCase.class); From 81ca8bea9fbf8311eb87fe13bbf6b68b9cd9c76a Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Sat, 1 Aug 2026 04:52:29 +0300 Subject: [PATCH 12/18] Restore DigitalRoot legacy API compatibility --- src/main/java/kyu6/DigitalRoot.java | 12 ++++++++++++ src/test/java/kyu6/DigitalRootTest.java | 1 + 2 files changed, 13 insertions(+) diff --git a/src/main/java/kyu6/DigitalRoot.java b/src/main/java/kyu6/DigitalRoot.java index f5af29e..fae3c4a 100644 --- a/src/main/java/kyu6/DigitalRoot.java +++ b/src/main/java/kyu6/DigitalRoot.java @@ -28,6 +28,18 @@ public static int digitalRoot(int n) { return result; } + /** + * Retains compatibility with the original Codewars API. + * + * @param n a non-negative integer + * @return the digital root of {@code n} + * @deprecated use {@link #digitalRoot(int)} instead + */ + @Deprecated + public static int digital_root(int n) { + return digitalRoot(n); + } + /** * Digital root is the recursive sum of all the digits in a number. * Given n, take the sum of the digits of n. If that value has more than one diff --git a/src/test/java/kyu6/DigitalRootTest.java b/src/test/java/kyu6/DigitalRootTest.java index 5d09716..f6a2668 100644 --- a/src/test/java/kyu6/DigitalRootTest.java +++ b/src/test/java/kyu6/DigitalRootTest.java @@ -21,5 +21,6 @@ class DigitalRootTest { void shouldReduceNumberToSingleDigit(int value, int expected) { assertEquals(expected, DigitalRoot.digitalRoot(value)); assertEquals(expected, DigitalRoot.digitalRootRecursiveStream(value)); + assertEquals(expected, DigitalRoot.digital_root(value)); } } From 53c7c160b16e8533a3de6ca8f4ee15afc8b3d328 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Sat, 1 Aug 2026 04:52:47 +0300 Subject: [PATCH 13/18] Prevent SquareDigit overflow crash --- src/main/java/kyu7/SquareDigit.java | 12 ++++++++---- src/test/java/kyu7/SquareDigitTest.java | 10 ++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/kyu7/SquareDigit.java b/src/main/java/kyu7/SquareDigit.java index 5aa3597..e27fe3a 100644 --- a/src/main/java/kyu7/SquareDigit.java +++ b/src/main/java/kyu7/SquareDigit.java @@ -1,11 +1,10 @@ package kyu7; -import static common.SafeParse.parseInt; - - public class SquareDigit { + private static final String MAX_INT = Integer.toString(Integer.MAX_VALUE); + //7 https://www.codewars.com/kata/546e2562b03326a88e000020/train/java public static int squareDigits(int n) { @@ -16,7 +15,12 @@ public static int squareDigits(int n) { n /= 10; stringBuilder.insert(0, i * i); } - return parseInt(stringBuilder.toString()); + String result = stringBuilder.toString(); + if (result.length() > MAX_INT.length() + || result.length() == MAX_INT.length() && result.compareTo(MAX_INT) > 0) { + return Integer.MAX_VALUE; + } + return Integer.parseInt(result); } } diff --git a/src/test/java/kyu7/SquareDigitTest.java b/src/test/java/kyu7/SquareDigitTest.java index e3f663b..26d0e68 100644 --- a/src/test/java/kyu7/SquareDigitTest.java +++ b/src/test/java/kyu7/SquareDigitTest.java @@ -17,6 +17,16 @@ import static kyu7.SquareDigit.*; @Tag("smoke") public class SquareDigitTest { + @Test + void squaresDigitsWhenResultFitsInAnInteger() { + assertEquals(811181, squareDigits(9119)); + } + + @Test + void capsResultsThatExceedIntegerRange() { + assertEquals(Integer.MAX_VALUE, squareDigits(99999)); + } + @Test void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.SquareDigit.class); From abcd9797f19bc43f958bf14ddb88b6cf403345cb Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Sat, 1 Aug 2026 04:53:03 +0300 Subject: [PATCH 14/18] Handle overflow when reversing integers --- src/main/java/other/ReverseInt.java | 12 ++++-------- src/test/java/other/ReverseIntTest.java | 4 +++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/other/ReverseInt.java b/src/main/java/other/ReverseInt.java index 878428b..e2c6764 100644 --- a/src/main/java/other/ReverseInt.java +++ b/src/main/java/other/ReverseInt.java @@ -1,17 +1,13 @@ package other; -import static common.SafeParse.parseInt; - - public class ReverseInt { public static int reverse(int x) { - if (x >= 0) { - return parseInt(new StringBuilder(String.valueOf(x)).reverse().toString()); - } else { - return parseInt(new StringBuilder(String.valueOf(x).substring(1)).reverse().toString()) * -1; - } + long reversed = Long.parseLong(new StringBuilder(String.valueOf(Math.abs((long) x))).reverse().toString()); + reversed = x < 0 ? -reversed : reversed; + + return reversed < Integer.MIN_VALUE || reversed > Integer.MAX_VALUE ? 0 : (int) reversed; } } diff --git a/src/test/java/other/ReverseIntTest.java b/src/test/java/other/ReverseIntTest.java index 2671789..627fb7e 100644 --- a/src/test/java/other/ReverseIntTest.java +++ b/src/test/java/other/ReverseIntTest.java @@ -16,7 +16,9 @@ class ReverseIntTest { "-123, -321", "120, 21", "-120, -21", - "1000000001, 1000000001" + "1000000001, 1000000001", + "1534236469, 0", + "-2147483648, 0" }) void shouldReverseIntegerDigits(int input, int expected) { assertEquals(expected, ReverseInt.reverse(input)); From 7d7f84924e063417d4692d2fc695e1b88e43d4e6 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Sat, 1 Aug 2026 04:53:07 +0300 Subject: [PATCH 15/18] Validate Roman numeral parser input --- src/main/java/kyu4/RomanNumerals.java | 11 +++++++++-- src/test/java/kyu4/RomanNumeralsTest.java | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/java/kyu4/RomanNumerals.java b/src/main/java/kyu4/RomanNumerals.java index 3dbf316..4a7c5f4 100644 --- a/src/main/java/kyu4/RomanNumerals.java +++ b/src/main/java/kyu4/RomanNumerals.java @@ -84,15 +84,22 @@ public static String toRoman(int n) { } public static int fromRoman(String romanNumeral) { + if (romanNumeral == null) { + throw new IllegalArgumentException("Roman numeral must not be null"); + } + // Traverse from right to left: add when symbol value is >= previous, // subtract otherwise. This naturally applies the subtractive rule. StringBuilder stringBuilderReverse = new StringBuilder(romanNumeral).reverse(); char[] chars = stringBuilderReverse.toString().toCharArray(); - int result = 0; + long result = 0; int previous = 0; for (char c : chars ) { Integer integer = MAP_FROM.get(c); + if (integer == null) { + throw new IllegalArgumentException("Unsupported Roman numeral character: " + c); + } if (integer >= previous) { result += integer; } else { @@ -101,7 +108,7 @@ public static int fromRoman(String romanNumeral) { previous = integer; } - return result; + return Math.toIntExact(result); } diff --git a/src/test/java/kyu4/RomanNumeralsTest.java b/src/test/java/kyu4/RomanNumeralsTest.java index b2f2904..1685c26 100644 --- a/src/test/java/kyu4/RomanNumeralsTest.java +++ b/src/test/java/kyu4/RomanNumeralsTest.java @@ -1,9 +1,11 @@ package kyu4; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.stream.Stream; import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -30,6 +32,19 @@ void shouldRoundTripCanonicalRomanNumerals(int value) { assertEquals(value, RomanNumerals.fromRoman(RomanNumerals.toRoman(value))); } + @ParameterizedTest + @MethodSource("invalidRomanNumerals") + void shouldRejectInvalidRomanNumerals(String romanNumeral) { + assertThrows(IllegalArgumentException.class, () -> RomanNumerals.fromRoman(romanNumeral)); + } + + @Test + void shouldRejectResultsOutsideIntegerRange() { + String romanNumeral = "M".repeat(2_147_484); + + assertThrows(ArithmeticException.class, () -> RomanNumerals.fromRoman(romanNumeral)); + } + private static Stream romanCases() { return Stream.of( Arguments.of(1, "I"), @@ -45,4 +60,8 @@ private static Stream romanCases() { Arguments.of(3_999, "MMMCMXCIX") ); } + + private static Stream invalidRomanNumerals() { + return Stream.of((String) null, "A", "MCMX?"); + } } From fefbb9e702ce89a4c757b674f4d2a8d0e900e948 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Sat, 1 Aug 2026 04:53:16 +0300 Subject: [PATCH 16/18] Validate resistor values before encoding --- src/main/java/kyu5/ResistorColorCodes2.java | 13 ++++++++++--- src/test/java/kyu5/ResistorColorCodes2Test.java | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/kyu5/ResistorColorCodes2.java b/src/main/java/kyu5/ResistorColorCodes2.java index 01522d8..b627ed0 100644 --- a/src/main/java/kyu5/ResistorColorCodes2.java +++ b/src/main/java/kyu5/ResistorColorCodes2.java @@ -9,9 +9,13 @@ public class ResistorColorCodes2 { public static String encodeResistorColors(String ohmsString) { if (ohmsString == null || !ohmsString.endsWith(" ohms")) return ""; - int resistorOhms = encodeResistorOhms(ohmsString); - int[] resistorOhmsArr = encodeResistorOhmsToArr(resistorOhms); - return encodeResistorArrToColor(resistorOhmsArr); + try { + int resistorOhms = encodeResistorOhms(ohmsString); + int[] resistorOhmsArr = encodeResistorOhmsToArr(resistorOhms); + return encodeResistorArrToColor(resistorOhmsArr); + } catch (IllegalArgumentException e) { + return ""; + } } private static int encodeResistorOhms(String ohmsString) { @@ -22,6 +26,9 @@ private static int encodeResistorOhms(String ohmsString) { else if (tempArray[0].endsWith("M")) tempValue = parseDouble(tempArray[0].trim().substring(0, tempArray[0].length() - 1)) * 1_000_000; else tempValue = parseDouble(tempArray[0].trim()); + if (!Double.isFinite(tempValue) || tempValue < 10 || tempValue > 990_000_000) { + throw new IllegalArgumentException("Resistance must be between 10 and 990M ohms"); + } return (int) Math.round(tempValue); } diff --git a/src/test/java/kyu5/ResistorColorCodes2Test.java b/src/test/java/kyu5/ResistorColorCodes2Test.java index 26086c4..d8112bb 100644 --- a/src/test/java/kyu5/ResistorColorCodes2Test.java +++ b/src/test/java/kyu5/ResistorColorCodes2Test.java @@ -23,6 +23,9 @@ void shouldRejectMalformedInput() { assertEquals("", ResistorColorCodes2.encodeResistorColors(null)); assertEquals("", ResistorColorCodes2.encodeResistorColors("47")); assertEquals("", ResistorColorCodes2.encodeResistorColors("47 ohm")); + assertEquals("", ResistorColorCodes2.encodeResistorColors("abc ohms")); + assertEquals("", ResistorColorCodes2.encodeResistorColors("-1 ohms")); + assertEquals("", ResistorColorCodes2.encodeResistorColors("1e309 ohms")); } private static Stream resistorCases() { From 0e23996d2d4ab05f4c0c37456bfd3f5a604ef485 Mon Sep 17 00:00:00 2001 From: Andrei Ovcharenko Date: Sat, 1 Aug 2026 11:34:06 +0300 Subject: [PATCH 17/18] Address remaining validation review findings --- src/main/java/algorithms/sprint0/Zip.java | 10 ++-- .../algorithms/sprint1/SleightOfHand.java | 12 ++--- src/main/java/algorithms/sprint2/Deque.java | 45 ++++++++--------- .../java/algorithms/sprint4/FindSystem.java | 49 ++++++++++++++++--- src/main/java/algorithms/sprint4/Map.java | 4 +- .../java/algorithms/sprint5/PyramidSort.java | 21 +++++--- .../java/algorithms/sprint6/DorogayaSet.java | 32 ++++++++---- .../sprint7/LevenshteinDistance.java | 25 ++++++---- .../java/algorithms/sprint8/PackedPrefix.java | 25 +++------- src/main/java/common/SafeParse.java | 20 ++++++++ src/main/java/kyu4/RomanNumerals.java | 6 ++- src/main/java/kyu5/ResistorColorCodes2.java | 24 ++++++--- src/main/java/kyu6/DigitalRoot.java | 2 - src/main/java/kyu7/DescendingOrder.java | 4 +- src/main/java/kyu7/MinimumLine.java | 4 +- src/main/java/kyu7/SquareDigit.java | 4 +- src/main/java/other/AdjustCase.java | 42 +++++----------- src/main/java/other/ReverseInt.java | 8 ++- src/test/java/algorithms/sprint0/ZipTest.java | 11 +++++ .../java/algorithms/sprint2/DequeTest.java | 26 ++++++++++ .../algorithms/sprint5/PyramidSortTest.java | 10 ++++ .../algorithms/sprint6/DorogayaSetTest.java | 31 ++++++++++++ .../sprint7/LevenshteinDistanceTest.java | 33 +++++++++++++ .../algorithms/sprint8/PackedPrefixTest.java | 20 ++++++++ src/test/java/kyu4/RomanNumeralsTest.java | 2 +- .../java/kyu5/ResistorColorCodes2Test.java | 1 + src/test/java/other/AdjustCaseTest.java | 9 ++++ 27 files changed, 345 insertions(+), 135 deletions(-) create mode 100644 src/test/java/algorithms/sprint2/DequeTest.java create mode 100644 src/test/java/algorithms/sprint6/DorogayaSetTest.java create mode 100644 src/test/java/algorithms/sprint7/LevenshteinDistanceTest.java create mode 100644 src/test/java/algorithms/sprint8/PackedPrefixTest.java diff --git a/src/main/java/algorithms/sprint0/Zip.java b/src/main/java/algorithms/sprint0/Zip.java index 944e66c..baec5c0 100644 --- a/src/main/java/algorithms/sprint0/Zip.java +++ b/src/main/java/algorithms/sprint0/Zip.java @@ -65,12 +65,16 @@ static void process(BufferedReader reader, BufferedWriter writer) throws IOExcep private static String readBoundedLine(BufferedReader reader) throws IOException { StringBuilder line = new StringBuilder(); int character; - while ((character = reader.read()) != -1 && character != '\n') { + while ((character = reader.read()) != -1 && character != '\n' && character != '\r') { if (line.length() == MAX_INPUT_LINE_LENGTH) { throw new IllegalArgumentException("Input line is too long"); } - if (character != '\r') { - line.append((char) character); + line.append((char) character); + } + if (character == '\r') { + reader.mark(1); + if (reader.read() != '\n') { + reader.reset(); } } return character == -1 && line.length() == 0 ? null : line.toString(); diff --git a/src/main/java/algorithms/sprint1/SleightOfHand.java b/src/main/java/algorithms/sprint1/SleightOfHand.java index 1a23981..eff10a0 100644 --- a/src/main/java/algorithms/sprint1/SleightOfHand.java +++ b/src/main/java/algorithms/sprint1/SleightOfHand.java @@ -115,14 +115,10 @@ void flush() throws IOException { } public static void main(String[] args) throws Exception { - try { - if (System.getProperty("os.name").startsWith("Windows")) { - test(); - } else { - run(); - } - } catch (IOException | IllegalArgumentException e) { - System.err.println(e.getMessage()); + if (System.getProperty("os.name").startsWith("Windows")) { + test(); + } else { + run(); } } diff --git a/src/main/java/algorithms/sprint2/Deque.java b/src/main/java/algorithms/sprint2/Deque.java index 2cd8803..43b15db 100644 --- a/src/main/java/algorithms/sprint2/Deque.java +++ b/src/main/java/algorithms/sprint2/Deque.java @@ -65,7 +65,7 @@ static final class RingDeque { private int size = 0; RingDeque(int cap) { - this.cap = safeCapacity(cap); + this.cap = validateCapacity(cap); this.a = new int[this.cap]; } @@ -114,16 +114,19 @@ int popBack() { } } - private static int safeCapacity(int cap) { - if (cap < 0) { - return 0; + private static int validateCapacity(int cap) { + if (cap < 0 || cap > MAX_CAPACITY) { + throw new IllegalArgumentException("Deque capacity is out of range"); } - return Math.min(cap, MAX_CAPACITY); + return cap; } private static void process(FastIn in, FastOut out) throws Exception { int n = in.nextInt(); int m = in.nextInt(); + if (n < 0 || n > MAX_CAPACITY) { + throw new IllegalArgumentException("Command count is out of range"); + } RingDeque dq = new RingDeque(m); @@ -244,26 +247,9 @@ private static void test() throws Exception { ) ); - // Некорректная емкость из ввода не должна приводить к аварийному завершению - assertEq( - "error\nerror\n", - solveIO( - "2\n" + - "-1\n" + - "push_back 1\n" + - "pop_front\n" - ) - ); - - // Слишком большая емкость ограничивается безопасным максимумом до выделения массива - assertEq( - "error\n", - solveIO( - "1\n" + - "1000000000\n" + - "pop_front\n" - ) - ); + // Некорректная емкость отклоняется, а не меняет заявленную семантику дека. + assertRejected("2\n-1\npush_back 1\npop_front\n"); + assertRejected("1\n1000000000\npop_front\n"); // Wrap-around: head/tail должны корректно "перепрыгивать" границу массива assertEq( @@ -291,6 +277,15 @@ static void assertEq(String exp, String act) { } } + private static void assertRejected(String input) throws Exception { + try { + solveIO(input); + throw new AssertionError("Expected invalid deque capacity to be rejected"); + } catch (IllegalArgumentException expected) { + // Expected validation failure. + } + } + public static void main(String[] args) throws Exception { if (System.getProperty("os.name").startsWith("Windows")) { test(); diff --git a/src/main/java/algorithms/sprint4/FindSystem.java b/src/main/java/algorithms/sprint4/FindSystem.java index 90f31dc..0999637 100644 --- a/src/main/java/algorithms/sprint4/FindSystem.java +++ b/src/main/java/algorithms/sprint4/FindSystem.java @@ -11,6 +11,7 @@ import java.util.StringTokenizer; import java.io.BufferedWriter; import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; // https://contest.yandex.ru/contest/24414/run-report/160043341/ @@ -19,6 +20,9 @@ class FindSystem { private static final int MAX_DOCUMENTS = 10_000; private static final int MAX_QUERIES = 10_000; private static final int MAX_LINE_LENGTH = 10_000; + private static final long MAX_TOTAL_DOCUMENT_CHARS = 2_000_000; + private static final long MAX_TOTAL_QUERY_CHARS = 2_000_000; + private static final long MAX_POSTING_VISITS = 20_000_000; /* * Принцип работы алгоритма: @@ -85,6 +89,17 @@ private static HashMap> buildIndex(String[] docs) { } private static String processQuery(String query, HashMap> index) { + try { + return processQuery(query, index, Long.MAX_VALUE).output(); + } catch (IOException impossible) { + throw new AssertionError(impossible); + } + } + + private static QueryResult processQuery( + String query, + HashMap> index, + long postingVisitBudget) throws IOException { HashSet uniqueWords = new HashSet<>(); StringTokenizer st = new StringTokenizer(query); @@ -93,12 +108,17 @@ private static String processQuery(String query, HashMap relevance = new HashMap<>(); + long postingVisits = 0; for (String word : uniqueWords) { ArrayList docs = index.get(word); if (docs == null) { continue; } + if (docs.size() > postingVisitBudget - postingVisits) { + throw new IOException("Aggregate query workload exceeds limit"); + } + postingVisits += docs.size(); for (int[] pair : docs) { int docId = pair[0]; @@ -134,7 +154,10 @@ private static String processQuery(String query, HashMap MAX_TOTAL_DOCUMENT_CHARS) { + throw new IOException("Aggregate document input exceeds limit"); + } } HashMap> index = buildIndex(docs); int m = reader.nextInt(MAX_QUERIES); - BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out)); + BufferedWriter out = new BufferedWriter( + new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); + long queryChars = 0; + long postingVisitsLeft = MAX_POSTING_VISITS; for (int i = 0; i < m; i++) { String query = reader.nextLine(MAX_LINE_LENGTH); - out.write(processQuery(query, index)); + queryChars += query.length(); + if (queryChars > MAX_TOTAL_QUERY_CHARS) { + throw new IOException("Aggregate query input exceeds limit"); + } + QueryResult result = processQuery(query, index, postingVisitsLeft); + postingVisitsLeft -= result.postingVisits(); + out.write(result.output()); out.newLine(); } @@ -212,11 +249,7 @@ public static void main(String[] args) throws Exception { if (System.getProperty("os.name").startsWith("Windows")) { test(); } else { - try { - solve(); - } catch (IOException ignored) { - // Invalid or excessive input is rejected without exhausting memory or CPU. - } + solve(); } } private static class FastReader { diff --git a/src/main/java/algorithms/sprint4/Map.java b/src/main/java/algorithms/sprint4/Map.java index 793d148..da9637b 100644 --- a/src/main/java/algorithms/sprint4/Map.java +++ b/src/main/java/algorithms/sprint4/Map.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.util.OptionalInt; import java.util.concurrent.ThreadLocalRandom; @@ -212,7 +213,8 @@ public static void main(String[] args) throws Exception { private static void solve() throws IOException { Reader reader = new Reader(); HashTable table = new HashTable(); - BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out)); + BufferedWriter out = new BufferedWriter( + new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); int n = reader.nextInt(0, MAX_COMMANDS); diff --git a/src/main/java/algorithms/sprint5/PyramidSort.java b/src/main/java/algorithms/sprint5/PyramidSort.java index e92fe2d..f4ad429 100644 --- a/src/main/java/algorithms/sprint5/PyramidSort.java +++ b/src/main/java/algorithms/sprint5/PyramidSort.java @@ -145,28 +145,33 @@ int nextInt() throws IOException { } } while (c <= ' '); - int sign = 1; - if (c == '-') { - sign = -1; + boolean negative = c == '-'; + if (negative) { c = read(); if (c <= ' ') { throw new NumberFormatException("Expected digit after sign"); } } - int val = 0; + int limit = negative ? Integer.MIN_VALUE : -Integer.MAX_VALUE; + int multiplyLimit = limit / 10; + int value = 0; while (c > ' ') { if (c < '0' || c > '9') { throw new NumberFormatException("Invalid integer input"); } int digit = c - '0'; - if (val > (Integer.MAX_VALUE - digit) / 10) { - throw new NumberFormatException("Integer input is too large"); + if (value < multiplyLimit) { + throw new NumberFormatException("Integer input is out of range"); } - val = val * 10 + digit; + value *= 10; + if (value < limit + digit) { + throw new NumberFormatException("Integer input is out of range"); + } + value -= digit; c = read(); } - return val * sign; + return negative ? value : -value; } String next() throws IOException { diff --git a/src/main/java/algorithms/sprint6/DorogayaSet.java b/src/main/java/algorithms/sprint6/DorogayaSet.java index d47b1fb..f2bca3b 100644 --- a/src/main/java/algorithms/sprint6/DorogayaSet.java +++ b/src/main/java/algorithms/sprint6/DorogayaSet.java @@ -181,21 +181,35 @@ int nextInt() throws IOException { } } while (c <= ' '); - int sign = 1; - - if (c == '-') { - sign = -1; + boolean negative = c == '-'; + if (negative) { c = read(); } - int val = 0; - + int limit = negative ? Integer.MIN_VALUE : -Integer.MAX_VALUE; + int multiplyLimit = limit / 10; + int value = 0; + boolean hasDigit = false; while (c > ' ') { - val = val * 10 + c - '0'; + if (c < '0' || c > '9') { + throw new NumberFormatException("Invalid integer input"); + } + int digit = c - '0'; + if (value < multiplyLimit) { + throw new NumberFormatException("Integer input is out of range"); + } + value *= 10; + if (value < limit + digit) { + throw new NumberFormatException("Integer input is out of range"); + } + value -= digit; + hasDigit = true; c = read(); } - - return val * sign; + if (!hasDigit) { + throw new NumberFormatException("Expected integer"); + } + return negative ? value : -value; } } diff --git a/src/main/java/algorithms/sprint7/LevenshteinDistance.java b/src/main/java/algorithms/sprint7/LevenshteinDistance.java index 2547124..aa3e61d 100644 --- a/src/main/java/algorithms/sprint7/LevenshteinDistance.java +++ b/src/main/java/algorithms/sprint7/LevenshteinDistance.java @@ -108,20 +108,25 @@ String nextLine(int maxLength) throws IOException { } while (c != -1 && c != '\n') { - if (c != '\r') { - if (size == maxLength) { - throw new IOException("Input line is too long"); - } - if (size == tmp.length) { - byte[] grown = new byte[Math.min(tmp.length * 2, maxLength)]; - System.arraycopy(tmp, 0, grown, 0, tmp.length); - tmp = grown; - } - tmp[size++] = (byte) c; + if (size == maxLength + 1) { + throw new IOException("Input line is too long"); } + if (size == tmp.length) { + byte[] grown = new byte[Math.min(tmp.length * 2, maxLength + 1)]; + System.arraycopy(tmp, 0, grown, 0, tmp.length); + tmp = grown; + } + tmp[size++] = (byte) c; c = read(); } + if (size > 0 && tmp[size - 1] == '\r') { + size--; + } + if (size > maxLength) { + throw new IOException("Input line is too long"); + } + return new String(tmp, 0, size, StandardCharsets.UTF_8); } } diff --git a/src/main/java/algorithms/sprint8/PackedPrefix.java b/src/main/java/algorithms/sprint8/PackedPrefix.java index 859ab05..299750d 100644 --- a/src/main/java/algorithms/sprint8/PackedPrefix.java +++ b/src/main/java/algorithms/sprint8/PackedPrefix.java @@ -247,34 +247,23 @@ private static boolean isValidPacked(String packed) { for (int i = 0; i < packed.length(); i++) { char current = packed.charAt(i); - if (current >= 'a' && current <= 'z') { - continue; - } - - if (current >= '0' && current <= '9') { + if (current >= '1' && current <= '9') { if (i + 1 >= packed.length() || packed.charAt(i + 1) != '[') { return false; } - continue; - } - - if (current == '[') { - if (i == 0 || packed.charAt(i - 1) < '0' || packed.charAt(i - 1) > '9') { + } else if (current == '[') { + if (i == 0 || packed.charAt(i - 1) < '1' || packed.charAt(i - 1) > '9') { return false; } top++; - continue; - } - - if (current == ']') { - if (top == 0) { + } else if (current == ']') { + if (top == 0 || packed.charAt(i - 1) == '[') { return false; } top--; - continue; + } else if (current < 'a' || current > 'z') { + return false; } - - return false; } return top == 0; diff --git a/src/main/java/common/SafeParse.java b/src/main/java/common/SafeParse.java index 8c4e14e..c5d9c50 100644 --- a/src/main/java/common/SafeParse.java +++ b/src/main/java/common/SafeParse.java @@ -24,4 +24,24 @@ public static double parseDouble(String value) { throw wrapped; } } + + public static int parseUnsignedInt(CharSequence value) { + if (value == null || value.length() == 0) { + throw new NumberFormatException("Expected unsigned integer"); + } + + int result = 0; + for (int index = 0; index < value.length(); index++) { + char current = value.charAt(index); + if (current < '0' || current > '9') { + throw new NumberFormatException("Invalid unsigned integer: " + value); + } + int digit = current - '0'; + if (result > (Integer.MAX_VALUE - digit) / 10) { + throw new NumberFormatException("Unsigned integer is out of range: " + value); + } + result = result * 10 + digit; + } + return result; + } } diff --git a/src/main/java/kyu4/RomanNumerals.java b/src/main/java/kyu4/RomanNumerals.java index 4a7c5f4..2dddf77 100644 --- a/src/main/java/kyu4/RomanNumerals.java +++ b/src/main/java/kyu4/RomanNumerals.java @@ -108,7 +108,11 @@ public static int fromRoman(String romanNumeral) { previous = integer; } - return Math.toIntExact(result); + int value = Math.toIntExact(result); + if (value < 1 || value > 3_999 || !toRoman(value).equals(romanNumeral)) { + throw new IllegalArgumentException("Roman numeral is not canonical: " + romanNumeral); + } + return value; } diff --git a/src/main/java/kyu5/ResistorColorCodes2.java b/src/main/java/kyu5/ResistorColorCodes2.java index b627ed0..8382a90 100644 --- a/src/main/java/kyu5/ResistorColorCodes2.java +++ b/src/main/java/kyu5/ResistorColorCodes2.java @@ -4,9 +4,15 @@ import static common.SafeParse.parseDouble; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class ResistorColorCodes2 { //5 + private static final Pattern OHMS_PATTERN = + Pattern.compile("([0-9]+(?:\\.[0-9]+)?)([kM]?) ohms"); + public static String encodeResistorColors(String ohmsString) { if (ohmsString == null || !ohmsString.endsWith(" ohms")) return ""; try { @@ -19,13 +25,17 @@ public static String encodeResistorColors(String ohmsString) { } private static int encodeResistorOhms(String ohmsString) { - double tempValue; - String[] tempArray = ohmsString.split("\\s"); - if (tempArray[0].endsWith("k")) - tempValue = parseDouble(tempArray[0].trim().substring(0, tempArray[0].length() - 1)) * 1_000; - else if (tempArray[0].endsWith("M")) - tempValue = parseDouble(tempArray[0].trim().substring(0, tempArray[0].length() - 1)) * 1_000_000; - else tempValue = parseDouble(tempArray[0].trim()); + Matcher matcher = OHMS_PATTERN.matcher(ohmsString); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid resistance format"); + } + + double tempValue = parseDouble(matcher.group(1)); + if (matcher.group(2).equals("k")) { + tempValue *= 1_000; + } else if (matcher.group(2).equals("M")) { + tempValue *= 1_000_000; + } if (!Double.isFinite(tempValue) || tempValue < 10 || tempValue > 990_000_000) { throw new IllegalArgumentException("Resistance must be between 10 and 990M ohms"); } diff --git a/src/main/java/kyu6/DigitalRoot.java b/src/main/java/kyu6/DigitalRoot.java index fae3c4a..9dbc467 100644 --- a/src/main/java/kyu6/DigitalRoot.java +++ b/src/main/java/kyu6/DigitalRoot.java @@ -33,9 +33,7 @@ public static int digitalRoot(int n) { * * @param n a non-negative integer * @return the digital root of {@code n} - * @deprecated use {@link #digitalRoot(int)} instead */ - @Deprecated public static int digital_root(int n) { return digitalRoot(n); } diff --git a/src/main/java/kyu7/DescendingOrder.java b/src/main/java/kyu7/DescendingOrder.java index 46137e9..5b13c5e 100644 --- a/src/main/java/kyu7/DescendingOrder.java +++ b/src/main/java/kyu7/DescendingOrder.java @@ -1,6 +1,8 @@ package kyu7; +import static common.SafeParse.parseUnsignedInt; + import java.util.ArrayList; import java.util.Comparator; @@ -23,7 +25,7 @@ public static int sortDesc(int num) { ) { result.append(i); } - return Integer.parseInt(result.toString()); + return parseUnsignedInt(result); } } diff --git a/src/main/java/kyu7/MinimumLine.java b/src/main/java/kyu7/MinimumLine.java index 4c10193..4cc5615 100644 --- a/src/main/java/kyu7/MinimumLine.java +++ b/src/main/java/kyu7/MinimumLine.java @@ -1,6 +1,8 @@ package kyu7; +import static common.SafeParse.parseUnsignedInt; + import java.util.Arrays; import java.util.stream.Collectors; @@ -15,7 +17,7 @@ public static int minValue(int[] values) { .distinct() .mapToObj(String::valueOf) .collect(Collectors.joining()); - return Integer.parseInt(result); + return parseUnsignedInt(result); } } diff --git a/src/main/java/kyu7/SquareDigit.java b/src/main/java/kyu7/SquareDigit.java index e27fe3a..a0ac22c 100644 --- a/src/main/java/kyu7/SquareDigit.java +++ b/src/main/java/kyu7/SquareDigit.java @@ -1,6 +1,8 @@ package kyu7; +import static common.SafeParse.parseUnsignedInt; + public class SquareDigit { private static final String MAX_INT = Integer.toString(Integer.MAX_VALUE); @@ -20,7 +22,7 @@ public static int squareDigits(int n) { || result.length() == MAX_INT.length() && result.compareTo(MAX_INT) > 0) { return Integer.MAX_VALUE; } - return Integer.parseInt(result); + return parseUnsignedInt(result); } } diff --git a/src/main/java/other/AdjustCase.java b/src/main/java/other/AdjustCase.java index 208b56a..4fb292a 100644 --- a/src/main/java/other/AdjustCase.java +++ b/src/main/java/other/AdjustCase.java @@ -2,7 +2,6 @@ import java.util.Locale; -import java.util.stream.Collectors; public class AdjustCase { @@ -11,41 +10,26 @@ public class AdjustCase { public static final String EMPTY = ""; public String adjustCaseToLower(String string) { - if (string == null || string.length() == 0) return string; - if (string.length() == 1) return string.toUpperCase(Locale.ROOT); - var stringLover = string.substring(1).toLowerCase(Locale.ROOT); - var firstChar = string.substring(0, 1).toUpperCase(Locale.ROOT); - return firstChar + stringLover; + return capitalizeFirst(string); } public String adjustCaseStream(String string) { - if (string == null || string.length() == 0) return string; - var lower = string.chars() - .mapToObj(i -> (char) i) - .map(Object::toString) - .map(value -> value.toLowerCase(Locale.ROOT)) - .collect(Collectors.joining()) - .substring(1); - return string.chars() - .mapToObj(i -> (char) i) - .map(Object::toString) - .findFirst() - .orElse("") - .toUpperCase(Locale.ROOT) + lower; + return capitalizeFirst(string); } public String adjustCaseFor(String string) { - if (string == null) return null; - char[] chars = string.toCharArray(); - for (int i = 0; i < chars.length; i++) { - if(i == 0) { - chars[i] = Character.toUpperCase(chars[i]); - } - else { - chars[i] = Character.toLowerCase(chars[i]); - } + return capitalizeFirst(string); + } + + private String capitalizeFirst(String string) { + if (string == null || string.isEmpty()) { + return string; } - return String.valueOf(chars); + int firstEnd = string.offsetByCodePoints(0, 1); + String first = string.substring(0, firstEnd); + String lower = string.toLowerCase(Locale.ROOT); + int loweredFirstLength = first.toLowerCase(Locale.ROOT).length(); + return first.toUpperCase(Locale.ROOT) + lower.substring(loweredFirstLength); } diff --git a/src/main/java/other/ReverseInt.java b/src/main/java/other/ReverseInt.java index e2c6764..e380fe5 100644 --- a/src/main/java/other/ReverseInt.java +++ b/src/main/java/other/ReverseInt.java @@ -4,8 +4,12 @@ public class ReverseInt { public static int reverse(int x) { - long reversed = Long.parseLong(new StringBuilder(String.valueOf(Math.abs((long) x))).reverse().toString()); - reversed = x < 0 ? -reversed : reversed; + long reversed = 0; + int remaining = x; + while (remaining != 0) { + reversed = reversed * 10 + remaining % 10; + remaining /= 10; + } return reversed < Integer.MIN_VALUE || reversed > Integer.MAX_VALUE ? 0 : (int) reversed; } diff --git a/src/test/java/algorithms/sprint0/ZipTest.java b/src/test/java/algorithms/sprint0/ZipTest.java index 57ef48f..dfe165c 100644 --- a/src/test/java/algorithms/sprint0/ZipTest.java +++ b/src/test/java/algorithms/sprint0/ZipTest.java @@ -65,6 +65,17 @@ void processAcceptsValidInput() throws IOException { assertEquals("1 7 5 8 6 9 ", output.toString()); } + @Test + void processAcceptsCrOnlyLineEndings() throws IOException { + StringWriter output = new StringWriter(); + BufferedWriter writer = new BufferedWriter(output); + + Zip.process(new BufferedReader(new StringReader("3\r1 3 5\r2 4 6\r")), writer); + writer.flush(); + + assertEquals("1 2 3 4 5 6 ", output.toString()); + } + @Test void processRejectsMissingOrShortLists() { assertThrows(IOException.class, () -> process("3\n1 2 3\n")); diff --git a/src/test/java/algorithms/sprint2/DequeTest.java b/src/test/java/algorithms/sprint2/DequeTest.java new file mode 100644 index 0000000..4e3e66f --- /dev/null +++ b/src/test/java/algorithms/sprint2/DequeTest.java @@ -0,0 +1,26 @@ +package algorithms.sprint2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class DequeTest { + + @Test + void rejectsCapacityInsteadOfSilentlyClampingIt() { + assertThrows(IllegalArgumentException.class, () -> new Deque.RingDeque(100_001)); + } + + @Test + void preservesSupportedCapacity() { + Deque.RingDeque deque = new Deque.RingDeque(2); + deque.pushBack(1); + deque.pushBack(2); + + assertEquals(2, deque.popBack()); + assertEquals(1, deque.popBack()); + } +} diff --git a/src/test/java/algorithms/sprint5/PyramidSortTest.java b/src/test/java/algorithms/sprint5/PyramidSortTest.java index cd12398..30c3ca8 100644 --- a/src/test/java/algorithms/sprint5/PyramidSortTest.java +++ b/src/test/java/algorithms/sprint5/PyramidSortTest.java @@ -22,6 +22,16 @@ void fastInRejectsOverflowingInteger() { assertThrows(NumberFormatException.class, in::nextInt); } + @Test + void fastInAcceptsMinimumInteger() throws Exception { + assertEquals(Integer.MIN_VALUE, fastIn("-2147483648 ").nextInt()); + } + + @Test + void fastInRejectsValuesBelowMinimumInteger() { + assertThrows(NumberFormatException.class, () -> fastIn("-2147483649 ").nextInt()); + } + @Test void fastInRejectsOversizedLoginToken() { PyramidSort.FastIn in = fastIn("a".repeat(1_025) + " "); diff --git a/src/test/java/algorithms/sprint6/DorogayaSetTest.java b/src/test/java/algorithms/sprint6/DorogayaSetTest.java new file mode 100644 index 0000000..6df401c --- /dev/null +++ b/src/test/java/algorithms/sprint6/DorogayaSetTest.java @@ -0,0 +1,31 @@ +package algorithms.sprint6; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class DorogayaSetTest { + + @Test + void fastInRejectsOverflowingIntegerTokens() { + assertThrows(NumberFormatException.class, () -> fastIn("4294967296 ").nextInt()); + } + + @Test + void fastInAcceptsIntegerBounds() throws Exception { + DorogayaSet.FastIn input = fastIn("-2147483648 2147483647 "); + + assertEquals(Integer.MIN_VALUE, input.nextInt()); + assertEquals(Integer.MAX_VALUE, input.nextInt()); + } + + private static DorogayaSet.FastIn fastIn(String value) { + return new DorogayaSet.FastIn( + new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/src/test/java/algorithms/sprint7/LevenshteinDistanceTest.java b/src/test/java/algorithms/sprint7/LevenshteinDistanceTest.java new file mode 100644 index 0000000..57d0916 --- /dev/null +++ b/src/test/java/algorithms/sprint7/LevenshteinDistanceTest.java @@ -0,0 +1,33 @@ +package algorithms.sprint7; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class LevenshteinDistanceTest { + + @Test + void stripsOnlyTheOptionalCrLfTerminator() throws IOException { + LevenshteinDistance.FastIn input = fastIn("abc\r\n"); + + assertEquals("abc", input.nextLine(3)); + } + + @Test + void countsEmbeddedCarriageReturnsTowardTheLimit() { + LevenshteinDistance.FastIn input = fastIn("\r\r\r\n"); + + assertThrows(IOException.class, () -> input.nextLine(1)); + } + + private static LevenshteinDistance.FastIn fastIn(String value) { + return new LevenshteinDistance.FastIn( + new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/src/test/java/algorithms/sprint8/PackedPrefixTest.java b/src/test/java/algorithms/sprint8/PackedPrefixTest.java new file mode 100644 index 0000000..8fa6eed --- /dev/null +++ b/src/test/java/algorithms/sprint8/PackedPrefixTest.java @@ -0,0 +1,20 @@ +package algorithms.sprint8; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class PackedPrefixTest { + + @Test + void rejectsEmptyRepeatBodies() { + assertEquals("", PackedPrefix.solve(new String[]{"9[9[9[]]]"})); + } + + @Test + void rejectsZeroRepeatCounts() { + assertEquals("", PackedPrefix.solve(new String[]{"0[a]"})); + } +} diff --git a/src/test/java/kyu4/RomanNumeralsTest.java b/src/test/java/kyu4/RomanNumeralsTest.java index 1685c26..4b9d67e 100644 --- a/src/test/java/kyu4/RomanNumeralsTest.java +++ b/src/test/java/kyu4/RomanNumeralsTest.java @@ -62,6 +62,6 @@ private static Stream romanCases() { } private static Stream invalidRomanNumerals() { - return Stream.of((String) null, "A", "MCMX?"); + return Stream.of((String) null, "", "A", "MCMX?", "IIII", "VX", "MMMM"); } } diff --git a/src/test/java/kyu5/ResistorColorCodes2Test.java b/src/test/java/kyu5/ResistorColorCodes2Test.java index d8112bb..ef5bd5b 100644 --- a/src/test/java/kyu5/ResistorColorCodes2Test.java +++ b/src/test/java/kyu5/ResistorColorCodes2Test.java @@ -26,6 +26,7 @@ void shouldRejectMalformedInput() { assertEquals("", ResistorColorCodes2.encodeResistorColors("abc ohms")); assertEquals("", ResistorColorCodes2.encodeResistorColors("-1 ohms")); assertEquals("", ResistorColorCodes2.encodeResistorColors("1e309 ohms")); + assertEquals("", ResistorColorCodes2.encodeResistorColors("47 garbage ohms")); } private static Stream resistorCases() { diff --git a/src/test/java/other/AdjustCaseTest.java b/src/test/java/other/AdjustCaseTest.java index 723d0bf..8f15c84 100644 --- a/src/test/java/other/AdjustCaseTest.java +++ b/src/test/java/other/AdjustCaseTest.java @@ -24,6 +24,15 @@ void adjustsSuffixWithoutRetainingExpandedLowercaseCharacters() { assertEquals("\u0130bc", adjustCase.adjustCaseToLower("\u0130BC")); } + @Test + void preservesContextSensitiveFinalSigmaAcrossImplementations() { + other.AdjustCase adjustCase = new other.AdjustCase(); + + assertEquals("Aς", adjustCase.adjustCaseToLower("AΣ")); + assertEquals("Aς", adjustCase.adjustCaseStream("AΣ")); + assertEquals("Aς", adjustCase.adjustCaseFor("AΣ")); + } + @Test void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(other.AdjustCase.class); From 8d82b972ce7086aaf34f77fa86e5ecad9b15127f Mon Sep 17 00:00:00 2001 From: "Andrei.Ovcharenko" Date: Sat, 1 Aug 2026 12:22:39 +0300 Subject: [PATCH 18/18] fix: preserve valid search workloads --- .../java/algorithms/sprint4/FindSystem.java | 39 +------------------ 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/src/main/java/algorithms/sprint4/FindSystem.java b/src/main/java/algorithms/sprint4/FindSystem.java index 0999637..b084c1e 100644 --- a/src/main/java/algorithms/sprint4/FindSystem.java +++ b/src/main/java/algorithms/sprint4/FindSystem.java @@ -20,9 +20,6 @@ class FindSystem { private static final int MAX_DOCUMENTS = 10_000; private static final int MAX_QUERIES = 10_000; private static final int MAX_LINE_LENGTH = 10_000; - private static final long MAX_TOTAL_DOCUMENT_CHARS = 2_000_000; - private static final long MAX_TOTAL_QUERY_CHARS = 2_000_000; - private static final long MAX_POSTING_VISITS = 20_000_000; /* * Принцип работы алгоритма: @@ -89,17 +86,6 @@ private static HashMap> buildIndex(String[] docs) { } private static String processQuery(String query, HashMap> index) { - try { - return processQuery(query, index, Long.MAX_VALUE).output(); - } catch (IOException impossible) { - throw new AssertionError(impossible); - } - } - - private static QueryResult processQuery( - String query, - HashMap> index, - long postingVisitBudget) throws IOException { HashSet uniqueWords = new HashSet<>(); StringTokenizer st = new StringTokenizer(query); @@ -108,17 +94,12 @@ private static QueryResult processQuery( } HashMap relevance = new HashMap<>(); - long postingVisits = 0; for (String word : uniqueWords) { ArrayList docs = index.get(word); if (docs == null) { continue; } - if (docs.size() > postingVisitBudget - postingVisits) { - throw new IOException("Aggregate query workload exceeds limit"); - } - postingVisits += docs.size(); for (int[] pair : docs) { int docId = pair[0]; @@ -154,10 +135,7 @@ private static QueryResult processQuery( sb.append(best.get(i)[0]); } - return new QueryResult(sb.toString(), postingVisits); - } - - private record QueryResult(String output, long postingVisits) { + return sb.toString(); } private static boolean isBetter(int docId1, int score1, int docId2, int score2) { @@ -172,13 +150,8 @@ private static void solve() throws Exception { int n = reader.nextInt(MAX_DOCUMENTS); String[] docs = new String[n]; - long documentChars = 0; for (int i = 0; i < n; i++) { docs[i] = reader.nextLine(MAX_LINE_LENGTH); - documentChars += docs[i].length(); - if (documentChars > MAX_TOTAL_DOCUMENT_CHARS) { - throw new IOException("Aggregate document input exceeds limit"); - } } HashMap> index = buildIndex(docs); @@ -186,18 +159,10 @@ private static void solve() throws Exception { int m = reader.nextInt(MAX_QUERIES); BufferedWriter out = new BufferedWriter( new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); - long queryChars = 0; - long postingVisitsLeft = MAX_POSTING_VISITS; for (int i = 0; i < m; i++) { String query = reader.nextLine(MAX_LINE_LENGTH); - queryChars += query.length(); - if (queryChars > MAX_TOTAL_QUERY_CHARS) { - throw new IOException("Aggregate query input exceeds limit"); - } - QueryResult result = processQuery(query, index, postingVisitsLeft); - postingVisitsLeft -= result.postingVisits(); - out.write(result.output()); + out.write(processQuery(query, index)); out.newLine(); }