Skip to content

Commit 07a2ec6

Browse files
committed
Add --extract-all command, fix root "."
Found some .unitypackages where everything is under a root "." directory.
1 parent 7e2e4f8 commit 07a2ec6

File tree

9 files changed

+340
-173
lines changed

9 files changed

+340
-173
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ This is a Java program requiring Java 8 or higher.
2626
* Right-click to copy name, size, or GUID
2727
* History.ini saves the last directory used
2828

29+
Alternatively, it can extract everything from the command line using the `--extract-all` command
30+
```
31+
java -jar UnityPackageViewer.x.x.x.jar path/to/file.unitypackage --extract-all
32+
```
2933

3034
# Disclaimers
3135

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>m35-projects</groupId>
55
<artifactId>unity-package-viewer</artifactId>
6-
<version>0.0.1</version>
6+
<version>0.0.2</version>
77
<packaging>jar</packaging>
88
<properties>
99
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Basic .unitypackage Viewer
3+
* Copyright (C) 2024 Michael Sabin
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package unitypackage.model;
20+
21+
import java.io.FilterInputStream;
22+
import java.io.IOException;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.TreeMap;
26+
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
27+
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
28+
29+
/**
30+
* Like {@link TarArchiveInputStream} but for .unitypackage files,
31+
* except it skips all directories.
32+
*/
33+
public class UnityArchiveInputStream extends FilterInputStream {
34+
35+
private final TarArchiveInputStream tarInputStream;
36+
private final Map<String, UnityAsset> tarPathsToUnityAsset = new TreeMap<>();
37+
38+
public UnityArchiveInputStream(UnityPackage unityPackage) throws IOException {
39+
this(unityPackage.getTarInputStream(), unityPackage.getUnityAssetList());
40+
}
41+
42+
public UnityArchiveInputStream(TarArchiveInputStream tarInputStream, List<UnityAsset> unityAssetsToRead) {
43+
super(tarInputStream);
44+
this.tarInputStream = tarInputStream;
45+
46+
for (UnityAsset unityAsset : unityAssetsToRead) {
47+
if (!unityAsset.isProbablyDirectory()) {
48+
tarPathsToUnityAsset.put(unityAsset.getTarPathOf_asset_File(), unityAsset);
49+
}
50+
}
51+
}
52+
53+
/**
54+
* Like {@link TarArchiveInputStream#getNextEntry()} but for Unity asset files,
55+
* except it only returns actual files, skipping directories.
56+
*/
57+
public UnityAsset getNextEntry() throws IOException {
58+
59+
TarArchiveEntry entry;
60+
// Seek for the file of interest
61+
while ((entry = tarInputStream.getNextEntry()) != null) {
62+
String tarEntryName = entry.getName();
63+
// Check if this file in the tar is an asset payload
64+
UnityAsset unityAsset = tarPathsToUnityAsset.get(tarEntryName);
65+
if (unityAsset != null) {
66+
return unityAsset;
67+
}
68+
}
69+
70+
return null;
71+
}
72+
}

src/main/java/unitypackage/model/UnityAssetBuilder.java

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,16 @@ public BufferedImage getPreview() {
102102
return _preview;
103103
}
104104

