mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00

# Description of Changes This pull request includes several changes primarily focused on improving configuration management, removing deprecated methods, and updating paths for external dependencies. The most important changes are summarized below: ### Configuration Management Improvements: * Added a new `RuntimePathConfig` class to manage dynamic paths for operations and pipeline configurations (`src/main/java/stirling/software/SPDF/config/RuntimePathConfig.java`). * Removed the `bookAndHtmlFormatsInstalled` bean and its associated logic from `AppConfig` and `EndpointConfiguration` (`src/main/java/stirling/software/SPDF/config/AppConfig.java`, `src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java`). [[1]](diffhunk://#diff-4d774ec79aa55750c0a4739bee971b68877078b73654e863fd40ee924347e143L130-L138) [[2]](diffhunk://#diff-750f31f6ecbd64b025567108a33775cad339e835a04360affff82a09410b697dL12-L35) [[3]](diffhunk://#diff-750f31f6ecbd64b025567108a33775cad339e835a04360affff82a09410b697dL275-L280) ### External Dependency Path Updates: * Updated paths for `weasyprint` and `unoconvert` in `ExternalAppDepConfig` to use values from `RuntimePathConfig` (`src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java`). [[1]](diffhunk://#diff-c47af298c07c2622aa98b038b78822c56bdb002de71081e102d344794e7832a6R12-L33) [[2]](diffhunk://#diff-c47af298c07c2622aa98b038b78822c56bdb002de71081e102d344794e7832a6L104-R115) ### Minor Adjustments: * Corrected a typo from "Unoconv" to "Unoconvert" in `EndpointConfiguration` (`src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java`). --- ## 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/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/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/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### 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/DeveloperGuide.md#6-testing) for more details.
625 lines
24 KiB
Java
625 lines
24 KiB
Java
package stirling.software.SPDF.utils;
|
|
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.net.*;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.*;
|
|
import java.nio.file.attribute.BasicFileAttributes;
|
|
import java.security.MessageDigest;
|
|
import java.util.ArrayDeque;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.Deque;
|
|
import java.util.Enumeration;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
import com.fathzer.soft.javaluator.DoubleEvaluator;
|
|
|
|
import io.github.pixee.security.HostValidator;
|
|
import io.github.pixee.security.Urls;
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
|
|
|
@Slf4j
|
|
public class GeneralUtils {
|
|
|
|
public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
|
File tempFile = Files.createTempFile("temp", null).toFile();
|
|
try (FileOutputStream os = new FileOutputStream(tempFile)) {
|
|
os.write(multipartFile.getBytes());
|
|
}
|
|
return tempFile;
|
|
}
|
|
|
|
public static void deleteDirectory(Path path) throws IOException {
|
|
Files.walkFileTree(
|
|
path,
|
|
new SimpleFileVisitor<Path>() {
|
|
@Override
|
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
|
throws IOException {
|
|
Files.deleteIfExists(file);
|
|
return FileVisitResult.CONTINUE;
|
|
}
|
|
|
|
@Override
|
|
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
|
|
throws IOException {
|
|
Files.deleteIfExists(dir);
|
|
return FileVisitResult.CONTINUE;
|
|
}
|
|
});
|
|
}
|
|
|
|
public static String convertToFileName(String name) {
|
|
String safeName = name.replaceAll("[^a-zA-Z0-9]", "_");
|
|
if (safeName.length() > 50) {
|
|
safeName = safeName.substring(0, 50);
|
|
}
|
|
return safeName;
|
|
}
|
|
|
|
public static boolean isValidURL(String urlStr) {
|
|
try {
|
|
Urls.create(
|
|
urlStr, Urls.HTTP_PROTOCOLS, HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS);
|
|
return true;
|
|
} catch (MalformedURLException e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static boolean isURLReachable(String urlStr) {
|
|
try {
|
|
// Parse the URL
|
|
URL url = URI.create(urlStr).toURL();
|
|
|
|
// Allow only http and https protocols
|
|
String protocol = url.getProtocol();
|
|
if (!"http".equals(protocol) && !"https".equals(protocol)) {
|
|
return false; // Disallow other protocols
|
|
}
|
|
|
|
// Check if the host is a local address
|
|
String host = url.getHost();
|
|
if (isLocalAddress(host)) {
|
|
return false; // Exclude local addresses
|
|
}
|
|
|
|
// Check if the URL is reachable
|
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
connection.setRequestMethod("HEAD");
|
|
// connection.setConnectTimeout(5000); // Set connection timeout
|
|
// connection.setReadTimeout(5000); // Set read timeout
|
|
int responseCode = connection.getResponseCode();
|
|
return (200 <= responseCode && responseCode <= 399);
|
|
} catch (Exception e) {
|
|
return false; // Return false in case of any exception
|
|
}
|
|
}
|
|
|
|
private static boolean isLocalAddress(String host) {
|
|
try {
|
|
// Resolve DNS to IP address
|
|
InetAddress address = InetAddress.getByName(host);
|
|
|
|
// Check for local addresses
|
|
return address.isAnyLocalAddress()
|
|
|| // Matches 0.0.0.0 or similar
|
|
address.isLoopbackAddress()
|
|
|| // Matches 127.0.0.1 or ::1
|
|
address.isSiteLocalAddress()
|
|
|| // Matches private IPv4 ranges: 192.168.x.x, 10.x.x.x, 172.16.x.x to
|
|
// 172.31.x.x
|
|
address.getHostAddress()
|
|
.startsWith("fe80:"); // Matches link-local IPv6 addresses
|
|
} catch (Exception e) {
|
|
return false; // Return false for invalid or unresolved addresses
|
|
}
|
|
}
|
|
|
|
public static File multipartToFile(MultipartFile multipart) throws IOException {
|
|
Path tempFile = Files.createTempFile("overlay-", ".pdf");
|
|
try (InputStream in = multipart.getInputStream();
|
|
FileOutputStream out = new FileOutputStream(tempFile.toFile())) {
|
|
byte[] buffer = new byte[1024];
|
|
int bytesRead;
|
|
while ((bytesRead = in.read(buffer)) != -1) {
|
|
out.write(buffer, 0, bytesRead);
|
|
}
|
|
}
|
|
return tempFile.toFile();
|
|
}
|
|
|
|
public static Long convertSizeToBytes(String sizeStr) {
|
|
if (sizeStr == null) {
|
|
return null;
|
|
}
|
|
|
|
sizeStr = sizeStr.trim().toUpperCase();
|
|
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
|
|
try {
|
|
if (sizeStr.endsWith("KB")) {
|
|
return (long)
|
|
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
|
|
} else if (sizeStr.endsWith("MB")) {
|
|
return (long)
|
|
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
|
* 1024
|
|
* 1024);
|
|
} else if (sizeStr.endsWith("GB")) {
|
|
return (long)
|
|
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
|
* 1024
|
|
* 1024
|
|
* 1024);
|
|
} else if (sizeStr.endsWith("B")) {
|
|
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
|
} else {
|
|
// Assume MB if no unit is specified
|
|
return (long) (Double.parseDouble(sizeStr) * 1024 * 1024);
|
|
}
|
|
} catch (NumberFormatException e) {
|
|
// The numeric part of the input string cannot be parsed, handle this case
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static List<Integer> parsePageList(String pages, int totalPages, boolean oneBased) {
|
|
if (pages == null) {
|
|
return List.of(1); // Default to first page if input is null
|
|
}
|
|
try {
|
|
return parsePageList(pages.split(","), totalPages, oneBased);
|
|
} catch (NumberFormatException e) {
|
|
return List.of(1); // Default to first page if input is invalid
|
|
}
|
|
}
|
|
|
|
public static List<Integer> parsePageList(String[] pages, int totalPages) {
|
|
return parsePageList(pages, totalPages, false);
|
|
}
|
|
|
|
public static List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
|
|
List<Integer> result = new ArrayList<>();
|
|
int offset = oneBased ? 1 : 0;
|
|
for (String page : pages) {
|
|
if ("all".equalsIgnoreCase(page)) {
|
|
|
|
for (int i = 0; i < totalPages; i++) {
|
|
result.add(i + offset);
|
|
}
|
|
} else if (page.contains(",")) {
|
|
// Split the string into parts, could be single pages or ranges
|
|
String[] parts = page.split(",");
|
|
for (String part : parts) {
|
|
result.addAll(handlePart(part, totalPages, offset));
|
|
}
|
|
} else {
|
|
result.addAll(handlePart(page, totalPages, offset));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public static List<Integer> evaluateNFunc(String expression, int maxValue) {
|
|
List<Integer> results = new ArrayList<>();
|
|
DoubleEvaluator evaluator = new DoubleEvaluator();
|
|
|
|
// Validate the expression
|
|
if (!expression.matches("[0-9n+\\-*/() ]+")) {
|
|
throw new IllegalArgumentException("Invalid expression");
|
|
}
|
|
|
|
for (int n = 1; n <= maxValue; n++) {
|
|
// Replace 'n' with the current value of n, correctly handling numbers before
|
|
// 'n'
|
|
String sanitizedExpression = sanitizeNFunction(expression, n);
|
|
Double result = evaluator.evaluate(sanitizedExpression);
|
|
|
|
// Check if the result is null or not within bounds
|
|
if (result == null) break;
|
|
|
|
if (result.intValue() > 0 && result.intValue() <= maxValue)
|
|
results.add(result.intValue());
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static String sanitizeNFunction(String expression, int nValue) {
|
|
String sanitizedExpression = expression.replace(" ", "");
|
|
String multiplyByOpeningRoundBracketPattern =
|
|
"([0-9n)])\\("; // example: n(n-1), 9(n-1), (n-1)(n-2)
|
|
sanitizedExpression =
|
|
sanitizedExpression.replaceAll(multiplyByOpeningRoundBracketPattern, "$1*(");
|
|
|
|
String multiplyByClosingRoundBracketPattern =
|
|
"\\)([0-9n)])"; // example: (n-1)n, (n-1)9, (n-1)(n-2)
|
|
sanitizedExpression =
|
|
sanitizedExpression.replaceAll(multiplyByClosingRoundBracketPattern, ")*$1");
|
|
|
|
sanitizedExpression = insertMultiplicationBeforeN(sanitizedExpression, nValue);
|
|
return sanitizedExpression;
|
|
}
|
|
|
|
private static String insertMultiplicationBeforeN(String expression, int nValue) {
|
|
// Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n")
|
|
String withMultiplication = expression.replaceAll("(\\d)n", "$1*n");
|
|
withMultiplication = formatConsecutiveNsForNFunction(withMultiplication);
|
|
// Now replace 'n' with its current value
|
|
return withMultiplication.replace("n", String.valueOf(nValue));
|
|
}
|
|
|
|
private static String formatConsecutiveNsForNFunction(String expression) {
|
|
String text = expression;
|
|
while (text.matches(".*n{2,}.*")) {
|
|
text = text.replaceAll("(?<!n)n{2}", "n*n");
|
|
}
|
|
return text;
|
|
}
|
|
|
|
private static List<Integer> handlePart(String part, int totalPages, int offset) {
|
|
List<Integer> partResult = new ArrayList<>();
|
|
|
|
// First check for n-syntax because it should not be processed as a range
|
|
if (part.contains("n")) {
|
|
partResult = evaluateNFunc(part, totalPages);
|
|
// Adjust the results according to the offset
|
|
for (int i = 0; i < partResult.size(); i++) {
|
|
int adjustedValue = partResult.get(i) - 1 + offset;
|
|
partResult.set(i, adjustedValue);
|
|
}
|
|
} else if (part.contains("-")) {
|
|
// Process ranges only if it's not n-syntax
|
|
String[] rangeParts = part.split("-");
|
|
try {
|
|
int start = Integer.parseInt(rangeParts[0]);
|
|
int end =
|
|
(rangeParts.length > 1 && !rangeParts[1].isEmpty())
|
|
? Integer.parseInt(rangeParts[1])
|
|
: totalPages;
|
|
for (int i = start; i <= end; i++) {
|
|
if (i >= 1 && i <= totalPages) {
|
|
partResult.add(i - 1 + offset);
|
|
}
|
|
}
|
|
} catch (NumberFormatException e) {
|
|
// Range is invalid, ignore this part
|
|
}
|
|
} else {
|
|
// This is a single page number
|
|
try {
|
|
int pageNum = Integer.parseInt(part.trim());
|
|
if (pageNum >= 1 && pageNum <= totalPages) {
|
|
partResult.add(pageNum - 1 + offset);
|
|
}
|
|
} catch (NumberFormatException ignored) {
|
|
// Ignore invalid numbers
|
|
}
|
|
}
|
|
return partResult;
|
|
}
|
|
|
|
public static boolean createDir(String path) {
|
|
Path folder = Paths.get(path);
|
|
if (!Files.exists(folder)) {
|
|
try {
|
|
Files.createDirectories(folder);
|
|
} catch (IOException e) {
|
|
log.error("exception", e);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static boolean isValidUUID(String uuid) {
|
|
if (uuid == null) {
|
|
return false;
|
|
}
|
|
try {
|
|
UUID.fromString(uuid);
|
|
return true;
|
|
} catch (IllegalArgumentException e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static void saveKeyToConfig(String id, String key) throws IOException {
|
|
saveKeyToConfig(id, key, true);
|
|
}
|
|
|
|
public static void saveKeyToConfig(String id, boolean key) throws IOException {
|
|
saveKeyToConfig(id, key, true);
|
|
}
|
|
|
|
public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
|
|
throws IOException {
|
|
doSaveKeyToConfig(id, (key == null ? "" : key), autoGenerated);
|
|
}
|
|
|
|
public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated)
|
|
throws IOException {
|
|
doSaveKeyToConfig(id, String.valueOf(key), autoGenerated);
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*
|
|
* Internal Implementation Details *
|
|
*------------------------------------------------------------------------*/
|
|
|
|
/**
|
|
* Actually performs the line-based update for the given path (e.g. "security.csrfDisabled") to
|
|
* a new string value (e.g. "true"), possibly marking it as auto-generated.
|
|
*/
|
|
private static void doSaveKeyToConfig(String fullPath, String newValue, boolean autoGenerated)
|
|
throws IOException {
|
|
// 1) Load the file (settings.yml)
|
|
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
|
if (!Files.exists(settingsPath)) {
|
|
log.warn("Settings file not found at {}, creating a new empty file...", settingsPath);
|
|
Files.createDirectories(settingsPath.getParent());
|
|
Files.createFile(settingsPath);
|
|
}
|
|
List<String> lines = Files.readAllLines(settingsPath);
|
|
|
|
// 2) Build a map of "nestedKeyPath -> lineIndex" by parsing indentation
|
|
// Also track each line's indentation so we can preserve it when rewriting.
|
|
Map<String, LineInfo> pathToLine = parseNestedYamlKeys(lines);
|
|
|
|
// 3) If the path is found, rewrite its line. Else, append at the bottom (no indentation).
|
|
boolean changed = false;
|
|
if (pathToLine.containsKey(fullPath)) {
|
|
// Rewrite existing line
|
|
LineInfo info = pathToLine.get(fullPath);
|
|
String oldLine = lines.get(info.lineIndex);
|
|
String newLine =
|
|
rewriteLine(oldLine, info.indentSpaces, fullPath, newValue, autoGenerated);
|
|
if (!newLine.equals(oldLine)) {
|
|
lines.set(info.lineIndex, newLine);
|
|
changed = true;
|
|
}
|
|
} else {
|
|
// Append a new line at the bottom, with zero indentation
|
|
String appended = fullPath + ": " + newValue;
|
|
if (autoGenerated) {
|
|
appended += " # Automatically Generated Settings (Do Not Edit Directly)";
|
|
}
|
|
lines.add(appended);
|
|
changed = true;
|
|
}
|
|
|
|
// 4) If changed, write back to file
|
|
if (changed) {
|
|
Files.write(settingsPath, lines);
|
|
log.info(
|
|
"Updated '{}' to '{}' (autoGenerated={}) in {}",
|
|
fullPath,
|
|
newValue,
|
|
autoGenerated,
|
|
settingsPath);
|
|
} else {
|
|
log.info("No changes for '{}' (already set to '{}').", fullPath, newValue);
|
|
}
|
|
}
|
|
|
|
/** A small record-like class that holds: - lineIndex - indentSpaces */
|
|
private static class LineInfo {
|
|
int lineIndex;
|
|
int indentSpaces;
|
|
|
|
public LineInfo(int lineIndex, int indentSpaces) {
|
|
this.lineIndex = lineIndex;
|
|
this.indentSpaces = indentSpaces;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse the YAML lines to build a map: "full.nested.key" -> (lineIndex, indentSpaces). We do a
|
|
* naive indentation-based path stacking: - 2 spaces = 1 indent level - lines that start with
|
|
* fewer or equal indentation pop the stack - lines that look like "key:" or "key: value" cause
|
|
* a push
|
|
*/
|
|
private static Map<String, LineInfo> parseNestedYamlKeys(List<String> lines) {
|
|
Map<String, LineInfo> result = new HashMap<>();
|
|
|
|
// We'll maintain a stack of (keyName, indentLevel).
|
|
// Each line that looks like "myKey:" or "myKey: value" is a new "child" of the top of the
|
|
// stack if indent is deeper.
|
|
Deque<String> pathStack = new ArrayDeque<>();
|
|
Deque<Integer> indentStack = new ArrayDeque<>();
|
|
indentStack.push(-1); // sentinel
|
|
|
|
for (int i = 0; i < lines.size(); i++) {
|
|
String line = lines.get(i);
|
|
String trimmed = line.trim();
|
|
|
|
// skip blank lines, comment lines, or list items
|
|
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) {
|
|
continue;
|
|
}
|
|
// check if there's a colon
|
|
int colonIdx = trimmed.indexOf(':');
|
|
if (colonIdx <= 0) { // must have at least one char before ':'
|
|
continue;
|
|
}
|
|
// parse out key
|
|
String keyPart = trimmed.substring(0, colonIdx).trim();
|
|
if (keyPart.isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
// count leading spaces for indentation
|
|
int leadingSpaces = countLeadingSpaces(line);
|
|
int indentLevel = leadingSpaces / 2; // assume 2 spaces per level
|
|
|
|
// pop from stack until we get to a shallower indentation
|
|
while (indentStack.peek() != null && indentStack.peek() >= indentLevel) {
|
|
indentStack.pop();
|
|
pathStack.pop();
|
|
}
|
|
|
|
// push the new key
|
|
pathStack.push(keyPart);
|
|
indentStack.push(indentLevel);
|
|
|
|
// build the full path
|
|
String[] arr = pathStack.toArray(new String[0]);
|
|
List<String> reversed = Arrays.asList(arr);
|
|
Collections.reverse(reversed);
|
|
String fullPath = String.join(".", reversed);
|
|
|
|
// store line info
|
|
result.put(fullPath, new LineInfo(i, leadingSpaces));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Rewrite a single line to set a new value, preserving indentation and (optionally) the
|
|
* existing or auto-generated inline comment.
|
|
*
|
|
* <p>For example, oldLine might be: " csrfDisabled: false # set to 'true' to disable CSRF
|
|
* protection" newValue = "true" autoGenerated = false
|
|
*
|
|
* <p>We'll produce something like: " csrfDisabled: true # set to 'true' to disable CSRF
|
|
* protection"
|
|
*/
|
|
private static String rewriteLine(
|
|
String oldLine, int indentSpaces, String path, String newValue, boolean autoGenerated) {
|
|
// We'll keep the exact leading indentation (indentSpaces).
|
|
// Then "key: newValue". We'll try to preserve any existing inline comment unless
|
|
// autoGenerated is true.
|
|
|
|
// 1) Extract leading spaces from the old line (just in case they differ from indentSpaces).
|
|
int actualLeadingSpaces = countLeadingSpaces(oldLine);
|
|
String leading = oldLine.substring(0, actualLeadingSpaces);
|
|
|
|
// 2) Remove leading spaces from the rest
|
|
String trimmed = oldLine.substring(actualLeadingSpaces);
|
|
|
|
// 3) Check for existing comment
|
|
int hashIndex = trimmed.indexOf('#');
|
|
String lineWithoutComment =
|
|
(hashIndex >= 0) ? trimmed.substring(0, hashIndex).trim() : trimmed.trim();
|
|
String oldComment = (hashIndex >= 0) ? trimmed.substring(hashIndex).trim() : "";
|
|
|
|
// 4) Rebuild "key: newValue"
|
|
// The "key" here is everything before ':' in lineWithoutComment
|
|
int colonIdx = lineWithoutComment.indexOf(':');
|
|
String existingKey =
|
|
(colonIdx >= 0)
|
|
? lineWithoutComment.substring(0, colonIdx).trim()
|
|
: path; // fallback if line is malformed
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append(leading); // restore original leading spaces
|
|
|
|
// "key: newValue"
|
|
sb.append(existingKey).append(": ").append(newValue);
|
|
|
|
// 5) If autoGenerated, add/replace comment
|
|
if (autoGenerated) {
|
|
sb.append(" # Automatically Generated Settings (Do Not Edit Directly)");
|
|
} else {
|
|
// preserve the old comment if it exists
|
|
if (!oldComment.isEmpty()) {
|
|
sb.append(" ").append(oldComment);
|
|
}
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
private static int countLeadingSpaces(String line) {
|
|
int count = 0;
|
|
for (char c : line.toCharArray()) {
|
|
if (c == ' ') count++;
|
|
else break;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
public static String generateMachineFingerprint() {
|
|
try {
|
|
// Get the MAC address
|
|
StringBuilder sb = new StringBuilder();
|
|
InetAddress ip = InetAddress.getLocalHost();
|
|
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
|
|
|
|
if (network == null) {
|
|
Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces();
|
|
while (networks.hasMoreElements()) {
|
|
NetworkInterface net = networks.nextElement();
|
|
byte[] mac = net.getHardwareAddress();
|
|
if (mac != null) {
|
|
for (int i = 0; i < mac.length; i++) {
|
|
sb.append(String.format("%02X", mac[i]));
|
|
}
|
|
break; // Use the first network interface with a MAC address
|
|
}
|
|
}
|
|
} else {
|
|
byte[] mac = network.getHardwareAddress();
|
|
if (mac != null) {
|
|
for (int i = 0; i < mac.length; i++) {
|
|
sb.append(String.format("%02X", mac[i]));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hash the MAC address for privacy and consistency
|
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
byte[] hash = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
|
|
StringBuilder fingerprint = new StringBuilder();
|
|
for (byte b : hash) {
|
|
fingerprint.append(String.format("%02x", b));
|
|
}
|
|
return fingerprint.toString();
|
|
} catch (Exception e) {
|
|
return "GenericID";
|
|
}
|
|
}
|
|
|
|
public static boolean isVersionHigher(String currentVersion, String compareVersion) {
|
|
if (currentVersion == null || compareVersion == null) {
|
|
return false;
|
|
}
|
|
|
|
// Split versions into components
|
|
String[] current = currentVersion.split("\\.");
|
|
String[] compare = compareVersion.split("\\.");
|
|
|
|
// Get the length of the shorter version array
|
|
int length = Math.min(current.length, compare.length);
|
|
|
|
// Compare each component
|
|
for (int i = 0; i < length; i++) {
|
|
int currentPart = Integer.parseInt(current[i]);
|
|
int comparePart = Integer.parseInt(compare[i]);
|
|
|
|
if (currentPart > comparePart) {
|
|
return true;
|
|
}
|
|
if (currentPart < comparePart) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If all components so far are equal, the longer version is considered higher
|
|
return current.length > compare.length;
|
|
}
|
|
}
|