Skip to content

Commit

Permalink
Add --extract-all command, fix root "."
Browse files Browse the repository at this point in the history
Found some .unitypackages where everything is
under a root "." directory.
  • Loading branch information
m35 committed Jul 27, 2024
1 parent 7e2e4f8 commit 07a2ec6
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 173 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ This is a Java program requiring Java 8 or higher.
* Right-click to copy name, size, or GUID
* History.ini saves the last directory used

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

# Disclaimers

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>m35-projects</groupId>
<artifactId>unity-package-viewer</artifactId>
<version>0.0.1</version>
<version>0.0.2</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
72 changes: 72 additions & 0 deletions src/main/java/unitypackage/model/UnityArchiveInputStream.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Basic .unitypackage Viewer
* Copyright (C) 2024 Michael Sabin
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package unitypackage.model;

import java.io.FilterInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;

/**
* Like {@link TarArchiveInputStream} but for .unitypackage files,
* except it skips all directories.
*/
public class UnityArchiveInputStream extends FilterInputStream {

private final TarArchiveInputStream tarInputStream;
private final Map<String, UnityAsset> tarPathsToUnityAsset = new TreeMap<>();

public UnityArchiveInputStream(UnityPackage unityPackage) throws IOException {
this(unityPackage.getTarInputStream(), unityPackage.getUnityAssetList());
}

public UnityArchiveInputStream(TarArchiveInputStream tarInputStream, List<UnityAsset> unityAssetsToRead) {
super(tarInputStream);
this.tarInputStream = tarInputStream;

for (UnityAsset unityAsset : unityAssetsToRead) {
if (!unityAsset.isProbablyDirectory()) {
tarPathsToUnityAsset.put(unityAsset.getTarPathOf_asset_File(), unityAsset);
}
}
}

/**
* Like {@link TarArchiveInputStream#getNextEntry()} but for Unity asset files,
* except it only returns actual files, skipping directories.
*/
public UnityAsset getNextEntry() throws IOException {

TarArchiveEntry entry;
// Seek for the file of interest
while ((entry = tarInputStream.getNextEntry()) != null) {
String tarEntryName = entry.getName();
// Check if this file in the tar is an asset payload
UnityAsset unityAsset = tarPathsToUnityAsset.get(tarEntryName);
if (unityAsset != null) {
return unityAsset;
}
}

return null;
}
}
34 changes: 13 additions & 21 deletions src/main/java/unitypackage/model/UnityAssetBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,16 @@ public BufferedImage getPreview() {
return _preview;
}

