diff --git a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleEntry.java b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleEntry.java index d4ff5e3..34c9685 100644 --- a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleEntry.java +++ b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleEntry.java @@ -15,5 +15,6 @@ public record BundleEntry( String name ) { public static short ATTR_REASSEMBLE = 1; + public static short ATTR_SYMLINK = 2; public static short MAGIC = (short) 0xA2E3; } diff --git a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleReader.java b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleReader.java index 534037e..21cc63c 100644 --- a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleReader.java +++ b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleReader.java @@ -16,21 +16,24 @@ import java.util.HashMap; public class BundleReader { protected final long maxTime; protected final long minTime; + protected final boolean linkSymbol; private final byte[] buffer = new byte[4096]; protected final boolean skipOldEntry; protected final ByteBufAllocator allocator; protected final byte[] dict; @Builder - public BundleReader(long maxTime, long minTime, boolean skipOldEntry, ByteBufAllocator allocator, byte[] dict) { + public BundleReader(long maxTime, long minTime, boolean linkSymbol, boolean skipOldEntry, ByteBufAllocator allocator, byte[] dict) { this.maxTime = maxTime == 0 ? Long.MAX_VALUE : maxTime; this.minTime = minTime; + this.linkSymbol = linkSymbol; this.skipOldEntry = skipOldEntry; this.allocator = allocator == null ? ByteBufAllocator.DEFAULT : allocator; this.dict = dict; } public void extract(Path bundle, Path destination) throws IOException { + var symbolsToLink = new HashMap(); var readMaps = new HashMap(); try (var zstd = new ZstdInputStreamNoFinalizer(Files.newInputStream(bundle, StandardOpenOption.READ)); var in = new EntryInputStream(zstd)) { @@ -52,7 +55,14 @@ public class BundleReader { var path = destination.resolve(entry.name()); // todo prevent path underflow var parentDir = path.getParent(); if (Files.notExists(parentDir)) Files.createDirectories(parentDir); - if ((entry.attribute() & BundleEntry.ATTR_REASSEMBLE) != 0) { + if ((entry.attribute() & BundleEntry.ATTR_SYMLINK) != 0) { + var toRead = (int) entry.length(); + var strArr = new byte[toRead]; + while ((toRead -= in.read(strArr, (int) (entry.length() - toRead), toRead)) > 0) ; + var target = new String(strArr); + var targetPath = Path.of(target); + Files.createSymbolicLink(targetPath, path); + } else if ((entry.attribute() & BundleEntry.ATTR_REASSEMBLE) != 0) { var entryData = allocator.buffer(); var toRead = entry.length(); var buffer = this.buffer; diff --git a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java index ff22d1f..ee50050 100644 --- a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java +++ b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java @@ -12,6 +12,7 @@ import lombok.Builder; import lombok.SneakyThrows; import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -142,6 +143,11 @@ public class BundleWriter implements Closeable { } public void write(Path path, boolean suggestGunzip) throws IOException { + if(Files.isDirectory(path)) return; + if(Files.isSymbolicLink(path)) { + writeSymbol(path); + return; + } if (suggestGunzip && allowReassemble) { //todo size limit writeReassembleInMem(path); @@ -150,6 +156,22 @@ public class BundleWriter implements Closeable { writePlain(path); } + private void writeSymbol(Path path) throws IOException { + if (!Files.isSymbolicLink(path)) return; + var target = Files.readSymbolicLink(path); + var rPath = relativeRoot.toAbsolutePath().relativize(path.toAbsolutePath()); + String linkTarget; + try{ + linkTarget = relativeRoot.relativize(target).toString(); + } catch (Exception e) { + linkTarget = target.toAbsolutePath().toString(); + } + var targetBytes = linkTarget.getBytes(StandardCharsets.UTF_8); + + this.outputStream.writeEntryHeader(rPath.toString(), BundleEntry.ATTR_SYMLINK,targetBytes.length, System.currentTimeMillis()); + this.outputStream.write(targetBytes); + } + @SneakyThrows private void writeReassembleInMem(Path path) { if (!"region".equals(path.getParent().toString()) && !path.toString().endsWith(".mca")) { // todo use pattern matcher diff --git a/bundler/src/main/java/io/ib67/sfcraft/bundler/Main.java b/bundler/src/main/java/io/ib67/sfcraft/bundler/Main.java index fb8e501..1a7bde1 100644 --- a/bundler/src/main/java/io/ib67/sfcraft/bundler/Main.java +++ b/bundler/src/main/java/io/ib67/sfcraft/bundler/Main.java @@ -31,7 +31,7 @@ public class Main { var out = opts.string("out", "Output. Can be jpack bundle or directory", null); var help = opts.bool("help", false); var allowGunzip = opts.bool("reasm", "Should we reassemble NBT files for maximum compression rate", true); - + var linkSymbol = opts.bool("link","Should we store/decompress symbol links? This may set you vulnerable if you're decompressing a malformed archive.", true); // useless parallel var parallel = opts.bool("parallel", "Use parallelized bundler. This conflicts with compress options", false); var parallelThreshold = opts.integer("threshold", "The threshold of parallel bundling", 512); @@ -82,7 +82,7 @@ public class Main { } if (in.endsWith(".swb.zst")) { - BundleReader.builder().build().extract(inPath, outPath); + BundleReader.builder().linkSymbol(linkSymbol).build().extract(inPath, outPath); } else if (out.endsWith(".swb.zst")) { UnaryOperator cfg = it -> it .allowGunzip(allowGunzip)