-
-
Notifications
You must be signed in to change notification settings - Fork 46
#207 Suppress CLI creation #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,17 +52,14 @@ public static void writeReportToDisk( | |
| final String reportOutputDirectory, final String filename, final String string) { | ||
| Path outputDirectory = Path.of(reportOutputDirectory).toAbsolutePath().normalize(); | ||
| Path reportName = validateFilename(filename); | ||
| List<DirectoryStream<Path>> openedDirectories = new ArrayList<>(); | ||
|
|
||
| try { | ||
| SecureDirectoryStream<Path> outputStream = openSecureDirectoryPath(outputDirectory, openedDirectories); | ||
| writeAtomically(outputStream, reportName, string); | ||
| SecureDirectoryOps ops = SecureDirectoryOps.create(outputDirectory); | ||
| ops.writeAtomically(reportName, string); | ||
| log.info("Done! View the report at {}", outputDirectory.resolve(reportName)); | ||
| } catch (IOException | UnsupportedOperationException e) { | ||
| log.error("Unable to write report {}", outputDirectory.resolve(reportName), e); | ||
| throw new ReportWriteException("Unable to write report " + outputDirectory.resolve(reportName), e); | ||
| } finally { | ||
| closeDirectories(openedDirectories); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -80,73 +77,162 @@ private static Path validateFilename(String filename) { | |
| return reportName; | ||
| } | ||
|
|
||
| private static SecureDirectoryStream<Path> openSecureDirectoryPath( | ||
| Path outputDirectory, List<DirectoryStream<Path>> openedDirectories) throws IOException { | ||
| Path root = outputDirectory.getRoot(); | ||
| if (root == null) { | ||
| throw new IOException("Report output directory has no filesystem root: " + outputDirectory); | ||
| private interface SecureDirectoryOps { | ||
| static SecureDirectoryOps create(Path outputDirectory) throws IOException { | ||
| // Try to use secure directory streams (Unix-like) | ||
| Path current = outputDirectory; | ||
| while (current != null) { | ||
| if (Files.exists(current, NOFOLLOW_LINKS) && !Files.isSymbolicLink(current)) { | ||
| try (DirectoryStream<Path> stream = Files.newDirectoryStream(current)) { | ||
| if (stream instanceof SecureDirectoryStream<?>) { | ||
| return new SecureDirectoryOpsImpl(current, outputDirectory); | ||
| } | ||
| } catch (IOException | UnsupportedOperationException ignored) { | ||
| } | ||
| } | ||
| current = current.getParent(); | ||
| } | ||
| // Fallback for Windows | ||
| return new FallbackDirectoryOps(outputDirectory); | ||
| } | ||
|
|
||
| DirectoryStream<Path> rootStream = Files.newDirectoryStream(root); | ||
| openedDirectories.add(rootStream); | ||
| SecureDirectoryStream<Path> current = asSecureDirectoryStream(rootStream, root); | ||
| Path currentPath = root; | ||
| void writeAtomically(Path reportName, String content) throws IOException; | ||
| } | ||
|
|
||
| private static final class SecureDirectoryOpsImpl implements SecureDirectoryOps { | ||
| private final Path startPath; | ||
| private final Path outputDirectory; | ||
| private final List<DirectoryStream<Path>> openedDirectories = new ArrayList<>(); | ||
|
|
||
| SecureDirectoryOpsImpl(Path startPath, Path outputDirectory) { | ||
| this.startPath = startPath; | ||
| this.outputDirectory = outputDirectory; | ||
| } | ||
|
|
||
| for (Path component : root.relativize(outputDirectory)) { | ||
| SecureDirectoryStream<Path> child; | ||
| @Override | ||
| public void writeAtomically(Path reportName, String content) throws IOException { | ||
| try { | ||
| child = current.newDirectoryStream(component, NOFOLLOW_LINKS); | ||
| } catch (NoSuchFileException e) { | ||
| Path directoryToCreate = currentPath.resolve(component); | ||
| Files.createDirectory(directoryToCreate); | ||
| child = current.newDirectoryStream(component, NOFOLLOW_LINKS); | ||
| SecureDirectoryStream<Path> current = openSecurePath(); | ||
| writeAtomicallySecure(current, reportName, content); | ||
| } finally { | ||
| closeDirectories(openedDirectories); | ||
| } | ||
| openedDirectories.add(child); | ||
| current = child; | ||
| currentPath = currentPath.resolve(component); | ||
| } | ||
| return current; | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| private static SecureDirectoryStream<Path> asSecureDirectoryStream(DirectoryStream<Path> stream, Path directory) { | ||
| if (!(stream instanceof SecureDirectoryStream<?>)) { | ||
| throw new UnsupportedOperationException( | ||
| "Secure directory operations are unavailable for report output: " + directory); | ||
| private SecureDirectoryStream<Path> openSecurePath() throws IOException { | ||
| DirectoryStream<Path> startStream = Files.newDirectoryStream(startPath); | ||
| openedDirectories.add(startStream); | ||
| SecureDirectoryStream<Path> current = asSecureDirectoryStream(startStream, startPath); | ||
| Path currentPath = startPath; | ||
|
|
||
| for (Path component : startPath.relativize(outputDirectory)) { | ||
| if (component.toString().isEmpty()) { | ||
| continue; | ||
| } | ||
| SecureDirectoryStream<Path> child; | ||
| try { | ||
| child = current.newDirectoryStream(component, NOFOLLOW_LINKS); | ||
| } catch (NoSuchFileException e) { | ||
| Path directoryToCreate = currentPath.resolve(component); | ||
| Files.createDirectory(directoryToCreate); | ||
| child = current.newDirectoryStream(component, NOFOLLOW_LINKS); | ||
| } | ||
| openedDirectories.add(child); | ||
| current = child; | ||
| currentPath = currentPath.resolve(component); | ||
| } | ||
| return current; | ||
| } | ||
| return (SecureDirectoryStream<Path>) stream; | ||
| } | ||
|
|
||
| private static void writeAtomically(SecureDirectoryStream<Path> directory, Path reportName, String content) | ||
| throws IOException { | ||
| BasicFileAttributeView targetView = | ||
| directory.getFileAttributeView(reportName, BasicFileAttributeView.class, NOFOLLOW_LINKS); | ||
| try { | ||
| BasicFileAttributes attributes = targetView.readAttributes(); | ||
| if (attributes.isSymbolicLink() || attributes.isDirectory()) { | ||
| throw new IOException("Refusing to replace non-regular report path: " + reportName); | ||
| private void writeAtomicallySecure(SecureDirectoryStream<Path> directory, Path reportName, String content) | ||
| throws IOException { | ||
| BasicFileAttributeView targetView = | ||
| directory.getFileAttributeView(reportName, BasicFileAttributeView.class, NOFOLLOW_LINKS); | ||
| try { | ||
| BasicFileAttributes attributes = targetView.readAttributes(); | ||
| if (attributes.isSymbolicLink() || attributes.isDirectory()) { | ||
| throw new IOException("Refusing to replace non-regular report path: " + reportName); | ||
| } | ||
| } catch (NoSuchFileException ignored) { | ||
| } | ||
|
|
||
| Path temporaryName = Path.of("." + reportName + "." + UUID.randomUUID() + ".tmp"); | ||
| Set<OpenOption> options = Set.of(CREATE_NEW, WRITE, NOFOLLOW_LINKS); | ||
| boolean moved = false; | ||
| try { | ||
| try (SeekableByteChannel channel = directory.newByteChannel(temporaryName, options); | ||
| BufferedWriter writer = new BufferedWriter( | ||
| new OutputStreamWriter(Channels.newOutputStream(channel), Charset.defaultCharset()))) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Description: Check the charset declared by report templates and any explicit charset use.
rg -n --glob '*.java' -C2 'charset|Charset|<meta' report/src/main/java | head -n 80Repository: refactorfirst/RefactorFirst Length of output: 3330 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- ReportWriter.java ---'
sed -n '1,240p' report/src/main/java/org/hjug/refactorfirst/report/ReportWriter.java
printf '%s\n' '--- charset declarations and report renderers ---'
rg -n -C3 'charset|Content-Type|CSV|csv|StandardCharsets|defaultCharset' report/src/main/java/org/hjug/refactorfirst/reportRepository: refactorfirst/RefactorFirst Length of output: 15967 🤖 get_repo_knowledge executed:
Length of output: 1333 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- report writer callers ---'
rg -n -C4 'writeReportToDisk|new HtmlReport|new CsvReport|<html|<head|meta' report/src/main/java report/src/test || true
printf '%s\n' '--- Java/runtime configuration ---'
rg -n -C3 '<maven.compiler|source>|target>|release>|java.version|<properties>' pom.xml */pom.xml
printf '%s\n' '--- report renderer declarations ---'
rg -n -C8 'StringBuilder|append\\(\"<|append\\('\"'\"'<|writeReportToDisk|REPORT|HTML' report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java report/src/main/java/org/hjug/refactorfirst/report/CsvReport.javaRepository: refactorfirst/RefactorFirst Length of output: 50383 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- generated HTML header ---'
sed -n '30,105p' report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java
printf '%s\n' '--- Java version configuration ---'
for f in pom.xml */pom.xml; do
if [ -f "$f" ]; then
echo "--- $f ---"
rg -n -C3 '<maven.compiler|<source>|<target>|<release>|<java.version>|<properties>' "$f" || true
fi
done
printf '%s\n' '--- report output calls ---'
rg -n 'writeReportToDisk' report/src/main/java/org/hjug/refactorfirst/reportRepository: refactorfirst/RefactorFirst Length of output: 6517 Use an explicit UTF-8 charset for report output. Both write methods use 🔧 Proposed fix-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
...
- new OutputStreamWriter(Channels.newOutputStream(channel), Charset.defaultCharset()))) {
+ new OutputStreamWriter(Channels.newOutputStream(channel), StandardCharsets.UTF_8))) {Apply the same change in 🤖 Prompt for AI Agents |
||
| writer.write(content); | ||
| } | ||
| directory.move(temporaryName, directory, reportName); | ||
| moved = true; | ||
| } finally { | ||
| if (!moved) { | ||
| try { | ||
| directory.deleteFile(temporaryName); | ||
| } catch (NoSuchFileException ignored) { | ||
| } | ||
| } | ||
| } | ||
| } catch (NoSuchFileException ignored) { | ||
| // The normal first-write case. | ||
| } | ||
|
|
||
| Path temporaryName = Path.of("." + reportName + "." + UUID.randomUUID() + ".tmp"); | ||
| Set<OpenOption> options = Set.of(CREATE_NEW, WRITE, NOFOLLOW_LINKS); | ||
| boolean moved = false; | ||
| try { | ||
| try (SeekableByteChannel channel = directory.newByteChannel(temporaryName, options); | ||
| BufferedWriter writer = new BufferedWriter( | ||
| new OutputStreamWriter(Channels.newOutputStream(channel), Charset.defaultCharset()))) { | ||
| writer.write(content); | ||
| @SuppressWarnings("unchecked") | ||
| private static SecureDirectoryStream<Path> asSecureDirectoryStream( | ||
| DirectoryStream<Path> stream, Path directory) { | ||
| if (!(stream instanceof SecureDirectoryStream<?>)) { | ||
| throw new UnsupportedOperationException( | ||
| "Secure directory operations are unavailable for report output: " + directory); | ||
| } | ||
| directory.move(temporaryName, directory, reportName); | ||
| moved = true; | ||
| } finally { | ||
| if (!moved) { | ||
| try { | ||
| directory.deleteFile(temporaryName); | ||
| } catch (NoSuchFileException ignored) { | ||
| // Nothing to clean up. | ||
| return (SecureDirectoryStream<Path>) stream; | ||
| } | ||
| } | ||
|
|
||
| private static final class FallbackDirectoryOps implements SecureDirectoryOps { | ||
| private final Path outputDirectory; | ||
|
|
||
| FallbackDirectoryOps(Path outputDirectory) { | ||
| this.outputDirectory = outputDirectory; | ||
| } | ||
|
|
||
| @Override | ||
| public void writeAtomically(Path reportName, String content) throws IOException { | ||
| Path targetFile = outputDirectory.resolve(reportName); | ||
| rejectExistingSymbolicLinkComponents(targetFile); | ||
|
|
||
| // Ensure parent directories exist | ||
| Files.createDirectories(outputDirectory); | ||
|
|
||
| // Check if target exists and is a symlink or directory | ||
| if (Files.exists(targetFile, NOFOLLOW_LINKS)) { | ||
| BasicFileAttributes attrs = Files.readAttributes(targetFile, BasicFileAttributes.class, NOFOLLOW_LINKS); | ||
| if (attrs.isSymbolicLink() || attrs.isDirectory()) { | ||
| throw new IOException("Refusing to replace non-regular report path: " + reportName); | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Write to temporary file then atomically move | ||
| Path temporaryName = outputDirectory.resolve("." + reportName + "." + UUID.randomUUID() + ".tmp"); | ||
| Set<OpenOption> options = Set.of(CREATE_NEW, WRITE); | ||
| boolean moved = false; | ||
| try { | ||
| try (SeekableByteChannel channel = Files.newByteChannel(temporaryName, options); | ||
| BufferedWriter writer = new BufferedWriter( | ||
| new OutputStreamWriter(Channels.newOutputStream(channel), Charset.defaultCharset()))) { | ||
| writer.write(content); | ||
| } | ||
| // Verify temp file is not a symlink before move | ||
| if (Files.isSymbolicLink(temporaryName)) { | ||
| throw new IOException("Temporary file is a symbolic link"); | ||
| } | ||
| Files.move(temporaryName, targetFile, java.nio.file.StandardCopyOption.ATOMIC_MOVE); | ||
| moved = true; | ||
| } finally { | ||
| if (!moved) { | ||
| try { | ||
| Files.deleteIfExists(temporaryName); | ||
| } catch (IOException ignored) { | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.