105-
public UnityAssetBuilder(String guidBaseDirectory) {
106-
this.guidBaseDirectory = guidBaseDirectory;
105+
public UnityAssetBuilder(String directoryGuidName) {
106+
this.guidBaseDirectory = directoryGuidName;
107107
}
108108

109-
public UnityAssetBuilder(TarArchiveEntry tarEntry,
109+
public UnityAssetBuilder(String guidBaseDirectory, String fileName, TarArchiveEntry tarEntry,
110110
TarArchiveInputStream tarInputStream) throws IOException
111111
{
112-
String rawFilePath = tarEntry.getName();
113-
File rawFile = new File(rawFilePath);
114-
guidBaseDirectory = rawFile.getParent();
112+
this.guidBaseDirectory = guidBaseDirectory;
115113

116-
addFileFoundInDirectory(tarEntry, tarInputStream);
114+
addFileFoundInDirectory(this.guidBaseDirectory, fileName, tarEntry, tarInputStream);
117115
}
118116

119117
public UnityAsset makeUnityAsset() {
@@ -123,34 +121,28 @@ public UnityAsset makeUnityAsset() {
123121
/**
124122
* Sanity check that only files that belong in this directory are being added.
125123
*/
126-
public void assertGuidMatchesDirectoryName(String guid) {
127-
if (!guid.equals(guidBaseDirectory)) {
128-
throw new IllegalArgumentException("Argument guid " + guid + " != this guid" +
124+
public void assertGuidMatchesDirectoryName(String directoryGuidName) {
125+
if (!directoryGuidName.equals(guidBaseDirectory)) {
126+
throw new IllegalArgumentException("Argument guid " + directoryGuidName + " != this guid" +
129127
" dir " + guidBaseDirectory);
130128
}
131129
}
132130

133-
public void addFileFoundInDirectory(TarArchiveEntry tarEntry,
131+
public void addFileFoundInDirectory(String directoryGuidName, String fileName, TarArchiveEntry tarEntry,
134132
TarArchiveInputStream tarInputStream) throws IOException {
135-
String rawFilePath = tarEntry.getName();
136-
File rawFile = new File(rawFilePath);
137-
138-
String directoryGuidName = rawFile.getParent();
139133

140134
assertGuidMatchesDirectoryName(directoryGuidName);
141135

142-
String rawFileName = rawFile.getName();
143-
144-
switch (rawFileName) {
136+
switch (fileName) {
145137
case "asset":
146138
asset_fileSize = tarEntry.getRealSize();
147-
rawPathTo_asset_file = rawFilePath;
139+
rawPathTo_asset_file = tarEntry.getName();
148140
asset_dateModified = tarEntry.getLastModifiedDate();
149141
break;
150142
case "asset.meta":
151143
asset_meta_guid = findGuidIn_asset_meta_File(tarEntry, tarInputStream);
152144
if (!asset_meta_guid.equals(guidBaseDirectory)) {
153-
// afaik the directory guid should match the guid in the asset.meta file
145+
// Usually the directory guid matches the guid in the asset.meta file, but not always
154146
String s = "Corrupted .unitypackage? directory guid" + " " + guidBaseDirectory + " != " + "asset.meta guid " + asset_meta_guid;
155147
if (STRICT) {
156148
throw new RuntimeException(s);
@@ -166,7 +158,7 @@ public void addFileFoundInDirectory(TarArchiveEntry tarEntry,
166158
_preview = ImageIO.read(tarInputStream);
167159
break;
168160
default:
169-
throw new RuntimeException("File name not recognized " + rawFilePath);
161+
throw new RuntimeException("File name not recognized " + tarEntry.getName());
170162
}
171163
}
172164

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Basic .unitypackage Viewer
3+
* Copyright (C) 2024 Michael Sabin
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package unitypackage.model;
20+
21+
import java.io.File;
22+
import java.io.FileInputStream;
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
import java.nio.file.Path;
26+
import java.nio.file.Paths;
27+
import java.util.Collections;
28+
import java.util.List;
29+
import java.util.TreeMap;
30+
import java.util.stream.Collectors;
31+
import java.util.zip.GZIPInputStream;
32+
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
33+
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
34+
35+
/**
36+
* Indexes a .unitypackage and provides methods to read assets out of it.
37+
*/
38+
public class UnityPackage {
39+
40+
private static final String ROOT_ICON = ".icon.png";
41+
42+
private final File unitypackageFile;
43+
private final List<UnityAsset> unityAssetList;
44+
45+
public UnityPackage(File unitypackageFile) throws IOException {
46+
this.unitypackageFile = unitypackageFile;
47+
48+
// TODO are empty asset directories possible?
49+
// that would break this program
50+
51+
TreeMap<String, UnityAssetBuilder> rootGuidDirectories = new TreeMap<>();
52+
53+
try (TarArchiveInputStream tarInput = getTarInputStream()) {
54+
55+
boolean hasDotRootDirectory = false;
56+
57+
TarArchiveEntry tarEntry;
58+
while ((tarEntry = tarInput.getNextEntry()) != null) {
59+
60+
final String rawFilePathString = tarEntry.getName();
61+
final boolean isDirectory = tarEntry.isDirectory();
62+
63+
Path rawPath = Paths.get(rawFilePathString);
64+
65+
if (rawPath.getNameCount() == 1) {
66+
String shouldBeDirName = rawPath.getName(0).toString();
67+
if (isDirectory) {
68+
if (".".equals(shouldBeDirName)) {
69+
hasDotRootDirectory = true;
70+
continue;
71+
}
72+
} else if (!ROOT_ICON.equals(shouldBeDirName)) {
73+
// I don't know if the root ".icon.png" would be next to a root "." or under it
74+
throw new RuntimeException("Found root path that is not a directory or " + ROOT_ICON + ": " + rawFilePathString);
75+
}
76+
77+
}
78+
79+
if (hasDotRootDirectory) {
80+
// Trim off the "." before continuing
81+
rawPath = rawPath.subpath(1, rawPath.getNameCount());
82+
}
83+
84+
Path rawPathParent = rawPath.getParent();
85+
86+
String guidDirectory;
87+
String fileName;
88+
89+
if (isDirectory) {
90+
if (rawPathParent != null) {
91+
throw new RuntimeException("Found nested directory " + rawFilePathString);
92+
}
93+
guidDirectory = rawPath.toString();
94+
fileName = null;
95+
} else {
96+
fileName = rawPath.getFileName().toString();
97+
guidDirectory = rawPathParent == null ? null : rawPathParent.toString();
98+
}
99+
100+
if (guidDirectory == null) {
101+
if (fileName.equals(ROOT_ICON)) {
102+
// Image icon exists in the root
103+
// TODO do something with this
104+
// For now ignore it
105+
continue;
106+
} else {
107+
throw new RuntimeException("Found nested directory " + rawFilePathString);
108+
}
109+
}
110+
111+
UnityAssetBuilder builder = rootGuidDirectories.get(guidDirectory);
112+
113+
if (builder == null) {
114+
if (isDirectory) {
115+
builder = new UnityAssetBuilder(guidDirectory);
116+
} else {
117+
// Do .tar archives always put a directory definition before any files under it?
118+
// In any case, be flexible.
119+
builder = new UnityAssetBuilder(guidDirectory, fileName, tarEntry, tarInput);
120+
}
121+
rootGuidDirectories.put(guidDirectory, builder);
122+
} else {
123+
if (isDirectory)
124+
builder.assertGuidMatchesDirectoryName(guidDirectory);
125+
else
126+
builder.addFileFoundInDirectory(guidDirectory, fileName, tarEntry, tarInput);
127+
}
128+
}
129+
}
130+
131+
List<UnityAsset> assets = rootGuidDirectories
132+
.values()
133+
.stream()
134+
.map(tad -> tad.makeUnityAsset())
135+
.collect(Collectors.toList());
136+
137+
unityAssetList = Collections.unmodifiableList(assets);
138+
}
139+
140+
public File getUnitypackageFile() {
141+
return unitypackageFile;
142+
}
143+
144+
public List<UnityAsset> getUnityAssetList() {
145+
return unityAssetList;
146+
}
147+
148+
final public TarArchiveInputStream getTarInputStream() throws IOException {
149+
return new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(unitypackageFile)));
150+
}
151+
152+
public UnityArchiveInputStream getUnityArchiveInputStream() throws IOException {
153+
return new UnityArchiveInputStream(this);
154+
}
155+
156+
public InputStream getFileStream(UnityAsset assetToExtract) throws IOException {
157+
158+
UnityArchiveInputStream unityInputStream = new UnityArchiveInputStream(getTarInputStream(),
159+
Collections.singletonList(assetToExtract));
160+
161+
UnityAsset assetFound = unityInputStream.getNextEntry();
162+
163+
// Sanity check
164+
if (assetToExtract != assetFound) {
165+
throw new IllegalArgumentException("This should never happen");
166+
}
167+
168+
return unityInputStream;
169+
}
170+
171+
}

0 commit comments

Comments
 (0)