Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
queries: +security-extended
languages: ${{ matrix.language }}
Expand All @@ -48,11 +48,16 @@ jobs:
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality

# Set up JDK for Maven build
- name: Set up JDK 19
uses: actions/setup-java@v3
with:
java-version: '19'
distribution: 'temurin'

# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Build with Maven instead of autobuild
- name: Build with Maven
run: mvn clean compile -DskipTests

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
Expand All @@ -65,4 +70,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public void windowClosing(WindowEvent e) {
* Starts the download process.
*/
public void startDownload() {
downloadWorker = new SwingWorker<Boolean, Void>() {
downloadWorker = new SwingWorker<>() {
@Override
protected Boolean doInBackground() {
return GameUpdateManager.downloadAndInstallUpdate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import com.dazednconfused.catalauncher.configuration.ConfigurationManager;
import com.dazednconfused.catalauncher.helper.Paths;
import com.dazednconfused.catalauncher.helper.result.Result;
import com.dazednconfused.catalauncher.utils.CustomTimeUtils;

import io.vavr.control.Try;
Expand Down Expand Up @@ -220,11 +221,12 @@ public static boolean downloadAndInstallUpdate(Consumer<Integer> progressCallbac
statusCallback.accept("Extracting...");
progressCallback.accept(65);

File extractedApp = extractGameBinary(downloadFile, downloadsDir.toFile());
if (extractedApp == null) {
Result<Throwable, File> extractResult = extractGameBinary(downloadFile, downloadsDir.toFile());
if (extractResult.toEither().isLeft()) {
statusCallback.accept("Error: Extraction failed");
return false;
}
final File extractedApp = extractResult.toEither().get().getResult().orElseThrow();

// move old binary to trash ---
statusCallback.accept("Moving old binary to trash...");
Expand Down Expand Up @@ -352,7 +354,7 @@ protected static boolean downloadFile(String urlString, File destination, Consum
/**
* Extracts the game binary from a downloaded archive.
*/
protected static File extractGameBinary(File archive, File extractDir) {
protected static Result<Throwable, File> extractGameBinary(File archive, File extractDir) {
String name = archive.getName().toLowerCase();

try {
Expand All @@ -369,42 +371,42 @@ protected static File extractGameBinary(File archive, File extractDir) {

} else if (name.endsWith(".app")) {
// Already an app bundle
return archive;
return Result.success(archive);
}

LOGGER.warn("Unknown archive format: {}", name);
return null;
return Result.failure(new IOException("Unknown archive format: " + name));

} catch (Exception e) {
LOGGER.error("Extraction failed: {}", e.getMessage(), e);
return null;
return Result.failure(e);
}
}

/**
* Finds a .app bundle at the root level of the given directory (no recursion).
*/
protected static File findAppBundle(File directory) {
protected static Result<Throwable, File> findAppBundle(File directory) {
if (directory == null) {
return null;
return Result.failure(new IllegalArgumentException("directory cannot be null"));
}
File[] files = directory.listFiles();
if (files == null) {
return null;
return Result.failure(new IllegalArgumentException("Cannot list files in directory: " + directory));
}

for (File file : files) {
if (file.getName().endsWith(".app")) {
return file;
return Result.success(file);
}
}
return null;
return Result.failure(new IOException("No .app bundle found in: " + directory));
}

/**
* Extracts .app from a DMG file using hdiutil.
*/
private static File extractFromDmg(File dmgFile, File extractDir) {
private static Result<Throwable, File> extractFromDmg(File dmgFile, File extractDir) {
String mountPoint = null;
try {
// mount the DMG and parse output to find mount point ---
Expand All @@ -429,7 +431,7 @@ private static File extractFromDmg(File dmgFile, File extractDir) {
int mountResult = mountProcess.waitFor();
if (mountResult != 0) {
LOGGER.error("Failed to mount DMG (exit code {}): {}", mountResult, mountOutput);
return null;
return Result.failure(new IOException("Failed to mount DMG (exit code " + mountResult + "): " + mountOutput));
}

// parse mount point from output (format: "/dev/diskX Apple_HFS /Volumes/Name")
Expand All @@ -444,18 +446,19 @@ private static File extractFromDmg(File dmgFile, File extractDir) {

if (mountPoint == null) {
LOGGER.error("Could not determine mount point from output: {}", mountOutput);
return null;
return Result.failure(new IOException("Could not determine mount point from output: " + mountOutput));
}

LOGGER.debug("DMG mounted at: {}", mountPoint);

// find .app in mounted volume ---
File mountDir = new File(mountPoint);
File appBundle = findAppBundle(mountDir);
if (appBundle == null) {
Result<Throwable, File> appBundleResult = findAppBundle(mountDir);
if (appBundleResult.toEither().isLeft()) {
LOGGER.error("No .app found in DMG at {}", mountPoint);
return null;
return appBundleResult;
}
File appBundle = appBundleResult.toEither().get().getResult().orElseThrow();

LOGGER.debug("Found app bundle: {}", appBundle.getAbsolutePath());

Expand All @@ -479,7 +482,7 @@ private static File extractFromDmg(File dmgFile, File extractDir) {
int copyResult = copyProcess.waitFor();
if (copyResult != 0) {
LOGGER.error("Failed to copy app bundle (exit code {})", copyResult);
return null;
return Result.failure(new IOException("Failed to copy app bundle (exit code " + copyResult + ")"));
}

LOGGER.debug("App bundle copied to: {}", destApp.getAbsolutePath());
Expand Down Expand Up @@ -508,11 +511,11 @@ private static File extractFromDmg(File dmgFile, File extractDir) {
LOGGER.debug("Set executable permissions on: {}", resourcesDir.getAbsolutePath());
}

return destApp;
return Result.success(destApp);

} catch (Exception e) {
LOGGER.error("DMG extraction failed: {}", e.getMessage(), e);
return null;
return Result.failure(e);
} finally {
// always try to unmount ---
if (mountPoint != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import com.dazednconfused.catalauncher.configuration.ConfigurationManager;
import com.dazednconfused.catalauncher.helper.Paths;
import com.dazednconfused.catalauncher.helper.result.Result;
import com.dazednconfused.catalauncher.utils.TestUtils;
import com.sun.net.httpserver.HttpServer;

Expand Down Expand Up @@ -1496,29 +1497,29 @@ void findAppBundle_finds_app_at_root_success(@TempDir Path tempDir) throws IOExc
Path appPath = tempDir.resolve("TestGame.app");
Files.createDirectories(appPath.resolve("Contents/MacOS"));

File result = GameUpdateManager.findAppBundle(tempDir.toFile());
Result<Throwable, File> result = GameUpdateManager.findAppBundle(tempDir.toFile());

assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("TestGame.app");
assertThat(result.toEither().isRight()).isTrue();
assertThat(result.getOrElseThrowUnchecked().getName()).isEqualTo("TestGame.app");
}

@Test
void findAppBundle_returns_null_when_no_app_success(@TempDir Path tempDir) throws IOException {
void findAppBundle_returns_failure_when_no_app_success(@TempDir Path tempDir) throws IOException {
Files.createDirectories(tempDir.resolve("SomeFolder"));

File result = GameUpdateManager.findAppBundle(tempDir.toFile());
Result<Throwable, File> result = GameUpdateManager.findAppBundle(tempDir.toFile());

assertThat(result).isNull();
assertThat(result.toEither().isLeft()).isTrue();
}

@Test
void findAppBundle_returns_null_for_null_directory_success() {
assertThat(GameUpdateManager.findAppBundle(null)).isNull();
void findAppBundle_returns_failure_for_null_directory_success() {
assertThat(GameUpdateManager.findAppBundle(null).toEither().isLeft()).isTrue();
}

@Test
void findAppBundle_returns_null_for_nonexistent_directory_success() {
assertThat(GameUpdateManager.findAppBundle(new File("/nonexistent/path"))).isNull();
void findAppBundle_returns_failure_for_nonexistent_directory_success() {
assertThat(GameUpdateManager.findAppBundle(new File("/nonexistent/path")).toEither().isLeft()).isTrue();
}

// ========================================
Expand Down Expand Up @@ -1606,33 +1607,34 @@ void findLargestMacOsAsset_recognizes_all_mac_patterns_success() throws IOExcept
void extractGameBinary_extracts_zip_with_app_bundle_success(@TempDir Path tempDir) {
File zipFile = TestUtils.getFromResource("gameupdate/zip/game-macos.zip");

File result = GameUpdateManager.extractGameBinary(zipFile, tempDir.toFile());
Result<Throwable, File> result = GameUpdateManager.extractGameBinary(zipFile, tempDir.toFile());

assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("TestGame.app");
assertThat(new File(result, "Contents/Info.plist")).exists();
assertThat(new File(result, "Contents/MacOS/TestGame")).exists();
assertThat(result.toEither().isRight()).isTrue();
File resultFile = result.getOrElseThrowUnchecked();
assertThat(resultFile.getName()).isEqualTo("TestGame.app");
assertThat(new File(resultFile, "Contents/Info.plist")).exists();
assertThat(new File(resultFile, "Contents/MacOS/TestGame")).exists();
}

@Test
void extractGameBinary_returns_null_for_unknown_format_success(@TempDir Path tempDir) throws IOException {
void extractGameBinary_returns_failure_for_unknown_format_success(@TempDir Path tempDir) throws IOException {
File unknownFile = tempDir.resolve("game.tar.gz").toFile();
Files.createFile(unknownFile.toPath());

File result = GameUpdateManager.extractGameBinary(unknownFile, tempDir.toFile());
Result<Throwable, File> result = GameUpdateManager.extractGameBinary(unknownFile, tempDir.toFile());

assertThat(result).isNull();
assertThat(result.toEither().isLeft()).isTrue();
}

@Test
void extractGameBinary_returns_app_file_directly_success(@TempDir Path tempDir) throws IOException {
Path appPath = tempDir.resolve("DirectGame.app");
Files.createDirectories(appPath);

File result = GameUpdateManager.extractGameBinary(appPath.toFile(), tempDir.toFile());
Result<Throwable, File> result = GameUpdateManager.extractGameBinary(appPath.toFile(), tempDir.toFile());

assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("DirectGame.app");
assertThat(result.toEither().isRight()).isTrue();
assertThat(result.getOrElseThrowUnchecked().getName()).isEqualTo("DirectGame.app");
}

// ========================================
Expand All @@ -1644,13 +1646,14 @@ void extractGameBinary_returns_app_file_directly_success(@TempDir Path tempDir)
void extractGameBinary_extracts_dmg_with_app_bundle_success(@TempDir Path tempDir) {
File dmgFile = TestUtils.getFromResource("gameupdate/dmg/game-macos.dmg");

File result = GameUpdateManager.extractGameBinary(dmgFile, tempDir.toFile());
Result<Throwable, File> result = GameUpdateManager.extractGameBinary(dmgFile, tempDir.toFile());

assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("TestGame.app");
assertThat(new File(result, "Contents/Info.plist")).exists();
assertThat(new File(result, "Contents/MacOS/TestGame")).exists();
assertThat(new File(result, "Contents/Resources/game.dat")).exists();
assertThat(result.toEither().isRight()).isTrue();
File resultFile = result.getOrElseThrowUnchecked();
assertThat(resultFile.getName()).isEqualTo("TestGame.app");
assertThat(new File(resultFile, "Contents/Info.plist")).exists();
assertThat(new File(resultFile, "Contents/MacOS/TestGame")).exists();
assertThat(new File(resultFile, "Contents/Resources/game.dat")).exists();
}

// ========================================
Expand Down Expand Up @@ -2042,10 +2045,11 @@ void full_zip_extraction_and_app_discovery_workflow_success(@TempDir Path tempDi
File zipFile = TestUtils.getFromResource("gameupdate/zip/game-macos.zip");

// execute test ---
File extractedApp = GameUpdateManager.extractGameBinary(zipFile, tempDir.toFile());
Result<Throwable, File> extractResult = GameUpdateManager.extractGameBinary(zipFile, tempDir.toFile());

// verify assertions ---
assertThat(extractedApp).isNotNull();
assertThat(extractResult.toEither().isRight()).isTrue();
File extractedApp = extractResult.getOrElseThrowUnchecked();
assertThat(extractedApp.isDirectory()).isTrue();

// verify complete app bundle structure
Expand All @@ -2063,10 +2067,11 @@ void full_dmg_extraction_and_app_discovery_workflow_success(@TempDir Path tempDi
File dmgFile = TestUtils.getFromResource("gameupdate/dmg/game-macos.dmg");

// execute test ---
File extractedApp = GameUpdateManager.extractGameBinary(dmgFile, tempDir.toFile());
Result<Throwable, File> extractResult = GameUpdateManager.extractGameBinary(dmgFile, tempDir.toFile());

// verify assertions ---
assertThat(extractedApp).isNotNull();
assertThat(extractResult.toEither().isRight()).isTrue();
File extractedApp = extractResult.getOrElseThrowUnchecked();

// verify Info.plist content
String plistContent = Files.readString(extractedApp.toPath().resolve("Contents/Info.plist"));
Expand Down Expand Up @@ -2101,10 +2106,11 @@ void full_download_and_extract_workflow_success(@TempDir Path tempDir) throws IO
assertThat(downloadDest).exists();

// execute extraction ---
File extractedApp = GameUpdateManager.extractGameBinary(downloadDest, tempDir.toFile());
Result<Throwable, File> extractResult = GameUpdateManager.extractGameBinary(downloadDest, tempDir.toFile());

// verify extraction ---
assertThat(extractedApp).isNotNull();
assertThat(extractResult.toEither().isRight()).isTrue();
File extractedApp = extractResult.getOrElseThrowUnchecked();
assertThat(extractedApp.getName()).isEqualTo("TestGame.app");
}

Expand Down
Loading