mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 11:37:31 +08:00
fix(wiki): match glob against symlink-resolved scan root
The base directory is canonicalized via toRealPath before walking, so the
walked files carry the symlink-resolved prefix. The PathMatcher was built
from the literal pattern, so a symlinked base never matched and files were
silently dropped. Rebuild the glob against the resolved scan root, escaping
glob metacharacters in the base so a real directory name containing */?/{}/[]
is treated literally.
This commit is contained in:
parent
7f4d3e3ab9
commit
37e9afa9de
@ -226,8 +226,17 @@ public class WikiDirectoryScanService {
|
|||||||
errors.add("Base directory does not exist: " + scanRoot);
|
errors.add("Base directory does not exist: " + scanRoot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// validateDirectory canonicalizes via toRealPath, so scanRoot is the
|
||||||
|
// symlink-resolved real path and walkFileTree yields real-path-prefixed
|
||||||
|
// files. The matcher must use that resolved base, not the literal pattern
|
||||||
|
// prefix — otherwise a symlinked base never matches. Rebuild the pattern
|
||||||
|
// by swapping the literal base for the resolved scanRoot, keeping the
|
||||||
|
// wildcard tail; escape glob metacharacters in the base so a real
|
||||||
|
// directory name containing */?/{}/[] is treated literally.
|
||||||
|
String wildcardTail = pattern.substring(basePath.length());
|
||||||
|
String effectivePattern = globEscape(scanRoot.toString()) + wildcardTail;
|
||||||
try {
|
try {
|
||||||
matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
|
matcher = FileSystems.getDefault().getPathMatcher("glob:" + effectivePattern);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
errors.add("Invalid glob pattern '" + pattern + "': " + e.getMessage());
|
errors.add("Invalid glob pattern '" + pattern + "': " + e.getMessage());
|
||||||
return;
|
return;
|
||||||
@ -302,6 +311,19 @@ public class WikiDirectoryScanService {
|
|||||||
return s.contains("*") || s.contains("?") || s.contains("{") || s.contains("[");
|
return s.contains("*") || s.contains("?") || s.contains("{") || s.contains("[");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Escape glob metacharacters so a literal path segment is matched verbatim. */
|
||||||
|
private static String globEscape(String s) {
|
||||||
|
StringBuilder b = new StringBuilder(s.length());
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if ("\\*?{}[]".indexOf(c) >= 0) {
|
||||||
|
b.append('\\');
|
||||||
|
}
|
||||||
|
b.append(c);
|
||||||
|
}
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private static String getExtension(String fileName) {
|
private static String getExtension(String fileName) {
|
||||||
int dot = fileName.lastIndexOf('.');
|
int dot = fileName.lastIndexOf('.');
|
||||||
return dot > 0 ? fileName.substring(dot + 1).toLowerCase() : "";
|
return dot > 0 ? fileName.substring(dot + 1).toLowerCase() : "";
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
package vip.mate.wiki.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies glob matching works when the pattern's base directory is reached
|
||||||
|
* through a symbolic link. {@code WikiSourcePathValidator} canonicalizes the
|
||||||
|
* base with {@code toRealPath()}, so the walked files carry the symlink-resolved
|
||||||
|
* prefix; the matcher must be built against that resolved scan root rather than
|
||||||
|
* the literal pattern prefix, otherwise nothing matches and files are silently
|
||||||
|
* dropped.
|
||||||
|
*/
|
||||||
|
@SpringBootTest(
|
||||||
|
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||||
|
properties = {
|
||||||
|
"spring.flyway.enabled=true",
|
||||||
|
"spring.flyway.locations=classpath:db/migration/h2",
|
||||||
|
"mateclaw.feature-flag.refresh-ms=999999",
|
||||||
|
"mate.wiki.auto-process-on-upload=false"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
class WikiGlobSymlinkBaseE2ETest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WikiDirectoryScanService scanService;
|
||||||
|
|
||||||
|
private static final java.util.concurrent.atomic.AtomicLong SEQ =
|
||||||
|
new java.util.concurrent.atomic.AtomicLong(System.nanoTime());
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void globWithSymlinkBase_singleLevel_matches(@TempDir Path base) throws IOException {
|
||||||
|
Path realDir = Files.createDirectories(base.resolve("real"));
|
||||||
|
Files.writeString(realDir.resolve("a.txt"), "alpha");
|
||||||
|
Files.write(realDir.resolve("b.pdf"), "PDF-bytes".getBytes());
|
||||||
|
|
||||||
|
Path linkDir = base.resolve("link");
|
||||||
|
try {
|
||||||
|
Files.createSymbolicLink(linkDir, realDir);
|
||||||
|
} catch (UnsupportedOperationException | IOException e) {
|
||||||
|
return; // filesystem without symlink support — skip
|
||||||
|
}
|
||||||
|
|
||||||
|
WikiDirectoryScanService.ScanResult result =
|
||||||
|
scanService.scanDirectory(SEQ.incrementAndGet(), linkDir + "/*.txt");
|
||||||
|
|
||||||
|
// Only a.txt matches; the .pdf is excluded by the explicit *.txt pattern.
|
||||||
|
// Before the fix the symlink-resolved file prefix never matched the literal
|
||||||
|
// pattern prefix, so added would be 0.
|
||||||
|
assertEquals(1, result.added(), "glob through a symlinked base must match the .txt file");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void globWithSymlinkBase_recursive_matchesSubdir(@TempDir Path base) throws IOException {
|
||||||
|
Path realDir = Files.createDirectories(base.resolve("real"));
|
||||||
|
Files.createDirectories(realDir.resolve("sub"));
|
||||||
|
Files.writeString(realDir.resolve("sub/c.txt"), "charlie");
|
||||||
|
|
||||||
|
Path linkDir = base.resolve("link");
|
||||||
|
try {
|
||||||
|
Files.createSymbolicLink(linkDir, realDir);
|
||||||
|
} catch (UnsupportedOperationException | IOException e) {
|
||||||
|
return; // filesystem without symlink support — skip
|
||||||
|
}
|
||||||
|
|
||||||
|
WikiDirectoryScanService.ScanResult result =
|
||||||
|
scanService.scanDirectory(SEQ.incrementAndGet(), linkDir + "/**/*.txt");
|
||||||
|
|
||||||
|
assertEquals(1, result.added(), "recursive glob through a symlinked base must match the nested .txt file");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user