Skip to content

Commit c4df1d6

Browse files
committed
Add support for directory mcpack
1 parent ce72618 commit c4df1d6

File tree

3 files changed

+192
-21
lines changed

3 files changed

+192
-21
lines changed

src/main/java/cn/nukkit/SharedConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public final class SharedConstants {
3737

3838
public static final boolean USE_FUNCTION_EVENT_EXECUTOR = true;
3939

40+
public static final boolean RESOURCE_PACK_ENCRYPTION = false;
4041
public static final int RESOURCE_PACK_CHUNK_SIZE = 128 * 1024; // 128KB
4142

4243
public static final boolean ENABLE_BLOCK_DESTROY_SPEED_COMPATIBILITY = true;

src/main/java/cn/nukkit/resourcepacks/ResourcePackManager.java

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,20 @@
44
import com.google.common.io.Files;
55
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
66
import lombok.extern.log4j.Log4j2;
7+
import org.apache.commons.io.FileUtils;
8+
import org.apache.commons.io.filefilter.TrueFileFilter;
79

10+
import javax.annotation.Nullable;
811
import java.io.File;
12+
import java.io.FileOutputStream;
913
import java.io.IOException;
14+
import java.nio.file.attribute.FileTime;
15+
import java.util.Collection;
1016
import java.util.Map;
17+
import java.util.TreeSet;
18+
import java.util.zip.Deflater;
19+
import java.util.zip.ZipEntry;
20+
import java.util.zip.ZipOutputStream;
1121

1222
@Log4j2
1323
public class ResourcePackManager {
@@ -42,7 +52,7 @@ public void tryLoad(File pack) {
4252
try {
4353
ResourcePack resourcePack = null;
4454

45-
if (!pack.isDirectory()) { //directory resource packs temporarily unsupported
55+
if (!pack.isDirectory()) {
4656
switch (Files.getFileExtension(pack.getName())) {
4757
case "zip":
4858
case "mcpack":
@@ -53,6 +63,12 @@ public void tryLoad(File pack) {
5363
.translate("nukkit.resources.unknown-format", pack.getName()));
5464
break;
5565
}
66+
} else {
67+
File tempPack = loadDirectoryPack(pack);
68+
if (tempPack == null) {
69+
return;
70+
}
71+
resourcePack = new ZippedResourcePack(tempPack);
5672
}
5773

5874
if (resourcePack != null) {
@@ -77,6 +93,39 @@ public void tryLoad(File pack) {
7793
}
7894
}
7995

96+
@Nullable
97+
private static File loadDirectoryPack(File directory) {
98+
File manifestFile = new File(directory, "manifest.json");
99+
if (!manifestFile.exists() || !manifestFile.isFile()) {
100+
return null;
101+
}
102+
103+
File tempFile;
104+
try {
105+
tempFile = File.createTempFile("pack", ".zip");
106+
tempFile.deleteOnExit();
107+
108+
FileTime time = FileTime.fromMillis(0);
109+
try (ZipOutputStream stream = new ZipOutputStream(new FileOutputStream(tempFile))) {
110+
stream.setLevel(Deflater.BEST_COMPRESSION);
111+
112+
Collection<File> files = new TreeSet<>(FileUtils.listFiles(directory, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE));
113+
for (File file : files) {
114+
ZipEntry entry = new ZipEntry(directory.toPath().relativize(file.toPath()).toString())
115+
.setCreationTime(time)
116+
.setLastModifiedTime(time)
117+
.setLastAccessTime(time);
118+
stream.putNextEntry(entry);
119+
stream.write(Files.toByteArray(file));
120+
stream.closeEntry();
121+
}
122+
}
123+
} catch (IOException e) {
124+
throw new RuntimeException("Unable to create temporary mcpack file", e);
125+
}
126+
return tempFile;
127+
}
128+
80129
public Map<String, ResourcePack> getResourcePacksMap() {
81130
return resourcePacksById;
82131
}

src/main/java/cn/nukkit/resourcepacks/ZippedResourcePack.java

Lines changed: 141 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,167 @@
33
import cn.nukkit.Server;
44
import cn.nukkit.math.Mth;
55
import cn.nukkit.resourcepacks.PackManifest.Module;
6-
6+
import cn.nukkit.utils.Hash;
7+
import cn.nukkit.utils.JsonUtil;
8+
import com.fasterxml.jackson.databind.JsonNode;
9+
import com.google.common.io.Files;
10+
import lombok.extern.log4j.Log4j2;
11+
import org.apache.commons.lang3.RandomStringUtils;
12+
13+
import javax.crypto.Cipher;
14+
import javax.crypto.spec.IvParameterSpec;
15+
import javax.crypto.spec.SecretKeySpec;
16+
import java.io.ByteArrayOutputStream;
17+
import java.io.DataOutputStream;
718
import java.io.File;
819
import java.io.IOException;
9-
import java.nio.file.Files;
20+
import java.nio.charset.StandardCharsets;
21+
import java.nio.file.attribute.FileTime;
1022
import java.security.MessageDigest;
1123
import java.security.NoSuchAlgorithmException;
12-
import java.util.Arrays;
13-
import java.util.Optional;
24+
import java.util.*;
25+
import java.util.zip.Deflater;
1426
import java.util.zip.ZipEntry;
1527
import java.util.zip.ZipFile;
28+
import java.util.zip.ZipOutputStream;
1629

1730
import static cn.nukkit.SharedConstants.*;
1831

32+
@Log4j2
1933
public class ZippedResourcePack extends AbstractResourcePack {
2034
private final int size;
2135
private final byte[][] chunks;
2236
private final byte[] sha256;
2337

38+
private final String encryptionKey;
39+
2440
public ZippedResourcePack(File file) throws IOException {
41+
this(file, true);
42+
}
43+
44+
public ZippedResourcePack(File file, boolean encrypt) throws IOException {
2545
if (!file.exists()) {
2646
throw new IllegalArgumentException(Server.getInstance().getLanguage()
2747
.translate("nukkit.resources.zip.not-found", file.getName()));
2848
}
2949

50+
byte[] bytes = Files.toByteArray(file);
3051
try (ZipFile zip = new ZipFile(file)) {
31-
ZipEntry entry = Optional.ofNullable(zip.getEntry("manifest.json"))
52+
ZipEntry manifestEntry = Optional.ofNullable(zip.getEntry("manifest.json"))
3253
.orElse(zip.getEntry("pack_manifest.json"));
3354

34-
if (entry == null) {
55+
if (manifestEntry == null) {
3556
throw new IllegalArgumentException(Server.getInstance().getLanguage()
3657
.translate("nukkit.resources.zip.no-manifest"));
3758
}
3859

39-
manifest = PackManifest.load(zip.getInputStream(entry));
40-
}
60+
manifest = PackManifest.load(zip.getInputStream(manifestEntry));
4161

42-
if (!manifest.isValid()) {
43-
throw new IllegalArgumentException(Server.getInstance().getLanguage()
44-
.translate("nukkit.resources.zip.invalid-manifest"));
45-
}
46-
47-
id = manifest.getHeader().getUuid().toString();
48-
version = manifest.getHeader().getVersion().toString();
49-
type = manifest.getModules().stream()
50-
.findFirst()
51-
.map(Module::getType)
52-
.orElse("resources");
62+
if (!manifest.isValid()) {
63+
throw new IllegalArgumentException(Server.getInstance().getLanguage()
64+
.translate("nukkit.resources.zip.invalid-manifest"));
65+
}
5366

54-
byte[] bytes = Files.readAllBytes(file.toPath());
67+
id = manifest.getHeader().getUuid().toString();
68+
version = manifest.getHeader().getVersion().toString();
69+
type = manifest.getModules().stream()
70+
.findFirst()
71+
.map(Module::getType)
72+
.orElse("resources");
73+
74+
String encryptionKey;
75+
if (RESOURCE_PACK_ENCRYPTION && encrypt) {
76+
Random random = new Random(Hash.xxh64(bytes));
77+
encryptionKey = generateToken(random);
78+
79+
FileTime time = FileTime.fromMillis(0);
80+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
81+
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
82+
zos.setLevel(Deflater.BEST_COMPRESSION);
83+
84+
List<Map<String, String>> pairs = new ArrayList<>();
85+
Iterator<? extends ZipEntry> iterator = zip.entries().asIterator();
86+
while (iterator.hasNext()) {
87+
ZipEntry entry = iterator.next();
88+
String name = entry.getName().replace('\\', '/');
89+
byte[] content = zip.getInputStream(entry).readAllBytes();
90+
91+
if (name.endsWith(".json") || name.endsWith(".material")) {
92+
try {
93+
JsonNode root = JsonUtil.TRUSTED_JSON_MAPPER.readTree(content);
94+
// minimize JSON file content
95+
content = JsonUtil.TRUSTED_JSON_MAPPER.writeValueAsBytes(root);
96+
} catch (Exception ignored) {
97+
}
98+
} else if (name.startsWith("subpacks/")) {
99+
//TODO: sub-packs
100+
}
101+
102+
byte[] data;
103+
if (!"manifest.json".equalsIgnoreCase(name)
104+
&& !"pack_icon.png".equalsIgnoreCase(name)
105+
&& !"bug_pack_icon.png".equalsIgnoreCase(name)) {
106+
String token = generateToken(random);
107+
byte[] encodedToken = token.getBytes(StandardCharsets.UTF_8);
108+
SecretKeySpec secretKey = new SecretKeySpec(encodedToken, "AES");
109+
Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
110+
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(Arrays.copyOf(encodedToken, 16)));
111+
data = cipher.doFinal(content);
112+
113+
Map<String, String> pair = new HashMap<>();
114+
pair.put("path", name);
115+
pair.put("key", token);
116+
pairs.add(pair);
117+
} else {
118+
data = content;
119+
120+
pairs.add(Collections.singletonMap("path", name));
121+
}
122+
123+
zos.putNextEntry(new ZipEntry(entry)
124+
.setCreationTime(time)
125+
.setLastModifiedTime(time)
126+
.setLastAccessTime(time));
127+
zos.write(data);
128+
zos.closeEntry();
129+
}
130+
131+
zos.putNextEntry(new ZipEntry("contents.json")
132+
.setCreationTime(time)
133+
.setLastModifiedTime(time)
134+
.setLastAccessTime(time));
135+
136+
DataOutputStream dos = new DataOutputStream(zos);
137+
dos.writeInt(0); // version
138+
dos.writeInt(0xfcb9cf9b); // magic
139+
dos.writeLong(0);
140+
141+
byte[] encodedId = id.getBytes(StandardCharsets.UTF_8);
142+
int length = encodedId.length;
143+
zos.write(length);
144+
zos.write(encodedId);
145+
zos.write(new byte[256 - (4 + 4 + 8 + 1 + length)]);
146+
147+
byte[] encodedKey = encryptionKey.getBytes(StandardCharsets.UTF_8);
148+
SecretKeySpec secretKey = new SecretKeySpec(encodedKey, "AES");
149+
Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
150+
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(Arrays.copyOf(encodedKey, 16)));
151+
String json = JsonUtil.TRUSTED_JSON_MAPPER.writeValueAsString(Collections.singletonMap("content", pairs));
152+
zos.write(cipher.doFinal(json.getBytes(StandardCharsets.UTF_8)));
153+
154+
zos.closeEntry();
155+
156+
zos.finish();
157+
bytes = baos.toByteArray();
158+
} catch (Exception e) {
159+
encryptionKey = "";
160+
log.error("Unable to encrypt mcpack", e);
161+
}
162+
} else {
163+
encryptionKey = "";
164+
}
165+
this.encryptionKey = encryptionKey;
166+
}
55167
size = bytes.length;
56168

57169
int count = Mth.ceil(size / (float) RESOURCE_PACK_CHUNK_SIZE);
@@ -92,4 +204,13 @@ public byte[] getPackChunk(int index) {
92204

93205
return chunks[index];
94206
}
207+
208+
@Override
209+
public String getEncryptionKey() {
210+
return this.encryptionKey;
211+
}
212+
213+
private static String generateToken(Random random) {
214+
return RandomStringUtils.random(32, 0, 0, true, true, null, random);
215+
}
95216
}

0 commit comments

Comments
 (0)