refactor(api): replace regex string literals with Pattern instances for improved performance and readability (#5680)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
Balázs Szücs 2026-02-14 22:01:19 +01:00 committed by GitHub
parent 0a1d2effdc
commit e310493966
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 130 additions and 53 deletions

View File

@ -9,6 +9,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@ -25,6 +26,9 @@ import lombok.extern.slf4j.Slf4j;
public class MobileScannerService {
private static final long SESSION_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
private static final Pattern FILENAME_SANITIZE_PATTERN = Pattern.compile("[^a-zA-Z0-9._-]");
private static final Pattern SESSION_ID_VALIDATION_PATTERN = Pattern.compile("[a-zA-Z0-9-]+");
private static final Pattern FILE_EXTENSION_PATTERN = Pattern.compile("[.][^.]+$");
private final Map<String, SessionData> activeSessions = new ConcurrentHashMap<>();
private final Path tempDirectory;
@ -121,7 +125,8 @@ public class MobileScannerService {
// Handle duplicate filenames
int counter = 1;
while (Files.exists(filePath)) {
String nameWithoutExt = safeFilename.replaceFirst("[.][^.]+$", "");
String nameWithoutExt =
FILE_EXTENSION_PATTERN.matcher(safeFilename).replaceFirst("");
String ext =
safeFilename.contains(".")
? safeFilename.substring(safeFilename.lastIndexOf("."))
@ -271,14 +276,14 @@ public class MobileScannerService {
throw new IllegalArgumentException("Session ID cannot be empty");
}
// Basic validation: alphanumeric and hyphens only
if (!sessionId.matches("[a-zA-Z0-9-]+")) {
if (!SESSION_ID_VALIDATION_PATTERN.matcher(sessionId).matches()) {
throw new IllegalArgumentException("Invalid session ID format");
}
}
private String sanitizeFilename(String filename) {
// Remove path traversal attempts and dangerous characters
String sanitized = filename.replaceAll("[^a-zA-Z0-9._-]", "_");
String sanitized = FILENAME_SANITIZE_PATTERN.matcher(filename).replaceAll("_");
// Ensure we have a non-empty, safe filename
if (sanitized.isBlank()) {
sanitized = "upload-" + System.currentTimeMillis();

View File

@ -47,6 +47,7 @@ public class SvgSanitizer {
private static final Pattern DATA_SCRIPT_PATTERN =
Pattern.compile(
"^\\s*data\\s*:[^,]*(?:script|javascript|vbscript)", Pattern.CASE_INSENSITIVE);
private static final Pattern NULL_BYTE_PATTERN = Pattern.compile("\u0000");
private final SsrfProtectionService ssrfProtectionService;
private final ApplicationProperties applicationProperties;
@ -210,7 +211,7 @@ public class SvgSanitizer {
String result = url.trim();
result = result.replaceAll("\u0000", "");
result = NULL_BYTE_PATTERN.matcher(result).replaceAll("");
for (int i = 0; i < 3; i++) {
try {

View File

@ -9,6 +9,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -38,6 +39,10 @@ import stirling.software.common.model.ApplicationProperties;
})
public class SPDFApplication {
private static final Pattern PORT_SUFFIX_PATTERN = Pattern.compile(".+:\\d+$");
private static final Pattern URL_SCHEME_PATTERN =
Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*://.*");
private static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/+$");
private static String serverPortStatic;
private static String baseUrlStatic;
private static String contextPathStatic;
@ -244,8 +249,8 @@ public class SPDFApplication {
String trimmedBase =
(backendUrl == null || backendUrl.isBlank())
? "http://localhost"
: backendUrl.trim().replaceAll("/+$", "");
boolean hasScheme = trimmedBase.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*");
: TRAILING_SLASH_PATTERN.matcher(backendUrl.trim()).replaceAll("");
boolean hasScheme = URL_SCHEME_PATTERN.matcher(trimmedBase).matches();
String baseForParsing = hasScheme ? trimmedBase : "http://" + trimmedBase;
Integer parsedPort = parsePort(port);
@ -298,7 +303,7 @@ public class SPDFApplication {
if (port == null) {
return trimmedBase;
}
if (trimmedBase.matches(".+:\\d+$")) {
if (PORT_SUFFIX_PATTERN.matcher(trimmedBase).matches()) {
return trimmedBase;
}
return trimmedBase + ":" + port;

View File

@ -8,6 +8,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
@ -51,6 +52,7 @@ import stirling.software.common.util.WebResponseUtils;
@RequiredArgsConstructor
public class MergeController {
private static final Pattern QUOTE_WRAP_PATTERN = Pattern.compile("^\"|\"$");
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@ -173,7 +175,7 @@ public class MergeController {
String[] parts = inside.split(",");
String[] result = new String[parts.length];
for (int i = 0; i < parts.length; i++) {
result[i] = parts[i].trim().replaceAll("^\"|\"$", "");
result[i] = QUOTE_WRAP_PATTERN.matcher(parts[i].trim()).replaceAll("");
}
return result;
}

View File

@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api.converters;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
@ -36,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils;
@RequiredArgsConstructor
public class ConvertPdfJsonController {
private static final Pattern FILE_EXTENSION_PATTERN = Pattern.compile("[.][^.]+$");
private final PdfJsonConversionService pdfJsonConversionService;
@Autowired(required = false)
@ -59,7 +61,9 @@ public class ConvertPdfJsonController {
String originalName = inputFile.getOriginalFilename();
String baseName =
(originalName != null && !originalName.isBlank())
? Filenames.toSimpleFileName(originalName).replaceFirst("[.][^.]+$", "")
? FILE_EXTENSION_PATTERN
.matcher(Filenames.toSimpleFileName(originalName))
.replaceFirst("")
: "document";
String docName = baseName + ".json";
return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON);
@ -82,7 +86,9 @@ public class ConvertPdfJsonController {
String originalName = jsonFile.getOriginalFilename();
String baseName =
(originalName != null && !originalName.isBlank())
? Filenames.toSimpleFileName(originalName).replaceFirst("[.][^.]+$", "")
? FILE_EXTENSION_PATTERN
.matcher(Filenames.toSimpleFileName(originalName))
.replaceFirst("")
: "document";
String docName = baseName.endsWith(".pdf") ? baseName : baseName + ".pdf";
return WebResponseUtils.bytesToWebResponse(pdfBytes, docName);
@ -115,7 +121,9 @@ public class ConvertPdfJsonController {
String originalName = inputFile.getOriginalFilename();
String baseName =
(originalName != null && !originalName.isBlank())
? Filenames.toSimpleFileName(originalName).replaceFirst("[.][^.]+$", "")
? FILE_EXTENSION_PATTERN
.matcher(Filenames.toSimpleFileName(originalName))
.replaceFirst("")
: "document";
String docName = baseName + "_metadata.json";
@ -152,7 +160,9 @@ public class ConvertPdfJsonController {
String baseName =
(filename != null && !filename.isBlank())
? Filenames.toSimpleFileName(filename).replaceFirst("[.][^.]+$", "")
? FILE_EXTENSION_PATTERN
.matcher(Filenames.toSimpleFileName(filename))
.replaceFirst("")
: Optional.ofNullable(document.getMetadata())
.map(PdfJsonMetadata::getTitle)
.filter(title -> title != null && !title.isBlank())

View File

@ -57,6 +57,7 @@ import stirling.software.common.util.WebResponseUtils;
@RequiredArgsConstructor
public class StampController {
private static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@ -266,7 +267,7 @@ public class StampController {
.getEscapedNewlinePattern()
.matcher(processedStampText)
.replaceAll("\n");
String[] lines = normalizedText.split("\\r?\\n");
String[] lines = NEWLINE_PATTERN.split(normalizedText);
PDRectangle pageSize = page.getMediaBox();

View File

@ -21,6 +21,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.springframework.core.io.Resource;
@ -44,6 +45,7 @@ import stirling.software.common.util.FileMonitor;
public class PipelineDirectoryProcessor {
private static final int MAX_DIRECTORY_DEPTH = 50; // Prevent excessive recursion
private static final Pattern WATCHED_FOLDERS_PATTERN = Pattern.compile("\\\\?watchedFolders");
private final ObjectMapper objectMapper;
private final ApiDocService apiDocService;
@ -433,10 +435,12 @@ public class PipelineDirectoryProcessor {
private Path determineOutputPath(PipelineConfig config, Path dir) {
String outputDir =
config.getOutputDir()
.replace("{outputFolder}", finishedFoldersDir)
.replace("{folderName}", dir.toString())
.replaceAll("\\\\?watchedFolders", "");
WATCHED_FOLDERS_PATTERN
.matcher(
config.getOutputDir()
.replace("{outputFolder}", finishedFoldersDir)
.replace("{folderName}", dir.toString()))
.replaceAll("");
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
}

View File

@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
@ -28,6 +29,8 @@ public class ReactRoutingController {
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(ReactRoutingController.class);
private static final Pattern BASE_HREF_PATTERN =
Pattern.compile("<base href=\\\"[^\\\"]*\\\"\\s*/?>");
@Value("${server.servlet.context-path:/}")
private String contextPath;
@ -94,9 +97,9 @@ public class ReactRoutingController {
html = html.replace("%BASE_URL%", baseUrl);
// Also rewrite any existing <base> tag (Vite may have baked one in)
html =
html.replaceFirst(
"<base href=\\\"[^\\\"]*\\\"\\s*/?>",
"<base href=\\\"" + baseUrl + "\\\" />");
BASE_HREF_PATTERN
.matcher(html)
.replaceFirst("<base href=\\\"" + baseUrl + "\\\" />");
// Inject context path as a global variable for API calls
String contextPathScript =

View File

@ -38,6 +38,7 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.imageio.ImageIO;
@ -133,6 +134,9 @@ import stirling.software.common.util.TempFileManager;
@RequiredArgsConstructor
public class PdfJsonConversionService {
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
private static final Pattern WHITESPACE_DASH_UNDERSCORE_PATTERN = Pattern.compile("[\\s\\-_]");
private static final Pattern FONT_SUBSET_PREFIX_PATTERN = Pattern.compile("^[A-Z]{6}\\+");
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final ObjectMapper objectMapper;
private final EndpointConfiguration endpointConfiguration;
@ -531,7 +535,10 @@ public class PdfJsonConversionService {
: "Unknown";
// Clean up subset prefix (e.g., "ABCDEF+TimesNewRoman"
// -> "TimesNewRoman")
String cleanName = name.replaceAll("^[A-Z]{6}\\+", "");
String cleanName =
FONT_SUBSET_PREFIX_PATTERN
.matcher(name)
.replaceAll("");
return String.format("%s (%s)", cleanName, subtype);
})
.collect(java.util.stream.Collectors.toList());
@ -1442,9 +1449,9 @@ public class PdfJsonConversionService {
if (!fallbackFontService.canEncodeFully(font, text)) {
String fontName =
fontModel != null && fontModel.getBaseName() != null
? fontModel
.getBaseName()
.replaceAll("^[A-Z]{6}\\+", "") // Remove subset prefix
? FONT_SUBSET_PREFIX_PATTERN
.matcher(fontModel.getBaseName())
.replaceAll("") // Remove subset prefix
: (font != null ? font.getName() : "unknown");
String fontKey = fontName + ":" + element.getFontId() + ":" + pageNumber;
if (!warnedFonts.contains(fontKey)) {
@ -1902,7 +1909,10 @@ public class PdfJsonConversionService {
if (plusIndex >= 0 && plusIndex < normalized.length() - 1) {
normalized = normalized.substring(plusIndex + 1);
}
normalized = normalized.toLowerCase(Locale.ROOT).replaceAll("[\\s\\-_]", "");
normalized =
WHITESPACE_DASH_UNDERSCORE_PATTERN
.matcher(normalized.toLowerCase(Locale.ROOT))
.replaceAll("");
// Exact match after normalization
try {
@ -3256,7 +3266,7 @@ public class PdfJsonConversionService {
if (value == null) {
return "";
}
String trimmed = value.replaceAll("\s+", " ").trim();
String trimmed = WHITESPACE_PATTERN.matcher(value).replaceAll(" ").trim();
if (trimmed.length() <= 32) {
return trimmed;
}

View File

@ -7,6 +7,7 @@ import java.io.InputStream;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.font.PDFont;
@ -310,6 +311,11 @@ public class PdfJsonFallbackFontService {
"classpath:/static/fonts/DejaVuSansMono-BoldOblique.ttf",
"DejaVuSansMono-BoldOblique",
"ttf")));
private static final Pattern BOLD_FONT_WEIGHT_PATTERN =
Pattern.compile(".*[_-]?[6-9]00(wght)?.*");
private static final Pattern FONT_NAME_DELIMITER_PATTERN = Pattern.compile("[-_,+]");
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
private static final Pattern PATTERN = Pattern.compile("^[A-Z]{6}\\+");
private final ResourceLoader resourceLoader;
private final stirling.software.common.model.ApplicationProperties applicationProperties;
@ -418,16 +424,17 @@ public class PdfJsonFallbackFontService {
// Normalize font name: remove subset prefix (e.g. "PXAAAC+"), convert to lowercase,
// remove spaces
String normalized =
originalFontName
.replaceAll("^[A-Z]{6}\\+", "") // Remove subset prefix
.toLowerCase()
.replaceAll("\\s+", ""); // Remove spaces (e.g. "Times New Roman" ->
WHITESPACE_PATTERN
.matcher(
PATTERN.matcher(originalFontName).replaceAll("") // Remove subset prefix
.toLowerCase())
.replaceAll(""); // Remove spaces (e.g. "Times New Roman" ->
// "timesnewroman")
// Extract base name without weight/style suffixes
// Split on common delimiters: hyphen, underscore, comma, plus
// Handles: "Arimo_700wght" -> "arimo", "Arial-Bold" -> "arial", "Arial,Bold" -> "arial"
String baseName = normalized.split("[-_,+]")[0];
String baseName = FONT_NAME_DELIMITER_PATTERN.split(normalized)[0];
String aliasedFontId = FONT_NAME_ALIASES.get(baseName);
if (aliasedFontId != null) {
@ -470,7 +477,7 @@ public class PdfJsonFallbackFontService {
// Check for numeric weight indicators (600-900 = bold)
// Handles: "Arimo_700wght", "Arial-700", "Font-w700"
if (normalizedFontName.matches(".*[_-]?[6-9]00(wght)?.*")) {
if (BOLD_FONT_WEIGHT_PATTERN.matcher(normalizedFontName).matches()) {
return true;
}
@ -514,7 +521,7 @@ public class PdfJsonFallbackFontService {
// Supported: Liberation (Sans/Serif/Mono), Noto Sans, DejaVu (Sans/Serif/Mono)
boolean isSupported =
baseFontId.startsWith("fallback-liberation-")
|| baseFontId.equals("fallback-noto-sans")
|| "fallback-noto-sans".equals(baseFontId)
|| baseFontId.startsWith("fallback-dejavu-");
if (!isSupported) {
@ -523,8 +530,8 @@ public class PdfJsonFallbackFontService {
// DejaVu Sans and Mono use "oblique" instead of "italic"
boolean useOblique =
baseFontId.equals("fallback-dejavu-sans")
|| baseFontId.equals("fallback-dejavu-mono");
"fallback-dejavu-sans".equals(baseFontId)
|| "fallback-dejavu-mono".equals(baseFontId);
if (isBold && isItalic) {
return baseFontId + (useOblique ? "-boldoblique" : "-bolditalic");

View File

@ -9,6 +9,7 @@ import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.springframework.stereotype.Service;
@ -27,6 +28,7 @@ import stirling.software.common.configuration.InstallationPathConfig;
@Slf4j
public class SharedSignatureService {
private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$");
private final String SIGNATURE_BASE_PATH;
private final String ALL_USERS_FOLDER = "ALL_USERS";
private final ObjectMapper objectMapper;
@ -105,7 +107,7 @@ public class SharedSignatureService {
throw new IllegalArgumentException("Invalid filename");
}
// Only allow alphanumeric, hyphen, underscore, and dot (for extensions)
if (!fileName.matches("^[a-zA-Z0-9_.-]+$")) {
if (!FILENAME_VALIDATION_PATTERN.matcher(fileName).matches()) {
throw new IllegalArgumentException("Filename contains invalid characters");
}
}
@ -113,7 +115,7 @@ public class SharedSignatureService {
private String validateAndNormalizeExtension(String extension) {
String normalized = extension.toLowerCase().trim();
// Whitelist only safe image extensions
if (normalized.equals("png") || normalized.equals("jpg") || normalized.equals("jpeg")) {
if ("png".equals(normalized) || "jpg".equals(normalized) || "jpeg".equals(normalized)) {
return normalized;
}
throw new IllegalArgumentException("Unsupported image extension: " + extension);

View File

@ -18,6 +18,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.jupiter.api.AfterEach;
@ -46,6 +47,7 @@ import stirling.software.common.util.WebResponseUtils;
public class ConvertWebsiteToPdfTest {
private static final Pattern PDF_FILENAME_PATTERN = Pattern.compile("[A-Za-z0-9_]+\\.pdf");
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private RuntimePathConfig runtimePathConfig;
@ -142,7 +144,7 @@ public class ConvertWebsiteToPdfTest {
assertTrue(out.endsWith(".pdf"));
// Only AZ, az, 09, underscore and dot allowed
assertTrue(out.matches("[A-Za-z0-9_]+\\.pdf"));
assertTrue(PDF_FILENAME_PATTERN.matcher(out).matches());
// no truncation here (source not that long)
assertTrue(out.length() <= 54);
}
@ -159,7 +161,7 @@ public class ConvertWebsiteToPdfTest {
String out = (String) m.invoke(sut, longUrl);
assertTrue(out.endsWith(".pdf"));
assertTrue(out.matches("[A-Za-z0-9_]+\\.pdf"));
assertTrue(PDF_FILENAME_PATTERN.matcher(out).matches());
// safeName limited to 50 -> total max 54 including '.pdf'
assertTrue(out.length() <= 54, "Filename should be truncated to 50 + '.pdf'");
}

View File

@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.regex.Pattern;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
@ -26,6 +27,18 @@ import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class StampControllerTest {
private static final Pattern UUID_HEX_PATTERN = Pattern.compile("[0-9a-f]{8}");
private static final Pattern DATE_LITERAL_REGEX =
Pattern.compile("@date is \\d{4}-\\d{2}-\\d{2}");
private static final Pattern DATE_TIME_MIN_PATTERN =
Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}");
private static final Pattern DATE_SLASH_PATTERN = Pattern.compile("\\d{2}/\\d{2}/\\d{4}");
private static final Pattern DAY_LABEL_PATTERN = Pattern.compile("Day: \\d{2}");
private static final Pattern MONTH_LABEL_PATTERN = Pattern.compile("Month: \\d{2}");
private static final Pattern DATE_TIME_FULL_PATTERN =
Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}");
private static final Pattern TIME_LABEL_PATTERN = Pattern.compile("Time: \\d{2}:\\d{2}:\\d{2}");
private static final Pattern DATE_LABEL_PATTERN = Pattern.compile("Date: \\d{4}-\\d{2}-\\d{2}");
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@ -173,7 +186,7 @@ class StampControllerTest {
void testDateReplacement() throws Exception {
String result = invokeProcessStampText("Date: @date", 1, 1, "test.pdf", null);
assertTrue(
result.matches("Date: \\d{4}-\\d{2}-\\d{2}"),
DATE_LABEL_PATTERN.matcher(result).matches(),
"Date should match YYYY-MM-DD format");
}
@ -182,7 +195,7 @@ class StampControllerTest {
void testTimeReplacement() throws Exception {
String result = invokeProcessStampText("Time: @time", 1, 1, "test.pdf", null);
assertTrue(
result.matches("Time: \\d{2}:\\d{2}:\\d{2}"),
TIME_LABEL_PATTERN.matcher(result).matches(),
"Time should match HH:mm:ss format");
}
@ -192,7 +205,7 @@ class StampControllerTest {
String result = invokeProcessStampText("@datetime", 1, 1, "test.pdf", null);
// DateTime format: YYYY-MM-DD HH:mm:ss
assertTrue(
result.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"),
DATE_TIME_FULL_PATTERN.matcher(result).matches(),
"DateTime should match YYYY-MM-DD HH:mm:ss format");
}
@ -208,14 +221,15 @@ class StampControllerTest {
@DisplayName("Should replace @month with zero-padded month")
void testMonthReplacement() throws Exception {
String result = invokeProcessStampText("Month: @month", 1, 1, "test.pdf", null);
assertTrue(result.matches("Month: \\d{2}"), "Month should be zero-padded");
assertTrue(
MONTH_LABEL_PATTERN.matcher(result).matches(), "Month should be zero-padded");
}
@Test
@DisplayName("Should replace @day with zero-padded day")
void testDayReplacement() throws Exception {
String result = invokeProcessStampText("Day: @day", 1, 1, "test.pdf", null);
assertTrue(result.matches("Day: \\d{2}"), "Day should be zero-padded");
assertTrue(DAY_LABEL_PATTERN.matcher(result).matches(), "Day should be zero-padded");
}
}
@ -228,7 +242,7 @@ class StampControllerTest {
void testCustomDateFormatSlash() throws Exception {
String result = invokeProcessStampText("@date{dd/MM/yyyy}", 1, 1, "test.pdf", null);
assertTrue(
result.matches("\\d{2}/\\d{2}/\\d{4}"),
DATE_SLASH_PATTERN.matcher(result).matches(),
"Should match dd/MM/yyyy format: " + result);
}
@ -238,7 +252,7 @@ class StampControllerTest {
String result =
invokeProcessStampText("@date{yyyy-MM-dd HH:mm}", 1, 1, "test.pdf", null);
assertTrue(
result.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}"),
DATE_TIME_MIN_PATTERN.matcher(result).matches(),
"Should match yyyy-MM-dd HH:mm format: " + result);
}
@ -345,7 +359,7 @@ class StampControllerTest {
// @@date should become @date, and @date should be replaced with actual date
assertTrue(result.startsWith("@date is "), "Should start with literal @date");
assertTrue(
result.matches("@date is \\d{4}-\\d{2}-\\d{2}"),
DATE_LITERAL_REGEX.matcher(result).matches(),
"Should have date after: " + result);
}
@ -463,7 +477,9 @@ class StampControllerTest {
@DisplayName("UUID should contain only hex characters")
void testUuidFormat() throws Exception {
String result = invokeProcessStampText("@uuid", 1, 1, "test.pdf", null);
assertTrue(result.matches("[0-9a-f]{8}"), "UUID should be 8 hex characters: " + result);
assertTrue(
UUID_HEX_PATTERN.matcher(result).matches(),
"UUID should be 8 hex characters: " + result);
}
}

View File

@ -9,6 +9,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.regex.Pattern;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -36,6 +37,7 @@ import stirling.software.common.configuration.RuntimePathConfig;
@RequiredArgsConstructor
public class UIDataTessdataController {
private static final Pattern INVALID_LANG_CHARS_PATTERN = Pattern.compile("[^A-Za-z0-9_+\\-]");
private final RuntimePathConfig runtimePathConfig;
private static volatile List<String> cachedRemoteTessdata = null;
private static volatile long cachedRemoteTessdataExpiry = 0L;
@ -88,7 +90,7 @@ public class UIDataTessdataController {
failed.add(language);
continue;
}
String safeLang = language.replaceAll("[^A-Za-z0-9_+\\-]", "");
String safeLang = INVALID_LANG_CHARS_PATTERN.matcher(language).replaceAll("");
if (!safeLang.equals(language)) {
failed.add(language);
continue;

View File

@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@ -33,6 +34,7 @@ public class TotpService {
private static final String HMAC_ALGORITHM = "HmacSHA1";
private static final String DEFAULT_ISSUER = "Stirling PDF";
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final Pattern TOTP_CODE_PATTERN = Pattern.compile("\\d{6}");
private final ApplicationProperties applicationProperties;
@ -71,7 +73,7 @@ public class TotpService {
}
String normalizedCode = code.replace(" ", "");
if (!normalizedCode.matches("\\d{6}")) {
if (!TOTP_CODE_PATTERN.matcher(normalizedCode).matches()) {
return null;
}

View File

@ -10,6 +10,7 @@ import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.springframework.stereotype.Service;
@ -32,6 +33,7 @@ import stirling.software.proprietary.model.api.signature.SavedSignatureResponse;
@Slf4j
public class SignatureService implements PersonalSignatureServiceInterface {
private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$");
private final String SIGNATURE_BASE_PATH;
private final String ALL_USERS_FOLDER = "ALL_USERS";
private final ObjectMapper objectMapper = new ObjectMapper();
@ -366,14 +368,14 @@ public class SignatureService implements PersonalSignatureServiceInterface {
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
throw new IllegalArgumentException("Invalid filename");
}
if (!fileName.matches("^[a-zA-Z0-9_.-]+$")) {
if (!FILENAME_VALIDATION_PATTERN.matcher(fileName).matches()) {
throw new IllegalArgumentException("Filename contains invalid characters");
}
}
private String validateAndNormalizeExtension(String extension) {
String normalized = extension.toLowerCase().trim();
if (normalized.equals("png") || normalized.equals("jpg") || normalized.equals("jpeg")) {
if ("png".equals(normalized) || "jpg".equals(normalized) || "jpeg".equals(normalized)) {
return normalized;
}
throw new IllegalArgumentException("Unsupported image extension: " + extension);

View File

@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
@ -16,6 +17,8 @@ import stirling.software.proprietary.security.util.Base32Codec;
class TotpServiceTest {
private static final Pattern PATTERN = Pattern.compile("[A-Z2-7]+");
private TotpService buildService(String appName) {
ApplicationProperties properties = new ApplicationProperties();
ApplicationProperties.Ui ui = new ApplicationProperties.Ui();
@ -32,7 +35,7 @@ class TotpServiceTest {
assertNotNull(secret);
assertEquals(32, secret.length());
assertTrue(secret.matches("[A-Z2-7]+"));
assertTrue(PATTERN.matcher(secret).matches());
}
@Test