mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +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);
|
||||
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 {
|
||||
matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
|
||||
matcher = FileSystems.getDefault().getPathMatcher("glob:" + effectivePattern);
|
||||
} catch (IllegalArgumentException e) {
|
||||
errors.add("Invalid glob pattern '" + pattern + "': " + e.getMessage());
|
||||
return;
|
||||
@ -302,6 +311,19 @@ public class WikiDirectoryScanService {
|
||||
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) {
|
||||
int dot = fileName.lastIndexOf('.');
|
||||
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