public UnityAssetBuilder(String guidBaseDirectory) {
this.guidBaseDirectory = guidBaseDirectory;
public UnityAssetBuilder(String directoryGuidName) {
this.guidBaseDirectory = directoryGuidName;
}

public UnityAssetBuilder(TarArchiveEntry tarEntry,
public UnityAssetBuilder(String guidBaseDirectory, String fileName, TarArchiveEntry tarEntry,
TarArchiveInputStream tarInputStream) throws IOException
{
String rawFilePath = tarEntry.getName();
File rawFile = new File(rawFilePath);
guidBaseDirectory = rawFile.getParent();
this.guidBaseDirectory = guidBaseDirectory;

addFileFoundInDirectory(tarEntry, tarInputStream);
addFileFoundInDirectory(this.guidBaseDirectory, fileName, tarEntry, tarInputStream);
}

public UnityAsset makeUnityAsset() {
Expand All @@ -123,34 +121,28 @@ public UnityAsset makeUnityAsset() {
/**
* Sanity check that only files that belong in this directory are being added.
*/
public void assertGuidMatchesDirectoryName(String guid) {
if (!guid.equals(guidBaseDirectory)) {
throw new IllegalArgumentException("Argument guid " + guid + " != this guid" +
public void assertGuidMatchesDirectoryName(String directoryGuidName) {
if (!directoryGuidName.equals(guidBaseDirectory)) {
throw new IllegalArgumentException("Argument guid " + directoryGuidName + " != this guid" +
" dir " + guidBaseDirectory);
}
}

public void addFileFoundInDirectory(TarArchiveEntry tarEntry,
public void addFileFoundInDirectory(String directoryGuidName, String fileName, TarArchiveEntry tarEntry,
TarArchiveInputStream tarInputStream) throws IOException {
String rawFilePath = tarEntry.getName();
File rawFile = new File(rawFilePath);

String directoryGuidName = rawFile.getParent();

assertGuidMatchesDirectoryName(directoryGuidName);

String rawFileName = rawFile.getName();

switch (rawFileName) {
switch (fileName) {
case "asset":
asset_fileSize = tarEntry.getRealSize();
rawPathTo_asset_file = rawFilePath;
rawPathTo_asset_file = tarEntry.getName();
asset_dateModified = tarEntry.getLastModifiedDate();
break;
case "asset.meta":
asset_meta_guid = findGuidIn_asset_meta_File(tarEntry, tarInputStream);
if (!asset_meta_guid.equals(guidBaseDirectory)) {
// afaik the directory guid should match the guid in the asset.meta file
// Usually the directory guid matches the guid in the asset.meta file, but not always
String s = "Corrupted .unitypackage? directory guid" + " " + guidBaseDirectory + " != " + "asset.meta guid " + asset_meta_guid;
if (STRICT) {
throw new RuntimeException(s);
Expand All @@ -166,7 +158,7 @@ public void addFileFoundInDirectory(TarArchiveEntry tarEntry,
_preview = ImageIO.read(tarInputStream);
break;
default:
throw new RuntimeException("File name not recognized " + rawFilePath);
throw new RuntimeException("File name not recognized " + tarEntry.getName());
}
}

Expand Down
171 changes: 171 additions & 0 deletions src/main/java/unitypackage/model/UnityPackage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* Basic .unitypackage Viewer
* Copyright (C) 2024 Michael Sabin
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package unitypackage.model;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;

/**
* Indexes a .unitypackage and provides methods to read assets out of it.
*/
public class UnityPackage {

private static final String ROOT_ICON = ".icon.png";

private final File unitypackageFile;
private final List<UnityAsset> unityAssetList;

public UnityPackage(File unitypackageFile) throws IOException {
this.unitypackageFile = unitypackageFile;

// TODO are empty asset directories possible?
// that would break this program

TreeMap<String, UnityAssetBuilder> rootGuidDirectories = new TreeMap<>();

try (TarArchiveInputStream tarInput = getTarInputStream()) {

boolean hasDotRootDirectory = false;

TarArchiveEntry tarEntry;
while ((tarEntry = tarInput.getNextEntry()) != null) {

final String rawFilePathString = tarEntry.getName();
final boolean isDirectory = tarEntry.isDirectory();

Path rawPath = Paths.get(rawFilePathString);

if (rawPath.getNameCount() == 1) {
String shouldBeDirName = rawPath.getName(0).toString();
if (isDirectory) {
if (".".equals(shouldBeDirName)) {
hasDotRootDirectory = true;
continue;
}
} else if (!ROOT_ICON.equals(shouldBeDirName)) {
// I don't know if the root ".icon.png" would be next to a root "." or under it
throw new RuntimeException("Found root path that is not a directory or " + ROOT_ICON + ": " + rawFilePathString);
}

}

if (hasDotRootDirectory) {
// Trim off the "." before continuing
rawPath = rawPath.subpath(1, rawPath.getNameCount());
}

Path rawPathParent = rawPath.getParent();

String guidDirectory;
String fileName;

if (isDirectory) {
if (rawPathParent != null) {
throw new RuntimeException("Found nested directory " + rawFilePathString);
}
guidDirectory = rawPath.toString();
fileName = null;
} else {
fileName = rawPath.getFileName().toString();
guidDirectory = rawPathParent == null ? null : rawPathParent.toString();
}

if (guidDirectory == null) {
if (fileName.equals(ROOT_ICON)) {
// Image icon exists in the root
// TODO do something with this
// For now ignore it
continue;
} else {
throw new RuntimeException("Found nested directory " + rawFilePathString);
}
}

UnityAssetBuilder builder = rootGuidDirectories.get(guidDirectory);

if (builder == null) {
if (isDirectory) {
builder = new UnityAssetBuilder(guidDirectory);
} else {
// Do .tar archives always put a directory definition before any files under it?
// In any case, be flexible.
builder = new UnityAssetBuilder(guidDirectory, fileName, tarEntry, tarInput);
}
rootGuidDirectories.put(guidDirectory, builder);
} else {
if (isDirectory)
builder.assertGuidMatchesDirectoryName(guidDirectory);
else
builder.addFileFoundInDirectory(guidDirectory, fileName, tarEntry, tarInput);
}
}
}

List<UnityAsset> assets = rootGuidDirectories
.values()
.stream()
.map(tad -> tad.makeUnityAsset())
.collect(Collectors.toList());

unityAssetList = Collections.unmodifiableList(assets);
}

public File getUnitypackageFile() {
return unitypackageFile;
}

public List<UnityAsset> getUnityAssetList() {
return unityAssetList;
}

final public TarArchiveInputStream getTarInputStream() throws IOException {
return new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(unitypackageFile)));
}

public UnityArchiveInputStream getUnityArchiveInputStream() throws IOException {
return new UnityArchiveInputStream(this);
}

public InputStream getFileStream(UnityAsset assetToExtract) throws IOException {

UnityArchiveInputStream unityInputStream = new UnityArchiveInputStream(getTarInputStream(),
Collections.singletonList(assetToExtract));

UnityAsset assetFound = unityInputStream.getNextEntry();

// Sanity check
if (assetToExtract != assetFound) {
throw new IllegalArgumentException("This should never happen");
}

return unityInputStream;
}

}
Loading

0 comments on commit 07a2ec6

Please sign in to comment.