Merge remote-tracking branch 'upstream/develop' into desysteminfo

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2025-12-07 11:58:21 +00:00
commit 283d49ba35
No known key found for this signature in database
GPG key ID: 5E39D70B4C93C38E
90 changed files with 1698 additions and 2299 deletions

View file

@ -6,3 +6,6 @@ root = true
# C++ Code Style settings
[*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}]
cpp_generate_documentation_comments = doxygen_slash_star
[CMakeLists.txt]
ij_continuation_indent_size = 4

View file

@ -60,43 +60,24 @@ runs:
run: |
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr
mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml
export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated
export OUTPUT="PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage"
chmod +x linuxdeploy-*.AppImage
mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib
mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp -r ${{ runner.workspace }}/Qt/${{ inputs.qt-version }}/gcc_*64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp /usr/lib/"$DEB_HOST_MULTIARCH"/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/"$DEB_HOST_MULTIARCH"/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/"$DEB_HOST_MULTIARCH"/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
export LD_LIBRARY_PATH
chmod +x AppImageUpdate-"$APPIMAGE_ARCH".AppImage
cp AppImageUpdate-"$APPIMAGE_ARCH".AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin
export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage.zsync"
cp ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.{metainfo,appdata}.xml
if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then
export SIGN=1
export SIGN_KEY=${{ inputs.gpg-private-key-id }}
mkdir -p ~/.gnupg/
echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key
gpg --import ~/.gnupg/private.key
echo "$GPG_PRIVATE_KEY" > privkey.asc
gpg --import privkey.asc
gpg --export --armor 9C7A2C9B62603299 > pubkey.asc
else
echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY
fi
./linuxdeploy-"$APPIMAGE_ARCH".AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg
mv "PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build-type }}-$APPIMAGE_ARCH.AppImage"
appimagetool -s deploy "$INSTALL_APPIMAGE_DIR"/usr/share/applications/*.desktop
cp ~/bin/AppImageUpdate.AppImage "$INSTALL_APPIMAGE_DIR"/usr/bin/
# FIXME(@getchoo): Validate AppStream information when https://github.com/probonopd/go-appimage/pull/379 is merged
mkappimage \
--no-appstream \
--updateinformation "gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage.zsync" \
"$INSTALL_APPIMAGE_DIR" \
"PrismLauncher-Linux-$VERSION-${{ inputs.build-type }}-$APPIMAGE_ARCH.AppImage"
- name: Package portable tarball
shell: bash
@ -128,4 +109,4 @@ runs:
uses: actions/upload-artifact@v5
with:
name: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync
path: PrismLauncher-${{ runner.os }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync
path: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync

View file

@ -56,7 +56,7 @@ runs:
# TODO(@getchoo): Get this working on MSYS2!
- name: Setup ccache
if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }}
uses: hendrikmuhs/ccache-action@v1.2.19
uses: hendrikmuhs/ccache-action@v1.2.20
with:
variant: sccache
create-symlink: ${{ runner.os != 'Windows' }}
@ -79,5 +79,5 @@ runs:
aqtversion: "==3.1.*"
version: ${{ inputs.qt-version }}
arch: ${{ inputs.qt-architecture }}
modules: qt5compat qtimageformats qtnetworkauth
modules: qtimageformats qtnetworkauth
cache: ${{ inputs.build-type == 'Debug' }}

View file

@ -10,26 +10,45 @@ runs:
sudo apt-get -y update
sudo apt-get -y install \
dpkg-dev \
ninja-build extra-cmake-modules scdoc \
libqrencode-dev \
appstream libxcb-cursor-dev
ninja-build extra-cmake-modules pkg-config scdoc \
cmark gamemode-dev libarchive-dev libcmark-dev libqrencode-dev zlib1g-dev \
libxcb-cursor-dev
# TODO(@getchoo): Install with the above when all targets use Ubuntu 24.04
- name: Install tomlplusplus
if: ${{ runner.arch == 'ARM64' }}
shell: bash
run: |
sudo apt-get -y install libtomlplusplus-dev
# FIXME(@getchoo): THIS IS HORRIBLE TO DO!
# Install tomlplusplus from Ubuntu 24.04, since it never got backported to 22.04
# I've done too much to continue keeping this as a submodule....
- name: Install tomlplusplus from 24.04
if: ${{ runner.arch != 'ARM64' }}
shell: bash
run: |
deb_arch="$(dpkg-architecture -q DEB_HOST_ARCH)"
curl -Lo libtomlplusplus-dev.deb http://mirrors.kernel.org/ubuntu/pool/universe/t/tomlplusplus/libtomlplusplus-dev_3.4.0+ds-0.2build1_"$deb_arch".deb
curl -Lo libtomlplusplus3t64.deb http://mirrors.kernel.org/ubuntu/pool/universe/t/tomlplusplus/libtomlplusplus3t64_3.4.0+ds-0.2build1_"$deb_arch".deb
sudo dpkg -i libtomlplusplus3t64.deb
sudo dpkg -i libtomlplusplus-dev.deb
rm *.deb
sudo apt-get install -f
- name: Setup AppImage tooling
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
declare -A appimage_deps
deb_arch="$(dpkg-architecture -q DEB_HOST_ARCH)"
case "$deb_arch" in
# Determinate AppImage architecture to use
dpkg_arch="$(dpkg-architecture -q DEB_HOST_ARCH_CPU)"
case "$dpkg_arch" in
"amd64")
appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"]="4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage"
appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"]="15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage"
appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"]="f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage"
APPIMAGE_ARCH="x86_64"
;;
"arm64")
appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-aarch64.AppImage"]="06706ac8189797dccd36bd384105892cb5e6e71f784f4df526cc958adc223cd6 linuxdeploy-aarch64.AppImage"
appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-aarch64.AppImage"]="bf1c24aff6d749b5cf423afad6f15abd4440f81dec1aab95706b25f6667cdcf1 linuxdeploy-plugin-qt-aarch64.AppImage"
appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-aarch64.AppImage"]="cf27f810dfe5eda41f130769e4a4b562b9d93665371c15ebeffb84ee06a41550 AppImageUpdate-aarch64.AppImage"
APPIMAGE_ARCH="aarch64"
;;
*)
echo "# 🚨 The Debian architecture \"$deb_arch\" is not recognized!" >> "$GITHUB_STEP_SUMMARY"
@ -37,9 +56,19 @@ runs:
;;
esac
for url in "${!appimage_deps[@]}"; do
curl -LO "$url"
sha256sum -c - <<< "${appimage_deps[$url]}"
done
gh release download continuous \
--repo probonopd/go-appimage \
--pattern "appimagetool-*-$APPIMAGE_ARCH.AppImage" \
--output ~/bin/appimagetool
gh release download continuous \
--repo probonopd/go-appimage \
--pattern "mkappimage-*-$APPIMAGE_ARCH.AppImage" \
--output ~/bin/mkappimage
chmod +x ~/bin/appimagetool ~/bin/mkappimage
echo "$HOME/bin" >> "$GITHUB_PATH"
sudo apt -y install libopengl0
gh release download \
--repo AppImageCommunity/AppImageUpdate \
--pattern "AppImageUpdate-$APPIMAGE_ARCH.AppImage" \
--output ~/bin/AppImageUpdate.AppImage
chmod +x ~/bin/AppImageUpdate.AppImage

View file

@ -76,12 +76,11 @@ runs:
qt6-base:p
qt6-svg:p
qt6-imageformats:p
qt6-5compat:p
qt6-networkauth:p
cmark:p
qrencode:p
tomlplusplus:p
quazip-qt6:p
libarchive:p
- name: List pacman packages (MinGW)
if: ${{ inputs.msystem != '' }}

View file

@ -21,7 +21,7 @@ jobs:
if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs

View file

@ -139,7 +139,7 @@ jobs:
##
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: true

View file

@ -60,7 +60,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: "true"

View file

@ -84,7 +84,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: true

View file

@ -92,7 +92,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install Nix
uses: cachix/install-nix-action@v31

View file

@ -20,7 +20,7 @@ jobs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: "true"
path: "PrismLauncher-source"

View file

@ -16,10 +16,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: DeterminateSystems/update-flake-lock@v27
- uses: DeterminateSystems/update-flake-lock@v28
with:
commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile"

15
.gitmodules vendored
View file

@ -1,21 +1,6 @@
[submodule "libraries/quazip"]
path = libraries/quazip
url = https://github.com/stachenov/quazip.git
[submodule "libraries/tomlplusplus"]
path = libraries/tomlplusplus
url = https://github.com/marzer/tomlplusplus.git
[submodule "libraries/libnbtplusplus"]
path = libraries/libnbtplusplus
url = https://github.com/PrismLauncher/libnbtplusplus.git
[submodule "libraries/zlib"]
path = libraries/zlib
url = https://github.com/madler/zlib.git
[submodule "libraries/extra-cmake-modules"]
path = libraries/extra-cmake-modules
url = https://github.com/KDE/extra-cmake-modules
[submodule "libraries/cmark"]
path = libraries/cmark
url = https://github.com/commonmark/cmark.git
[submodule "flatpak/shared-modules"]
path = flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git

View file

@ -1,2 +1 @@
libraries/nbtplusplus
libraries/quazip

View file

@ -104,9 +104,6 @@ endif()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_WARN_DEPRECATED_UP_TO=0x060200")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_UP_TO=0x060000")
# Fix aarch64 build for toml++
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
# set CXXFLAGS for build targets
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}")
@ -174,20 +171,9 @@ endif()
option(BUILD_TESTING "Build the testing tree." ON)
find_package(ECM QUIET NO_MODULE)
if(NOT ECM_FOUND)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/extra-cmake-modules/CMakeLists.txt")
message(STATUS "Using bundled ECM")
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/libraries/extra-cmake-modules/modules;${CMAKE_MODULE_PATH}")
else()
message(FATAL_ERROR
" Could not find ECM\n \n"
" Either install ECM using the system package manager or clone submodules\n"
" Submodules can be cloned with 'git submodule update --init --recursive'")
endif()
else()
find_package(ECM NO_MODULE REQUIRED)
set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}")
endif()
include(CTest)
include(ECMAddTests)
if(BUILD_TESTING)
@ -244,7 +230,6 @@ set(Launcher_DISCORD_URL "https://prismlauncher.org/discord" CACHE STRING "URL f
set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL for the subreddit.")
# Builds
set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules")
set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against")
# Java downloader
@ -309,32 +294,12 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}")
################################ 3rd Party Libs ################################
# Successive configurations of cmake without cleaning the build dir will cause zlib fallback to fail due to cached values
# Record when fallback triggered and skip this find_package
if(NOT Launcher_FORCE_BUNDLED_LIBS AND NOT FORCE_BUNDLED_ZLIB)
find_package(ZLIB QUIET)
endif()
if(NOT ZLIB_FOUND)
set(FORCE_BUNDLED_ZLIB TRUE CACHE BOOL "")
mark_as_advanced(FORCE_BUNDLED_ZLIB)
endif()
# Find the required Qt parts
include(QtVersionlessBackport)
if(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 6)
find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth OpenGL)
find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml NetworkAuth OpenGL)
find_package(Qt6 COMPONENTS DBus)
list(APPEND Launcher_QT_DBUS Qt6::DBus)
list(APPEND Launcher_QT_LIBS Qt6::Core5Compat)
if(NOT Launcher_FORCE_BUNDLED_LIBS)
find_package(QuaZip-Qt6 1.3 QUIET)
endif()
if (NOT QuaZip-Qt6_FOUND)
set(QUAZIP_QT_MAJOR_VERSION ${QT_VERSION_MAJOR} CACHE STRING "Qt version to use (4, 5 or 6), defaults to ${QT_VERSION_MAJOR}" FORCE)
set(FORCE_BUNDLED_QUAZIP 1)
endif()
else()
message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported")
endif()
@ -345,6 +310,13 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS})
endif()
find_package(cmark REQUIRED)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(PkgConfig REQUIRED)
pkg_check_modules(gamemode REQUIRED IMPORTED_TARGET gamemode)
endif()
# Find libqrencode
## NOTE(@getchoo): Never use pkg-config with MSVC since the vcpkg port makes our install bundle fail to find the dll
if(MSVC)
@ -357,21 +329,23 @@ else()
pkg_check_modules(libqrencode REQUIRED IMPORTED_TARGET libqrencode)
endif()
if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find toml++
find_package(tomlplusplus 3.2.0 QUIET)
# Fallback to pkg-config (if available) if CMake files aren't found
# Find libarchive through CMake packages, mainly for vcpkg
find_package(LibArchive)
# CMake packages aren't available in most distributions of libarchive, so fallback to pkg-config
if(NOT LibArchive_FOUND)
find_package(PkgConfig REQUIRED)
pkg_check_modules(libarchive REQUIRED IMPORTED_TARGET libarchive)
endif()
find_package(tomlplusplus 3.2.0 REQUIRED)
# fallback to pkgconfig, important especially as many distros package toml++ built with meson
if(NOT tomlplusplus_FOUND)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(tomlplusplus IMPORTED_TARGET tomlplusplus>=3.2.0)
endif()
find_package(PkgConfig REQUIRED)
pkg_check_modules(tomlplusplus REQUIRED IMPORTED_TARGET tomlplusplus>=3.2.0)
endif()
find_package(ZLIB REQUIRED)
# Find cmark
find_package(cmark QUIET)
endif()
include(ECMQtDeclareLoggingCategory)
@ -438,6 +412,7 @@ elseif(UNIX)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_PNG_256} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/256x256/apps" RENAME "${Launcher_AppID}.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR})
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}")
@ -478,60 +453,9 @@ add_subdirectory(libraries/libnbtplusplus)
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker
if(FORCE_BUNDLED_ZLIB)
message(STATUS "Using bundled zlib")
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib
set(SKIP_INSTALL_ALL ON)
add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL)
# On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not.
# We cannot safely undo the rename on those systems, and they generally have packages for zlib anyway.
check_include_file(unistd.h NEED_GENERATED_ZCONF)
if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" AND NOT NEED_GENERATED_ZCONF)
# zlib's cmake script renames a file, dirtying the submodule, see https://github.com/madler/zlib/issues/162
message(STATUS "Undoing Rename")
message(STATUS " ${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h")
file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h")
endif()
set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" CACHE STRING "" FORCE)
set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}")
add_library(ZLIB::ZLIB ALIAS zlibstatic)
set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name")
find_package(ZLIB REQUIRED)
else()
message(STATUS "Using system zlib")
endif()
if (FORCE_BUNDLED_QUAZIP)
message(STATUS "Using bundled QuaZip")
set(BUILD_SHARED_LIBS 0) # link statically to avoid conflicts.
set(QUAZIP_INSTALL 0)
add_subdirectory(libraries/quazip) # zip manipulation library
else()
message(STATUS "Using system QuaZip")
endif()
add_subdirectory(libraries/rainbow) # Qt extension for colors
add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions
if(NOT tomlplusplus_FOUND)
message(STATUS "Using bundled tomlplusplus")
add_subdirectory(libraries/tomlplusplus) # toml parser
else()
message(STATUS "Using system tomlplusplus")
endif()
if(NOT cmark_FOUND)
message(STATUS "Using bundled cmark")
set(ORIGINAL_BUILD_TESTING ${BUILD_TESTING})
set(BUILD_TESTING 0)
set(BUILD_SHARED_LIBS 0)
add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser
add_library(cmark::cmark ALIAS cmark)
set(BUILD_TESTING ${ORIGINAL_BUILD_TESTING})
else()
message(STATUS "Using system cmark")
endif()
add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
add_subdirectory(libraries/qdcss) # css parser

View file

@ -212,30 +212,6 @@
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
## Quazip
Copyright (C) 2005-2021 Sergey A. Tachenov
The QuaZip library is licensed under the GNU Lesser General Public
License V2.1 plus a static linking exception.
The original ZIP/UNZIP package (MiniZip) is copyrighted by Gilles
Vollant and contributors, see quazip/(un)zip.h files for details.
Basically it's the zlib license.
STATIC LINKING EXCEPTION
The copyright holders give you permission to link this library with
independent modules to produce an executable, regardless of the license
terms of these independent modules, and to copy and distribute the
resulting executable under terms of your choice, provided that you also
meet, for each linked independent module, the terms and conditions of
the license of that module. An independent module is a module which is
not derived from or based on this library. If you modify this library,
you must extend this exception to your version of the library.
See COPYING file for the full LGPL text.
## launcher (`libraries/launcher`)
PolyMC - Minecraft Launcher

View file

@ -1,100 +0,0 @@
# SPDX-FileCopyrightText: 2014 Rohan Garg <rohan16garg@gmail.com>
# SPDX-FileCopyrightText: 2014 Alex Merry <alex.merry@kde.org>
# SPDX-FileCopyrightText: 2014-2016 Aleix Pol <aleixpol@kde.org>
# SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau <kossebau@kde.org>
# SPDX-FileCopyrightText: 2022 Ahmad Samir <a.samir78@gmail.com>
#
# SPDX-License-Identifier: BSD-3-Clause
#[=======================================================================[.rst:
ECMQueryQt
---------------
This module can be used to query the installation paths used by Qt.
For Qt5 this uses ``qmake``, and for Qt6 this used ``qtpaths`` (the latter has built-in
support to query the paths of a target platform when cross-compiling).
This module defines the following function:
::
ecm_query_qt(<result_variable> <qt_variable> [TRY])
Passing ``TRY`` will result in the method not making the build fail if the executable
used for querying has not been found, but instead simply print a warning message and
return an empty string.
Example usage:
.. code-block:: cmake
include(ECMQueryQt)
ecm_query_qt(bin_dir QT_INSTALL_BINS)
If the call succeeds ``${bin_dir}`` will be set to ``<prefix>/path/to/bin/dir`` (e.g.
``/usr/lib64/qt/bin/``).
Since: 5.93
#]=======================================================================]
include(${CMAKE_CURRENT_LIST_DIR}/QtVersionOption.cmake)
include(CheckLanguage)
check_language(CXX)
if (CMAKE_CXX_COMPILER)
# Enable the CXX language to let CMake look for config files in library dirs.
# See: https://gitlab.kitware.com/cmake/cmake/-/issues/23266
enable_language(CXX)
endif()
if (QT_MAJOR_VERSION STREQUAL "5")
# QUIET to accommodate the TRY option
find_package(Qt${QT_MAJOR_VERSION}Core QUIET)
if(TARGET Qt5::qmake)
get_target_property(_qmake_executable_default Qt5::qmake LOCATION)
set(QUERY_EXECUTABLE ${_qmake_executable_default}
CACHE FILEPATH "Location of the Qt5 qmake executable")
set(_exec_name_text "Qt5 qmake")
set(_cli_option "-query")
endif()
elseif(QT_MAJOR_VERSION STREQUAL "6")
# QUIET to accommodate the TRY option
find_package(Qt6 COMPONENTS CoreTools QUIET CONFIG)
if (TARGET Qt6::qtpaths)
get_target_property(_qtpaths_executable Qt6::qtpaths LOCATION)
set(QUERY_EXECUTABLE ${_qtpaths_executable}
CACHE FILEPATH "Location of the Qt6 qtpaths executable")
set(_exec_name_text "Qt6 qtpaths")
set(_cli_option "--query")
endif()
endif()
function(ecm_query_qt result_variable qt_variable)
set(options TRY)
set(oneValueArgs)
set(multiValueArgs)
cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT QUERY_EXECUTABLE)
if(ARGS_TRY)
set(${result_variable} "" PARENT_SCOPE)
message(STATUS "No ${_exec_name_text} executable found. Can't check ${qt_variable}")
return()
else()
message(FATAL_ERROR "No ${_exec_name_text} executable found. Can't check ${qt_variable} as required")
endif()
endif()
execute_process(
COMMAND ${QUERY_EXECUTABLE} ${_cli_option} "${qt_variable}"
RESULT_VARIABLE return_code
OUTPUT_VARIABLE output
)
if(return_code EQUAL 0)
string(STRIP "${output}" output)
file(TO_CMAKE_PATH "${output}" output_path)
set(${result_variable} "${output_path}" PARENT_SCOPE)
else()
message(WARNING "Failed call: ${_command} \"${qt_variable}\"")
message(FATAL_ERROR "${_exec_name_text} call failed: ${return_code}")
endif()
endfunction()

View file

@ -1,38 +0,0 @@
#.rst:
# QtVersionOption
# ---------------
#
# Adds a build option to select the major Qt version if necessary,
# that is, if the major Qt version has not yet been determined otherwise
# (e.g. by a corresponding find_package() call).
#
# This module is typically included by other modules requiring knowledge
# about the major Qt version.
#
# ``QT_MAJOR_VERSION`` is defined to either be "5" or "6".
#
#
# Since 5.82.0.
#=============================================================================
# SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
#
# SPDX-License-Identifier: BSD-3-Clause
if (DEFINED QT_MAJOR_VERSION)
return()
endif()
if (TARGET Qt5::Core)
set(QT_MAJOR_VERSION 5)
elseif (TARGET Qt6::Core)
set(QT_MAJOR_VERSION 6)
else()
option(BUILD_WITH_QT6 "Build against Qt 6" OFF)
if (BUILD_WITH_QT6)
set(QT_MAJOR_VERSION 6)
else()
set(QT_MAJOR_VERSION 5)
endif()
endif()

View file

@ -1,97 +0,0 @@
#=============================================================================
# Copyright 2005-2011 Kitware, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# * Neither the name of Kitware, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#=============================================================================
# From Qt5CoreMacros.cmake
function(qt_generate_moc)
if(QT_VERSION_MAJOR EQUAL 5)
qt5_generate_moc(${ARGV})
elseif(QT_VERSION_MAJOR EQUAL 6)
qt6_generate_moc(${ARGV})
endif()
endfunction()
function(qt_wrap_cpp outfiles)
if(QT_VERSION_MAJOR EQUAL 5)
qt5_wrap_cpp("${outfiles}" ${ARGN})
elseif(QT_VERSION_MAJOR EQUAL 6)
qt6_wrap_cpp("${outfiles}" ${ARGN})
endif()
set("${outfiles}" "${${outfiles}}" PARENT_SCOPE)
endfunction()
function(qt_add_binary_resources)
if(QT_VERSION_MAJOR EQUAL 5)
qt5_add_binary_resources(${ARGV})
elseif(QT_VERSION_MAJOR EQUAL 6)
qt6_add_binary_resources(${ARGV})
endif()
endfunction()
function(qt_add_resources outfiles)
if(QT_VERSION_MAJOR EQUAL 5)
qt5_add_resources("${outfiles}" ${ARGN})
elseif(QT_VERSION_MAJOR EQUAL 6)
qt6_add_resources("${outfiles}" ${ARGN})
endif()
set("${outfiles}" "${${outfiles}}" PARENT_SCOPE)
endfunction()
function(qt_add_big_resources outfiles)
if(QT_VERSION_MAJOR EQUAL 5)
qt5_add_big_resources(${outfiles} ${ARGN})
elseif(QT_VERSION_MAJOR EQUAL 6)
qt6_add_big_resources(${outfiles} ${ARGN})
endif()
set("${outfiles}" "${${outfiles}}" PARENT_SCOPE)
endfunction()
function(qt_import_plugins)
if(QT_VERSION_MAJOR EQUAL 5)
qt5_import_plugins(${ARGV})
elseif(QT_VERSION_MAJOR EQUAL 6)
qt6_import_plugins(${ARGV})
endif()
endfunction()
# From Qt5WidgetsMacros.cmake
function(qt_wrap_ui outfiles)
if(QT_VERSION_MAJOR EQUAL 5)
qt5_wrap_ui("${outfiles}" ${ARGN})
elseif(QT_VERSION_MAJOR EQUAL 6)
qt6_wrap_ui("${outfiles}" ${ARGN})
endif()
set("${outfiles}" "${${outfiles}}" PARENT_SCOPE)
endfunction()

6
flake.lock generated
View file

@ -18,11 +18,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1762977756,
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
"lastModified": 1764242076,
"narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
"type": "github"
},
"original": {

14
flatpak/cmark.yml Normal file
View file

@ -0,0 +1,14 @@
name: cmark
buildsystem: cmake-ninja
builddir: true
config-opts:
- -DCMAKE_TESTS=OFF
sources:
- type: archive
url: https://github.com/commonmark/cmark/archive/0.31.1.tar.gz
sha256: 3da93db5469c30588cfeb283d9d62edfc6ded9eb0edc10a4f5bbfb7d722ea802
x-checker-data:
type: anitya
project-id: 9159
stable-only: true
url-template: https://github.com/commonmark/cmark/archive/$version.tar.gz

View file

@ -27,6 +27,9 @@ finish-args:
- --filesystem=/sys/kernel/mm/transparent_hugepage:ro
modules:
- cmark.yml
- tomlplusplus.yml
# Might be needed by some Controller mods (see https://github.com/isXander/Controlify/issues/31)
- shared-modules/libusb/libusb.json

6
flatpak/tomlplusplus.yml Normal file
View file

@ -0,0 +1,6 @@
name: tomlplusplus
buildsystem: cmake-ninja
sources:
- type: archive
url: https://github.com/marzer/tomlplusplus/archive/v3.4.0.tar.gz
sha256: 8517f65938a4faae9ccf8ebb36631a38c1cadfb5efa85d9a72e15b9e97d25155

View file

@ -370,7 +370,25 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
}
QString origcwdPath = QDir::currentPath();
#if defined(Q_OS_LINUX)
const QString binFilePath = applicationFilePath();
const bool isAppImage = binFilePath.startsWith("/tmp/.mount_");
// Yes, this can technically trigger the logic below if someone makes an AppImage with an actual launcher exe named "ld-linux"
// Please don't :)
const bool executedFromLinker = QFileInfo(binFilePath).fileName().startsWith("ld-linux");
// NOTE(@getchoo): In order for `go-appimage` to generate self-contained AppImages, it executes apps from a bundled linker at
// <root>/lib64
// This is not the path to our actual binary, which we want
QString binPath;
if (isAppImage && executedFromLinker) {
binPath = FS::PathCombine(applicationDirPath(), "../usr/bin");
} else {
binPath = applicationDirPath();
}
#else
QString binPath = applicationDirPath();
#endif
{
// Root path is used for updates and portable data

View file

@ -26,8 +26,14 @@ set(CORE_SOURCES
NullInstance.h
MMCZip.h
MMCZip.cpp
Untar.h
Untar.cpp
archive/ArchiveReader.cpp
archive/ArchiveReader.h
archive/ArchiveWriter.cpp
archive/ArchiveWriter.h
archive/ExportToZipTask.cpp
archive/ExportToZipTask.h
archive/ExtractZipTask.cpp
archive/ExtractZipTask.h
StringUtils.h
StringUtils.cpp
QVariantUtils.h
@ -615,6 +621,10 @@ set(PRISMUPDATER_SOURCES
# Zip
MMCZip.h
MMCZip.cpp
archive/ArchiveReader.cpp
archive/ArchiveReader.h
archive/ArchiveWriter.cpp
archive/ArchiveWriter.h
# Time
MMCTime.h
@ -1319,8 +1329,13 @@ if(TARGET PkgConfig::tomlplusplus)
else()
target_link_libraries(Launcher_logic tomlplusplus::tomlplusplus)
endif()
if(TARGET PkgConfig::libarchive)
target_link_libraries(Launcher_logic PkgConfig::libarchive)
else()
target_link_libraries(Launcher_logic LibArchive::LibArchive)
endif()
if (UNIX AND NOT CYGWIN AND NOT APPLE)
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_libraries(Launcher_logic
gamemode
)
@ -1339,7 +1354,6 @@ target_link_libraries(Launcher_logic
${Launcher_QT_LIBS}
)
target_link_libraries(Launcher_logic
QuaZip::QuaZip
cmark::cmark
LocalPeer
Launcher_rainbow
@ -1406,7 +1420,6 @@ if(Launcher_BUILD_UPDATER)
add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI})
target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(prism_updater_logic
QuaZip::QuaZip
${ZLIB_LIBRARIES}
BuildConfig
Qt${QT_VERSION_MAJOR}::Widgets
@ -1415,6 +1428,11 @@ if(Launcher_BUILD_UPDATER)
${Launcher_QT_LIBS}
cmark::cmark
)
if(TARGET PkgConfig::libarchive)
target_link_libraries(prism_updater_logic PkgConfig::libarchive)
else()
target_link_libraries(prism_updater_logic LibArchive::LibArchive)
endif()
add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp)
target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest)

View file

@ -1702,4 +1702,14 @@ QString getUniqueResourceName(const QString& filePath)
return newFileName;
}
bool removeFiles(QStringList listFile)
{
bool ret = true;
// For each file
for (int i = 0; i < listFile.count(); i++) {
// Remove
ret = ret && QFile::remove(listFile.at(i));
}
return ret;
}
} // namespace FS

View file

@ -291,6 +291,8 @@ bool move(const QString& source, const QString& dest);
*/
bool deletePath(QString path);
bool removeFiles(QStringList listFile);
/**
* Trash a folder / file
*/

View file

@ -38,10 +38,11 @@
#include "Application.h"
#include "FileSystem.h"
#include "MMCZip.h"
#include "NullInstance.h"
#include "QObjectPtr.h"
#include "archive/ArchiveReader.h"
#include "archive/ExtractZipTask.h"
#include "icons/IconList.h"
#include "icons/IconUtils.h"
@ -54,12 +55,10 @@
#include "net/ApiDownload.h"
#include <QFileInfo>
#include <QtConcurrentRun>
#include <algorithm>
#include <memory>
#include <quazip/quazipdir.h>
InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap<QString, QString>&& extra_info)
: m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent)
{}
@ -109,38 +108,34 @@ void InstanceImportTask::downloadFromUrl()
filesNetJob->start();
}
QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root)
QString InstanceImportTask::getRootFromZip(QStringList files)
{
if (!isRunning()) {
return {};
}
QuaZipDir rootDir(zip, root);
for (auto&& fileName : rootDir.entryList(QDir::Files)) {
auto cleanPath = [](QString path) {
if (path == ".")
return QString();
QString result = path;
if (result.startsWith("./"))
result = result.mid(2);
return result;
};
for (auto&& fileName : files) {
setDetails(fileName);
if (fileName == "instance.cfg") {
QFileInfo fileInfo(fileName);
if (fileInfo.fileName() == "instance.cfg") {
qDebug() << "MultiMC:" << true;
m_modpackType = ModpackType::MultiMC;
return root;
return cleanPath(fileInfo.path());
}
if (fileName == "manifest.json") {
if (fileInfo.fileName() == "manifest.json") {
qDebug() << "Flame:" << true;
m_modpackType = ModpackType::Flame;
return root;
return cleanPath(fileInfo.path());
}
QCoreApplication::processEvents();
}
// Recurse the search to non-ignored subfolders
for (auto&& fileName : rootDir.entryList(QDir::Dirs)) {
if ("overrides/" == fileName)
continue;
QString result = getRootFromZip(zip, root + fileName);
if (!result.isEmpty())
return result;
}
return {};
}
@ -151,13 +146,12 @@ void InstanceImportTask::processZipPack()
qDebug() << "Attempting to create instance from" << m_archivePath;
// open the zip and find relevant files in it
auto packZip = std::make_shared<QuaZip>(m_archivePath);
if (!packZip->open(QuaZip::mdUnzip)) {
MMCZip::ArchiveReader packZip(m_archivePath);
if (!packZip.collectFiles()) {
emitFailed(tr("Unable to open supplied modpack zip file."));
return;
}
QuaZipDir packZipDir(packZip.get());
qDebug() << "Attempting to determine instance type";
QString root;
@ -165,18 +159,18 @@ void InstanceImportTask::processZipPack()
// NOTE: Prioritize modpack platforms that aren't searched for recursively.
// Especially Flame has a very common filename for its manifest, which may appear inside overrides for example
// https://docs.modrinth.com/docs/modpacks/format_definition/#storage
if (packZipDir.exists("/modrinth.index.json")) {
if (packZip.exists("/modrinth.index.json")) {
// process as Modrinth pack
qDebug() << "Modrinth:" << true;
m_modpackType = ModpackType::Modrinth;
} else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) {
} else if (packZip.exists("/bin/modpack.jar") || packZip.exists("/bin/version.json")) {
// process as Technic pack
qDebug() << "Technic:" << true;
extractDir.mkpath("minecraft");
extractDir.cd("minecraft");
m_modpackType = ModpackType::Technic;
} else {
root = getRootFromZip(packZip.get());
root = getRootFromZip(packZip.getFiles());
setDetails("");
}
if (m_modpackType == ModpackType::Unknown) {
@ -186,7 +180,7 @@ void InstanceImportTask::processZipPack()
setStatus(tr("Extracting modpack"));
// make sure we extract just the pack
auto zipTask = makeShared<MMCZip::ExtractZipTask>(packZip, extractDir, root);
auto zipTask = makeShared<MMCZip::ExtractZipTask>(m_archivePath, extractDir, root);
auto progressStep = std::make_shared<TaskStepProgress>();
connect(zipTask.get(), &Task::finished, this, [this, progressStep] {

View file

@ -40,8 +40,6 @@
#include <QUrl>
#include "InstanceTask.h"
class QuaZip;
class InstanceImportTask : public InstanceTask {
Q_OBJECT
public:
@ -58,7 +56,7 @@ class InstanceImportTask : public InstanceTask {
void processTechnic();
void processFlame();
void processModrinth();
QString getRootFromZip(QuaZip* zip, const QString& root = "");
QString getRootFromZip(QStringList files);
private slots:
void processZipPack();

View file

@ -148,8 +148,12 @@ bool LaunchController::askPlayDemo()
return box.clickedButton() == demoButton;
}
QString LaunchController::askOfflineName(QString playerName, bool demo, bool& ok)
QString LaunchController::askOfflineName(QString playerName, bool demo, bool* ok)
{
if (ok != nullptr) {
*ok = false;
}
// we ask the user for a player name
QString message = tr("Choose your offline mode player name.");
if (demo) {
@ -166,9 +170,12 @@ QString LaunchController::askOfflineName(QString playerName, bool demo, bool& ok
return {};
}
if (const QString name = dialog.getUsername(); !name.isEmpty()) {
const QString name = dialog.getUsername();
usedname = name;
APPLICATION->settings()->set("LastOfflinePlayerName", usedname);
if (ok != nullptr) {
*ok = true;
}
return usedname;
}
@ -185,7 +192,7 @@ void LaunchController::login()
if (m_demo) {
// we ask the user for a player name
bool ok = false;
auto name = askOfflineName("Player", m_demo, ok);
auto name = askOfflineName("Player", m_demo, &ok);
if (ok) {
m_session = std::make_shared<AuthSession>();
static const QRegularExpression s_removeChars("[{}-]");
@ -264,7 +271,7 @@ void LaunchController::login()
bool ok = false;
QString name;
if (m_offlineName.isEmpty()) {
name = askOfflineName(m_session->player_name, m_session->demo, ok);
name = askOfflineName(m_session->player_name, m_session->demo, &ok);
if (!ok) {
tryagain = false;
break;

View file

@ -77,7 +77,7 @@ class LaunchController : public Task {
void launchInstance();
void decideAccount();
bool askPlayDemo();
QString askOfflineName(QString playerName, bool demo, bool& ok);
QString askOfflineName(QString playerName, bool demo, bool* ok = nullptr);
bool reauthenticateAccount(MinecraftAccountPtr account);
private slots:

View file

@ -36,10 +36,10 @@
#include "LoggedProcess.h"
#include <QDebug>
#include <QTextDecoder>
#include <QStringDecoder>
#include "MessageLevel.h"
LoggedProcess::LoggedProcess(const QTextCodec* output_codec, QObject* parent)
LoggedProcess::LoggedProcess(const QStringConverter::Encoding output_codec, QObject* parent)
: QProcess(parent), m_err_decoder(output_codec), m_out_decoder(output_codec)
{
// QProcess has a strange interface... let's map a lot of those into a few.
@ -57,9 +57,9 @@ LoggedProcess::~LoggedProcess()
}
}
QStringList LoggedProcess::reprocess(const QByteArray& data, QTextDecoder& decoder)
QStringList LoggedProcess::reprocess(const QByteArray& data, QStringDecoder& decoder)
{
auto str = decoder.toUnicode(data);
QString str = decoder(data);
if (!m_leftover_line.isEmpty()) {
str.prepend(m_leftover_line);

View file

@ -36,7 +36,7 @@
#pragma once
#include <QProcess>
#include <QTextDecoder>
#include <QStringDecoder>
#include "MessageLevel.h"
/*
@ -49,7 +49,7 @@ class LoggedProcess : public QProcess {
enum State { NotRunning, Starting, FailedToStart, Running, Finished, Crashed, Aborted };
public:
explicit LoggedProcess(const QTextCodec* output_codec = QTextCodec::codecForLocale(), QObject* parent = 0);
explicit LoggedProcess(QStringConverter::Encoding outputEncoding = QStringConverter::System, QObject* parent = nullptr);
virtual ~LoggedProcess();
State state() const;
@ -77,11 +77,11 @@ class LoggedProcess : public QProcess {
private:
void changeState(LoggedProcess::State state);
QStringList reprocess(const QByteArray& data, QTextDecoder& decoder);
QStringList reprocess(const QByteArray& data, QStringDecoder& decoder);
private:
QTextDecoder m_err_decoder;
QTextDecoder m_out_decoder;
QStringDecoder m_err_decoder;
QStringDecoder m_out_decoder;
QString m_leftover_line;
bool m_killed = false;
State m_state = NotRunning;

View file

@ -35,66 +35,46 @@
*/
#include "MMCZip.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <archive.h>
#include "FileSystem.h"
#include "archive/ArchiveReader.h"
#include "archive/ArchiveWriter.h"
#include <QCoreApplication>
#include <QDebug>
#include <QFileInfo>
#include <QUrl>
#if defined(LAUNCHER_APPLICATION)
#include <QtConcurrentRun>
#endif
#include <memory>
namespace MMCZip {
// ours
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const Filter& filter)
using FilterFunction = std::function<bool(const QString&)>;
#if defined(LAUNCHER_APPLICATION)
bool mergeZipFiles(ArchiveWriter& into, QFileInfo from, QSet<QString>& contained, const FilterFunction& filter = nullptr)
{
QuaZip modZip(from.filePath());
modZip.open(QuaZip::mdUnzip);
QuaZipFile fileInsideMod(&modZip);
QuaZipFile zipOutFile(into);
for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) {
QString filename = modZip.getCurrentFileName();
ArchiveReader r(from.absoluteFilePath());
return r.parse([&into, &contained, &filter, from](ArchiveReader::File* f) {
auto filename = f->filename();
if (filter && !filter(filename)) {
qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered";
continue;
f->skip();
return true;
}
if (contained.contains(filename)) {
qDebug() << "Skipping already contained file " << filename << " from " << from.fileName();
continue;
f->skip();
return true;
}
contained.insert(filename);
if (!fileInsideMod.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open " << filename << " from " << from.fileName();
return false;
}
QuaZipNewInfo info_out(fileInsideMod.getActualFileName());
if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) {
qCritical() << "Failed to open " << filename << " in the jar";
fileInsideMod.close();
return false;
}
if (!JlCompress::copyData(fileInsideMod, zipOutFile)) {
zipOutFile.close();
fileInsideMod.close();
if (!into.addFile(f)) {
qCritical() << "Failed to copy data of " << filename << " into the jar";
return false;
}
zipOutFile.close();
fileInsideMod.close();
}
return true;
});
}
bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks)
bool compressDirFiles(ArchiveWriter& zip, QString dir, QFileInfoList files)
{
QDir directory(dir);
if (!directory.exists())
@ -103,48 +83,18 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow
for (auto e : files) {
auto filePath = directory.relativeFilePath(e.absoluteFilePath());
auto srcPath = e.absoluteFilePath();
if (followSymlinks) {
if (e.isSymLink()) {
srcPath = e.symLinkTarget();
} else {
srcPath = e.canonicalFilePath();
}
}
if (!JlCompress::compressFile(zip, srcPath, filePath))
if (!zip.addFile(srcPath, filePath))
return false;
}
return true;
}
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
{
QuaZip zip(fileCompressed);
zip.setUtf8Enabled(true);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
if (!zip.open(QuaZip::mdCreate)) {
FS::deletePath(fileCompressed);
return false;
}
auto result = compressDirFiles(&zip, dir, files, followSymlinks);
zip.close();
if (zip.getZipError() != 0) {
FS::deletePath(fileCompressed);
return false;
}
return result;
}
#if defined(LAUNCHER_APPLICATION)
// ours
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods)
{
QuaZip zipOut(targetJarPath);
zipOut.setUtf8Enabled(true);
if (!zipOut.open(QuaZip::mdCreate)) {
ArchiveWriter zipOut(targetJarPath);
if (!zipOut.open()) {
FS::deletePath(targetJarPath);
qCritical() << "Failed to open the minecraft.jar for modding";
return false;
@ -161,7 +111,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
if (!mod->enabled())
continue;
if (mod->type() == ResourceType::ZIPFILE) {
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) {
if (!mergeZipFiles(zipOut, mod->fileinfo(), addedFiles)) {
zipOut.close();
FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
@ -170,7 +120,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
} else if (mod->type() == ResourceType::SINGLEFILE) {
// FIXME: buggy - does not work with addedFiles
auto filename = mod->fileinfo();
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) {
if (!zipOut.addFile(filename.absoluteFilePath(), filename.fileName())) {
zipOut.close();
FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
@ -193,7 +143,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
files.removeAll(e);
}
if (!compressDirFiles(&zipOut, parent_dir, files)) {
if (!compressDirFiles(zipOut, parent_dir, files)) {
zipOut.close();
FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
@ -209,7 +159,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
}
}
if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) {
if (!mergeZipFiles(zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) {
zipOut.close();
FS::deletePath(targetJarPath);
qCritical() << "Failed to insert minecraft.jar contents.";
@ -217,8 +167,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
}
// Recompress the jar
zipOut.close();
if (zipOut.getZipError() != 0) {
if (!zipOut.close()) {
FS::deletePath(targetJarPath);
qCritical() << "Failed to finalize minecraft.jar!";
return false;
@ -228,70 +177,32 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
#endif
// ours
QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root)
{
QuaZipDir rootDir(zip, root);
for (auto&& fileName : rootDir.entryList(QDir::Files)) {
if (fileName == what)
return root;
QCoreApplication::processEvents();
}
// Recurse the search to non-ignored subfolders
for (auto&& fileName : rootDir.entryList(QDir::Dirs)) {
if (ignore_paths.contains(fileName))
continue;
QString result = findFolderOfFileInZip(zip, what, ignore_paths, root + fileName);
if (!result.isEmpty())
return result;
}
return {};
}
// ours
bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root)
{
QuaZipDir rootDir(zip, root);
for (auto fileName : rootDir.entryList(QDir::Files)) {
if (fileName == what) {
result.append(root);
return true;
}
}
for (auto fileName : rootDir.entryList(QDir::Dirs)) {
findFilesInZip(zip, what, result, root + fileName);
}
return !result.isEmpty();
}
// ours
std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, const QString& target)
std::optional<QStringList> extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target)
{
auto target_top_dir = QUrl::fromLocalFile(target);
QStringList extracted;
qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target;
auto numEntries = zip->getEntriesCount();
if (numEntries < 0) {
if (!zip->collectFiles()) {
qWarning() << "Failed to enumerate files in archive";
return std::nullopt;
} else if (numEntries == 0) {
}
if (zip->getFiles().isEmpty()) {
qDebug() << "Extracting empty archives seems odd...";
return extracted;
} else if (!zip->goToFirstFile()) {
qWarning() << "Failed to seek to first file in zip";
return std::nullopt;
}
do {
QString file_name = zip->getCurrentFileName();
auto extPtr = ArchiveWriter::createDiskWriter();
auto ext = extPtr.get();
if (!zip->parse([&subdir, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) {
QString file_name = f->filename();
file_name = FS::RemoveInvalidPathChars(file_name);
if (!file_name.startsWith(subdir))
continue;
if (!file_name.startsWith(subdir)) {
f->skip();
return true;
}
auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size()));
auto original_name = relative_file_name;
@ -308,7 +219,6 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
relative_file_name = relative_file_name.split('/').last();
}
QString target_file_path;
if (relative_file_name.isEmpty()) {
target_file_path = target + '/';
@ -321,101 +231,67 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path"
<< target;
return std::nullopt;
return false;
}
if (!JlCompress::extractFile(zip, "", target_file_path)) {
if (!f->writeFile(ext, target_file_path)) {
qWarning() << "Failed to extract file" << original_name << "to" << target_file_path;
JlCompress::removeFile(extracted);
return std::nullopt;
return false;
}
extracted.append(target_file_path);
auto fileInfo = QFileInfo(target_file_path);
if (fileInfo.isFile()) {
auto permissions = fileInfo.permissions();
auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser |
QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther;
auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
auto newPermisions = (permissions & maxPermisions) | minPermisions;
if (newPermisions != permissions) {
if (!QFile::setPermissions(target_file_path, newPermisions)) {
qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path));
}
}
} else if (fileInfo.isDir()) {
// Ensure the folder has the minimal required permissions
QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup |
QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther;
QFile::Permissions currentPermissions = fileInfo.permissions();
if ((currentPermissions & minimalPermissions) != minimalPermissions) {
if (!QFile::setPermissions(target_file_path, minimalPermissions)) {
qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path));
}
}
}
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (zip->goToNextFile());
return true;
})) {
qWarning() << "Failed to parse file" << zip->getZipName();
FS::removeFiles(extracted);
return std::nullopt;
}
return extracted;
}
// ours
bool extractRelFile(QuaZip* zip, const QString& file, const QString& target)
{
return JlCompress::extractFile(zip, file, target);
}
// ours
std::optional<QStringList> extractDir(QString fileCompressed, QString dir)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip)) {
// check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed);
if (fileInfo.size() == 22) {
return QStringList();
}
qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError();
;
return std::nullopt;
}
ArchiveReader zip(fileCompressed);
return extractSubDir(&zip, "", dir);
}
// ours
std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip)) {
// check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed);
if (fileInfo.size() == 22) {
return QStringList();
}
qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError();
;
return std::nullopt;
}
ArchiveReader zip(fileCompressed);
return extractSubDir(&zip, subdir, dir);
}
// ours
bool extractFile(QString fileCompressed, QString file, QString target)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip)) {
// check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed);
if (fileInfo.size() == 22) {
return true;
}
qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError();
ArchiveReader zip(fileCompressed);
auto f = zip.goToFile(file);
if (!f) {
return false;
}
return extractRelFile(&zip, file, target);
auto extPtr = ArchiveWriter::createDiskWriter();
auto ext = extPtr.get();
return f->writeFile(ext, target);
}
bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter)
@ -453,218 +329,4 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
}
return true;
}
#if defined(LAUNCHER_APPLICATION)
void ExportToZipTask::executeTask()
{
setStatus("Adding files...");
setProgress(0, m_files.length());
m_build_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); });
connect(&m_build_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExportToZipTask::finish);
m_build_zip_watcher.setFuture(m_build_zip_future);
}
auto ExportToZipTask::exportZip() -> ZipResult
{
if (!m_dir.exists()) {
return ZipResult(tr("Folder doesn't exist"));
}
if (!m_output.isOpen() && !m_output.open(QuaZip::mdCreate)) {
return ZipResult(tr("Could not create file"));
}
for (auto fileName : m_extra_files.keys()) {
if (m_build_zip_future.isCanceled())
return ZipResult();
QuaZipFile indexFile(&m_output);
if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileName))) {
return ZipResult(tr("Could not create:") + fileName);
}
indexFile.write(m_extra_files[fileName]);
}
for (const QFileInfo& file : m_files) {
if (m_build_zip_future.isCanceled())
return ZipResult();
auto absolute = file.absoluteFilePath();
auto relative = m_dir.relativeFilePath(absolute);
setStatus("Compressing: " + relative);
setProgress(m_progress + 1, m_progressTotal);
if (m_follow_symlinks) {
if (file.isSymLink())
absolute = file.symLinkTarget();
else
absolute = file.canonicalFilePath();
}
if (!m_exclude_files.contains(relative) && !JlCompress::compressFile(&m_output, absolute, m_destination_prefix + relative)) {
return ZipResult(tr("Could not read and compress %1").arg(relative));
}
}
m_output.close();
if (m_output.getZipError() != 0) {
return ZipResult(tr("A zip error occurred"));
}
return ZipResult();
}
void ExportToZipTask::finish()
{
if (m_build_zip_future.isCanceled()) {
FS::deletePath(m_output_path);
emitAborted();
} else if (auto result = m_build_zip_future.result(); result.has_value()) {
FS::deletePath(m_output_path);
emitFailed(result.value());
} else {
emitSucceeded();
}
}
bool ExportToZipTask::abort()
{
if (m_build_zip_future.isRunning()) {
m_build_zip_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
void ExtractZipTask::executeTask()
{
if (!m_input->isOpen() && !m_input->open(QuaZip::mdUnzip)) {
emitFailed(tr("Unable to open supplied zip file."));
return;
}
m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); });
connect(&m_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExtractZipTask::finish);
m_zip_watcher.setFuture(m_zip_future);
}
auto ExtractZipTask::extractZip() -> ZipResult
{
auto target = m_output_dir.absolutePath();
auto target_top_dir = QUrl::fromLocalFile(target);
QStringList extracted;
qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target;
auto numEntries = m_input->getEntriesCount();
if (numEntries < 0) {
return ZipResult(tr("Failed to enumerate files in archive"));
}
if (numEntries == 0) {
logWarning(tr("Extracting empty archives seems odd..."));
return ZipResult();
}
if (!m_input->goToFirstFile()) {
return ZipResult(tr("Failed to seek to first file in zip"));
}
setStatus("Extracting files...");
setProgress(0, numEntries);
do {
if (m_zip_future.isCanceled())
return ZipResult();
setProgress(m_progress + 1, m_progressTotal);
QString file_name = m_input->getCurrentFileName();
if (!file_name.startsWith(m_subdirectory))
continue;
auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size()));
auto original_name = relative_file_name;
setStatus("Unpacking: " + relative_file_name);
// Fix subdirs/files ending with a / getting transformed into absolute paths
if (relative_file_name.startsWith('/'))
relative_file_name = relative_file_name.mid(1);
// Fix weird "folders with a single file get squashed" thing
QString sub_path;
if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) {
sub_path = relative_file_name.section('/', 0, -2) + '/';
FS::ensureFolderPathExists(FS::PathCombine(target, sub_path));
relative_file_name = relative_file_name.split('/').last();
}
QString target_file_path;
if (relative_file_name.isEmpty()) {
target_file_path = target + '/';
} else {
target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name);
if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/'))
target_file_path += '/';
}
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
return ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2")
.arg(relative_file_name, target));
}
if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) {
JlCompress::removeFile(extracted);
return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path));
}
extracted.append(target_file_path);
auto fileInfo = QFileInfo(target_file_path);
if (fileInfo.isFile()) {
auto permissions = fileInfo.permissions();
auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser |
QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther;
auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
auto newPermisions = (permissions & maxPermisions) | minPermisions;
if (newPermisions != permissions) {
if (!QFile::setPermissions(target_file_path, newPermisions)) {
logWarning(tr("Could not fix permissions for %1").arg(target_file_path));
}
}
} else if (fileInfo.isDir()) {
// Ensure the folder has the minimal required permissions
QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup |
QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther;
QFile::Permissions currentPermissions = fileInfo.permissions();
if ((currentPermissions & minimalPermissions) != minimalPermissions) {
if (!QFile::setPermissions(target_file_path, minimalPermissions)) {
logWarning(tr("Could not fix permissions for %1").arg(target_file_path));
}
}
}
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (m_input->goToNextFile());
return ZipResult();
}
void ExtractZipTask::finish()
{
if (m_zip_future.isCanceled()) {
emitAborted();
} else if (auto result = m_zip_future.result(); result.has_value()) {
emitFailed(result.value());
} else {
emitSucceeded();
}
}
bool ExtractZipTask::abort()
{
if (m_zip_future.isRunning()) {
m_zip_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
#endif
} // namespace MMCZip

View file

@ -36,8 +36,6 @@
#pragma once
#include <quazip.h>
#include <quazip/JlCompress.h>
#include <QDir>
#include <QFileInfo>
#include <QFuture>
@ -46,72 +44,27 @@
#include <QSet>
#include <QString>
#include <functional>
#include <memory>
#include <optional>
#include "archive/ArchiveReader.h"
#if defined(LAUNCHER_APPLICATION)
#include "minecraft/mod/Mod.h"
#endif
#include "Filter.h"
#include "tasks/Task.h"
namespace MMCZip {
using FilterFileFunction = std::function<bool(const QFileInfo&)>;
/**
* Merge two zip files, using a filter function
*/
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const Filter& filter = nullptr);
/**
* Compress directory, by providing a list of files to compress
* \param zip target archive
* \param dir directory that will be compressed (to compress with relative paths)
* \param files list of files to compress
* \param followSymlinks should follow symlinks when compressing file data
* \return true for success or false for failure
*/
bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks = false);
/**
* Compress directory, by providing a list of files to compress
* \param fileCompressed target archive file
* \param dir directory that will be compressed (to compress with relative paths)
* \param files list of files to compress
* \param followSymlinks should follow symlinks when compressing file data
* \return true for success or false for failure
*/
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false);
#if defined(LAUNCHER_APPLICATION)
/**
* take a source jar, add mods to it, resulting in target jar
*/
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods);
#endif
/**
* Find a single file in archive by file name (not path)
*
* \param ignore_paths paths to skip when recursing the search
*
* \return the path prefix where the file is
*/
QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths = {}, const QString& root = QString(""));
/**
* Find a multiple files of the same name in archive by file name
* If a file is found in a path, no deeper paths are searched
*
* \return true if anything was found
*/
bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root = QString());
/**
* Extract a subdirectory from an archive
*/
std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, const QString& target);
bool extractRelFile(QuaZip* zip, const QString& file, const QString& target);
std::optional<QStringList> extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target);
/**
* Extract a whole archive.
@ -151,90 +104,4 @@ bool extractFile(QString fileCompressed, QString file, QString dir);
* \return true for success or false for failure
*/
bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter);
#if defined(LAUNCHER_APPLICATION)
class ExportToZipTask : public Task {
Q_OBJECT
public:
ExportToZipTask(QString outputPath,
QDir dir,
QFileInfoList files,
QString destinationPrefix = "",
bool followSymlinks = false,
bool utf8Enabled = false)
: m_output_path(outputPath)
, m_output(outputPath)
, m_dir(dir)
, m_files(files)
, m_destination_prefix(destinationPrefix)
, m_follow_symlinks(followSymlinks)
{
setAbortable(true);
m_output.setUtf8Enabled(utf8Enabled);
};
ExportToZipTask(QString outputPath,
QString dir,
QFileInfoList files,
QString destinationPrefix = "",
bool followSymlinks = false,
bool utf8Enabled = false)
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled) {};
virtual ~ExportToZipTask() = default;
void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; }
void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); }
using ZipResult = std::optional<QString>;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult exportZip();
void finish();
private:
QString m_output_path;
QuaZip m_output;
QDir m_dir;
QFileInfoList m_files;
QString m_destination_prefix;
bool m_follow_symlinks;
QStringList m_exclude_files;
QHash<QString, QByteArray> m_extra_files;
QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher;
};
class ExtractZipTask : public Task {
Q_OBJECT
public:
ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "")
: ExtractZipTask(std::make_shared<QuaZip>(input), outputDir, subdirectory)
{}
ExtractZipTask(std::shared_ptr<QuaZip> input, QDir outputDir, QString subdirectory = "")
: m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory)
{}
virtual ~ExtractZipTask() = default;
using ZipResult = std::optional<QString>;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult extractZip();
void finish();
private:
std::shared_ptr<QuaZip> m_input;
QDir m_output_dir;
QString m_subdirectory;
QFuture<ZipResult> m_zip_future;
QFutureWatcher<ZipResult> m_zip_watcher;
};
#endif
} // namespace MMCZip

View file

@ -1,260 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "Untar.h"
#include <quagzipfile.h>
#include <QByteArray>
#include <QFileInfo>
#include <QIODevice>
#include <QString>
#include "FileSystem.h"
// adaptation of the:
// - https://github.com/madler/zlib/blob/develop/contrib/untgz/untgz.c
// - https://en.wikipedia.org/wiki/Tar_(computing)
// - https://github.com/euroelessar/cutereader/blob/master/karchive/src/ktar.cpp
#define BLOCKSIZE 512
#define SHORTNAMESIZE 100
enum class TypeFlag : char {
Regular = '0', // regular file
ARegular = 0, // regular file
Link = '1', // link
Symlink = '2', // reserved
Character = '3', // character special
Block = '4', // block special
Directory = '5', // directory
FIFO = '6', // FIFO special
Contiguous = '7', // reserved
// Posix stuff
GlobalPosixHeader = 'g',
ExtendedPosixHeader = 'x',
// 'A' 'Z' Vendor specific extensions(POSIX .1 - 1988)
// GNU
GNULongLink = 'K', /* long link name */
GNULongName = 'L', /* long file name */
};
// struct Header { /* byte offset */
// char name[100]; /* 0 */
// char mode[8]; /* 100 */
// char uid[8]; /* 108 */
// char gid[8]; /* 116 */
// char size[12]; /* 124 */
// char mtime[12]; /* 136 */
// char chksum[8]; /* 148 */
// TypeFlag typeflag; /* 156 */
// char linkname[100]; /* 157 */
// char magic[6]; /* 257 */
// char version[2]; /* 263 */
// char uname[32]; /* 265 */
// char gname[32]; /* 297 */
// char devmajor[8]; /* 329 */
// char devminor[8]; /* 337 */
// char prefix[155]; /* 345 */
// /* 500 */
// };
bool readLonglink(QIODevice* in, qint64 size, QByteArray& longlink)
{
qint64 n = 0;
size--; // ignore trailing null
if (size < 0) {
qCritical() << "The filename size is negative";
return false;
}
longlink.resize(size + (BLOCKSIZE - size % BLOCKSIZE)); // make the size divisible by BLOCKSIZE
for (qint64 offset = 0; offset < longlink.size(); offset += BLOCKSIZE) {
n = in->read(longlink.data() + offset, BLOCKSIZE);
if (n != BLOCKSIZE) {
qCritical() << "The expected blocksize was not respected for the name";
return false;
}
}
longlink.truncate(qstrlen(longlink.constData()));
return true;
}
int getOctal(char* buffer, int maxlenght, bool* ok)
{
return QByteArray(buffer, qstrnlen(buffer, maxlenght)).toInt(ok, 8);
}
QString decodeName(char* name)
{
return QFile::decodeName(QByteArray(name, qstrnlen(name, 100)));
}
bool Tar::extract(QIODevice* in, QString dst)
{
char buffer[BLOCKSIZE];
QString name, symlink, firstFolderName;
bool doNotReset = false, ok;
while (true) {
auto n = in->read(buffer, BLOCKSIZE);
if (n != BLOCKSIZE) { // allways expect complete blocks
qCritical() << "The expected blocksize was not respected";
return false;
}
if (buffer[0] == 0) { // end of archive
return true;
}
int mode = getOctal(buffer + 100, 8, &ok) | QFile::ReadUser | QFile::WriteUser; // hack to ensure write and read permisions
if (!ok) {
qCritical() << "The file mode can't be read";
return false;
}
// there are names that are exactly 100 bytes long
// and neither longlink nor \0 terminated (bug:101472)
if (name.isEmpty()) {
name = decodeName(buffer);
if (!firstFolderName.isEmpty() && name.startsWith(firstFolderName)) {
name = name.mid(firstFolderName.size());
}
}
if (symlink.isEmpty())
symlink = decodeName(buffer);
qint64 size = getOctal(buffer + 124, 12, &ok);
if (!ok) {
qCritical() << "The file size can't be read";
return false;
}
switch (TypeFlag(buffer[156])) {
case TypeFlag::Regular:
/* fallthrough */
case TypeFlag::ARegular: {
auto fileName = FS::PathCombine(dst, name);
if (!FS::ensureFilePathExists(fileName)) {
qCritical() << "Can't ensure the file path to exist: " << fileName;
return false;
}
QFile out(fileName);
if (!out.open(QFile::WriteOnly)) {
qCritical() << "Can't open file:" << fileName;
return false;
}
out.setPermissions(QFile::Permissions(mode));
while (size > 0) {
QByteArray tmp(BLOCKSIZE, 0);
n = in->read(tmp.data(), BLOCKSIZE);
if (n != BLOCKSIZE) {
qCritical() << "The expected blocksize was not respected when reading file";
return false;
}
tmp.truncate(qMin(qint64(BLOCKSIZE), size));
out.write(tmp);
size -= BLOCKSIZE;
}
break;
}
case TypeFlag::Directory: {
if (firstFolderName.isEmpty()) {
firstFolderName = name;
break;
}
auto folderPath = FS::PathCombine(dst, name);
if (!FS::ensureFolderPathExists(folderPath)) {
qCritical() << "Can't ensure that folder exists: " << folderPath;
return false;
}
break;
}
case TypeFlag::GNULongLink: {
doNotReset = true;
QByteArray longlink;
if (readLonglink(in, size, longlink)) {
symlink = QFile::decodeName(longlink.constData());
} else {
qCritical() << "Failed to read long link";
return false;
}
break;
}
case TypeFlag::GNULongName: {
doNotReset = true;
QByteArray longlink;
if (readLonglink(in, size, longlink)) {
name = QFile::decodeName(longlink.constData());
} else {
qCritical() << "Failed to read long name";
return false;
}
break;
}
case TypeFlag::Link:
/* fallthrough */
case TypeFlag::Symlink: {
auto fileName = FS::PathCombine(dst, name);
if (!FS::create_link(FS::PathCombine(QFileInfo(fileName).path(), symlink), fileName)()) { // do not use symlinks
qCritical() << "Can't create link for:" << fileName << " to:" << FS::PathCombine(QFileInfo(fileName).path(), symlink);
return false;
}
FS::ensureFilePathExists(fileName);
QFile::setPermissions(fileName, QFile::Permissions(mode));
break;
}
case TypeFlag::Character:
/* fallthrough */
case TypeFlag::Block:
/* fallthrough */
case TypeFlag::FIFO:
/* fallthrough */
case TypeFlag::Contiguous:
/* fallthrough */
case TypeFlag::GlobalPosixHeader:
/* fallthrough */
case TypeFlag::ExtendedPosixHeader:
/* fallthrough */
default:
break;
}
if (!doNotReset) {
name.truncate(0);
symlink.truncate(0);
}
doNotReset = false;
}
return true;
}
bool GZTar::extract(QString src, QString dst)
{
QuaGzipFile a(src);
if (!a.open(QIODevice::ReadOnly)) {
qCritical() << "Can't open tar file:" << src;
return false;
}
return Tar::extract(&a, dst);
}

View file

@ -1,46 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QIODevice>
// this is a hack used for the java downloader (feel free to remove it in favor of a library)
// both extract functions will extract the first folder inside dest(disregarding the prefix)
namespace Tar {
bool extract(QIODevice* in, QString dst);
}
namespace GZTar {
bool extract(QString src, QString dst);
}

View file

@ -0,0 +1,230 @@
// SPDX-License-Identifier: GPL-3.0-only AND LicenseRef-PublicDomain
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Additional note: Portions of this file are released into the public domain
* under LicenseRef-PublicDomain.
*/
#include "ArchiveReader.h"
#include <archive.h>
#include <archive_entry.h>
#include <QDir>
#include <QFileInfo>
namespace MMCZip {
QStringList ArchiveReader::getFiles()
{
return m_fileNames;
}
bool ArchiveReader::collectFiles(bool onlyFiles)
{
return parse([this, onlyFiles](File* f) {
if (!onlyFiles || f->isFile())
m_fileNames << f->filename();
return f->skip();
});
}
QString ArchiveReader::File::filename()
{
return QString::fromUtf8(archive_entry_pathname(m_entry));
}
QByteArray ArchiveReader::File::readAll(int* outStatus)
{
QByteArray data;
const void* buff;
size_t size;
la_int64_t offset;
int status;
while ((status = archive_read_data_block(m_archive.get(), &buff, &size, &offset)) == ARCHIVE_OK) {
data.append(static_cast<const char*>(buff), static_cast<qsizetype>(size));
}
if (status != ARCHIVE_EOF && status != ARCHIVE_OK) {
qWarning() << "libarchive read error: " << archive_error_string(m_archive.get());
}
if (outStatus) {
*outStatus = status;
}
return data;
}
QDateTime ArchiveReader::File::dateTime()
{
auto mtime = archive_entry_mtime(m_entry);
auto mtime_nsec = archive_entry_mtime_nsec(m_entry);
auto dt = QDateTime::fromSecsSinceEpoch(mtime);
return dt.addMSecs(mtime_nsec / 1e6);
}
int ArchiveReader::File::readNextHeader()
{
return archive_read_next_header(m_archive.get(), &m_entry);
}
auto ArchiveReader::goToFile(QString filename) -> std::unique_ptr<File>
{
auto f = std::make_unique<File>();
auto a = f->m_archive.get();
archive_read_support_format_all(a);
archive_read_support_filter_all(a);
auto fileName = m_archivePath.toUtf8();
if (archive_read_open_filename(a, fileName.constData(), m_blockSize) != ARCHIVE_OK) {
qCritical() << "Failed to open archive file:" << m_archivePath << "-" << archive_error_string(a);
return nullptr;
}
while (f->readNextHeader() == ARCHIVE_OK) {
if (f->filename() == filename) {
return f;
}
f->skip();
}
archive_read_close(a);
return nullptr;
}
static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false)
{
int r;
const void* buff;
size_t size;
la_int64_t offset;
for (;;) {
r = archive_read_data_block(ar, &buff, &size, &offset);
if (r == ARCHIVE_EOF)
return (ARCHIVE_OK);
if (r < ARCHIVE_OK) {
qCritical() << "Failed reading data block:" << archive_error_string(ar);
return (r);
}
if (notBlock) {
r = archive_write_data(aw, buff, size);
} else {
r = archive_write_data_block(aw, buff, size, offset);
}
if (r < ARCHIVE_OK) {
qCritical() << "Failed writing data block:" << archive_error_string(aw);
return (r);
}
}
}
bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool notBlock)
{
auto entry = m_entry;
if (!targetFileName.isEmpty()) {
entry = archive_entry_clone(m_entry);
auto nameUtf8 = targetFileName.toUtf8();
archive_entry_set_pathname(entry, nameUtf8.constData());
}
if (archive_write_header(out, entry) < ARCHIVE_OK) {
qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out);
return false;
} else if (archive_entry_size(m_entry) > 0) {
auto r = copy_data(m_archive.get(), out, notBlock);
if (r < ARCHIVE_OK)
qCritical() << "Failed reading data block:" << archive_error_string(out);
if (r < ARCHIVE_WARN)
return false;
}
auto r = archive_write_finish_entry(out);
if (r < ARCHIVE_OK)
qCritical() << "Failed to finish writing entry:" << archive_error_string(out);
return (r > ARCHIVE_WARN);
}
bool ArchiveReader::parse(std::function<bool(File*, bool&)> doStuff)
{
auto f = std::make_unique<File>();
auto a = f->m_archive.get();
archive_read_support_format_all(a);
archive_read_support_filter_all(a);
auto fileName = m_archivePath.toUtf8();
if (archive_read_open_filename(a, fileName.constData(), m_blockSize) != ARCHIVE_OK) {
qCritical() << "Failed to open archive file:" << m_archivePath << "-" << f->error();
return false;
}
bool breakControl = false;
while (f->readNextHeader() == ARCHIVE_OK) {
if (!doStuff(f.get(), breakControl)) {
qCritical() << "Failed to parse file:" << f->filename() << "-" << f->error();
return false;
}
if (breakControl) {
break;
}
}
archive_read_close(a);
return true;
}
bool ArchiveReader::parse(std::function<bool(File*)> doStuff)
{
return parse([doStuff](File* f, bool&) { return doStuff(f); });
}
bool ArchiveReader::File::isFile()
{
return (archive_entry_filetype(m_entry) & AE_IFMT) == AE_IFREG;
}
bool ArchiveReader::File::skip()
{
return archive_read_data_skip(m_archive.get()) == ARCHIVE_OK;
}
const char* ArchiveReader::File::error()
{
return archive_error_string(m_archive.get());
}
QString ArchiveReader::getZipName()
{
return m_archivePath;
}
bool ArchiveReader::exists(const QString& filePath) const
{
if (filePath == QLatin1String("/") || filePath.isEmpty())
return true;
// Normalize input path (remove trailing slash, if any)
QString normalizedPath = QDir::cleanPath(filePath);
if (normalizedPath.startsWith('/'))
normalizedPath.remove(0, 1);
if (normalizedPath == QLatin1String("."))
return true;
if (normalizedPath == QLatin1String(".."))
return false; // root only
// Check for exact file match
if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive))
return true;
// Check for directory existence by seeing if any file starts with that path
QString dirPath = normalizedPath + QLatin1Char('/');
for (const QString& f : m_fileNames) {
if (f.startsWith(dirPath, Qt::CaseInsensitive))
return true;
}
return false;
}
ArchiveReader::File::File() : m_archive(ArchivePtr(archive_read_new(), archive_read_free)) {}
} // namespace MMCZip

View file

@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QByteArray>
#include <QDateTime>
#include <QStringList>
#include <memory>
struct archive;
struct archive_entry;
namespace MMCZip {
class ArchiveReader {
public:
using ArchivePtr = std::unique_ptr<struct archive, int (*)(struct archive*)>;
ArchiveReader(QString fileName) : m_archivePath(fileName) {}
virtual ~ArchiveReader() = default;
QStringList getFiles();
QString getZipName();
bool collectFiles(bool onlyFiles = true);
bool exists(const QString& filePath) const;
class File {
public:
File();
virtual ~File() = default;
QString filename();
bool isFile();
QDateTime dateTime();
const char* error();
QByteArray readAll(int* outStatus = nullptr);
bool skip();
bool writeFile(archive* out, QString targetFileName = "", bool notBlock = false);
private:
int readNextHeader();
private:
friend ArchiveReader;
ArchivePtr m_archive;
archive_entry* m_entry;
};
std::unique_ptr<File> goToFile(QString filename);
bool parse(std::function<bool(File*)>);
bool parse(std::function<bool(File*, bool&)>);
private:
QString m_archivePath;
size_t m_blockSize = 10240;
QStringList m_fileNames = {};
};
} // namespace MMCZip

View file

@ -0,0 +1,213 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ArchiveWriter.h"
#include <archive.h>
#include <archive_entry.h>
#include <sys/stat.h>
#include <QFile>
#include <QFileInfo>
#include <memory>
namespace MMCZip {
ArchiveWriter::ArchiveWriter(const QString& archiveName) : m_filename(archiveName) {}
ArchiveWriter::~ArchiveWriter()
{
close();
}
bool ArchiveWriter::open()
{
if (m_filename.isEmpty()) {
qCritical() << "Archive m_filename not set.";
return false;
}
m_archive = archive_write_new();
if (!m_archive) {
qCritical() << "Archive not initialized.";
return false;
}
auto format = m_format.toUtf8();
archive_write_set_format_by_name(m_archive, format.constData());
if (archive_write_set_options(m_archive, "hdrcharset=UTF-8") != ARCHIVE_OK) {
qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive);
return false;
}
auto archiveNameUtf8 = m_filename.toUtf8();
if (archive_write_open_filename(m_archive, archiveNameUtf8.constData()) != ARCHIVE_OK) {
qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive);
return false;
}
return true;
}
bool ArchiveWriter::close()
{
bool success = true;
if (m_archive) {
if (archive_write_close(m_archive) != ARCHIVE_OK) {
qCritical() << "Failed to close archive" << m_filename << "-" << archive_error_string(m_archive);
success = false;
}
if (archive_write_free(m_archive) != ARCHIVE_OK) {
qCritical() << "Failed to free archive" << m_filename << "-" << archive_error_string(m_archive);
success = false;
}
m_archive = nullptr;
}
return success;
}
bool ArchiveWriter::addFile(const QString& fileName, const QString& fileDest)
{
QFileInfo fileInfo(fileName);
if (!fileInfo.exists()) {
qCritical() << "File does not exist:" << fileInfo.filePath();
return false;
}
std::unique_ptr<archive_entry, void (*)(archive_entry*)> entry_ptr(archive_entry_new(), archive_entry_free);
auto entry = entry_ptr.get();
if (!entry) {
qCritical() << "Failed to create archive entry";
return false;
}
auto fileDestUtf8 = fileDest.toUtf8();
archive_entry_set_pathname(entry, fileDestUtf8.constData());
QByteArray utf8 = fileInfo.absoluteFilePath().toUtf8();
const char* cpath = utf8.constData();
struct stat st;
if (stat(cpath, &st) != 0) {
qCritical() << "Failed to stat file:" << fileInfo.filePath();
}
// This should handle the copying of most attributes
archive_entry_copy_stat(entry, &st);
// However:
// "The [filetype] constants used by stat(2) may have different numeric values from the corresponding [libarchive constants]."
// - `archive_entry_stat(3)`
if (fileInfo.isSymLink()) {
archive_entry_set_filetype(entry, AE_IFLNK);
// We also need to manually copy some attributes from the link itself, as `stat` above operates on its target
auto target = fileInfo.symLinkTarget().toUtf8();
archive_entry_set_symlink(entry, target.constData());
archive_entry_set_size(entry, 0);
archive_entry_set_perm(entry, fileInfo.permissions());
} else if (fileInfo.isFile()) {
archive_entry_set_filetype(entry, AE_IFREG);
} else {
qCritical() << "Unsupported file type:" << fileInfo.filePath();
return false;
}
if (archive_write_header(m_archive, entry) != ARCHIVE_OK) {
qCritical() << "Failed to write header for: " << fileDest << "-" << archive_error_string(m_archive);
return false;
}
if (fileInfo.isFile() && !fileInfo.isSymLink()) {
QFile file(fileInfo.absoluteFilePath());
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file: " << fileInfo.filePath();
return false;
}
constexpr qint64 chunkSize = 8192;
QByteArray buffer;
buffer.resize(chunkSize);
while (!file.atEnd()) {
auto bytesRead = file.read(buffer.data(), chunkSize);
if (bytesRead < 0) {
qCritical() << "Read error in file: " << fileInfo.filePath();
return false;
}
if (archive_write_data(m_archive, buffer.constData(), bytesRead) < 0) {
qCritical() << "Write error in archive for: " << fileDest;
return false;
}
}
}
return true;
}
bool ArchiveWriter::addFile(const QString& fileDest, const QByteArray& data)
{
std::unique_ptr<archive_entry, void (*)(archive_entry*)> entry_ptr(archive_entry_new(), archive_entry_free);
auto entry = entry_ptr.get();
if (!entry) {
qCritical() << "Failed to create archive entry";
return false;
}
auto fileDestUtf8 = fileDest.toUtf8();
archive_entry_set_pathname(entry, fileDestUtf8.constData());
archive_entry_set_perm(entry, 0644);
archive_entry_set_filetype(entry, AE_IFREG);
archive_entry_set_size(entry, data.size());
if (archive_write_header(m_archive, entry) != ARCHIVE_OK) {
qCritical() << "Failed to write header for: " << fileDest << "-" << archive_error_string(m_archive);
return false;
}
if (archive_write_data(m_archive, data.constData(), data.size()) < 0) {
qCritical() << "Write error in archive for: " << fileDest << "-" << archive_error_string(m_archive);
return false;
}
return true;
}
bool ArchiveWriter::addFile(ArchiveReader::File* f)
{
return f->writeFile(m_archive, "", true);
}
std::unique_ptr<archive, void (*)(archive*)> ArchiveWriter::createDiskWriter()
{
int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS |
ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS;
std::unique_ptr<archive, void (*)(archive*)> extPtr(archive_write_disk_new(), [](archive* a) {
if (a) {
archive_write_close(a);
archive_write_free(a);
}
});
archive* ext = extPtr.get();
archive_write_disk_set_options(ext, flags);
archive_write_disk_set_standard_lookup(ext);
return extPtr;
}
} // namespace MMCZip

View file

@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QByteArray>
#include <QFileDevice>
#include "archive/ArchiveReader.h"
struct archive;
namespace MMCZip {
class ArchiveWriter {
public:
ArchiveWriter(const QString& archiveName);
virtual ~ArchiveWriter();
bool open();
bool close();
bool addFile(const QString& fileName, const QString& fileDest);
bool addFile(const QString& fileDest, const QByteArray& data);
bool addFile(ArchiveReader::File* f);
static std::unique_ptr<archive, void (*)(archive*)> createDiskWriter();
private:
struct archive* m_archive = nullptr;
QString m_filename;
QString m_format = "zip";
};
} // namespace MMCZip

View file

@ -0,0 +1,100 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ExportToZipTask.h"
#include <QtConcurrent>
#include "FileSystem.h"
namespace MMCZip {
void ExportToZipTask::executeTask()
{
setStatus("Adding files...");
setProgress(0, m_files.length());
m_build_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); });
connect(&m_build_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExportToZipTask::finish);
m_build_zip_watcher.setFuture(m_build_zip_future);
}
auto ExportToZipTask::exportZip() -> ZipResult
{
if (!m_dir.exists()) {
return ZipResult(tr("Folder doesn't exist"));
}
if (!m_output.open()) {
return ZipResult(tr("Could not create file"));
}
for (auto fileName : m_extra_files.keys()) {
if (m_build_zip_future.isCanceled())
return ZipResult();
if (!m_output.addFile(fileName, m_extra_files[fileName])) {
return ZipResult(tr("Could not add:") + fileName);
}
}
for (const QFileInfo& file : m_files) {
if (m_build_zip_future.isCanceled())
return ZipResult();
auto absolute = file.absoluteFilePath();
auto relative = m_dir.relativeFilePath(absolute);
setStatus("Compressing: " + relative);
setProgress(m_progress + 1, m_progressTotal);
if (m_follow_symlinks) {
if (file.isSymLink())
absolute = file.symLinkTarget();
else
absolute = file.canonicalFilePath();
}
if (!m_exclude_files.contains(relative) && !m_output.addFile(absolute, m_destination_prefix + relative)) {
return ZipResult(tr("Could not read and compress %1").arg(relative));
}
}
if (!m_output.close()) {
return ZipResult(tr("A zip error occurred"));
}
return ZipResult();
}
void ExportToZipTask::finish()
{
if (m_build_zip_future.isCanceled()) {
FS::deletePath(m_output_path);
emitAborted();
} else if (auto result = m_build_zip_future.result(); result.has_value()) {
FS::deletePath(m_output_path);
emitFailed(result.value());
} else {
emitSucceeded();
}
}
bool ExportToZipTask::abort()
{
if (m_build_zip_future.isRunning()) {
m_build_zip_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
} // namespace MMCZip

View file

@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDir>
#include <QFileInfoList>
#include <QFuture>
#include <QFutureWatcher>
#include "archive/ArchiveWriter.h"
#include "tasks/Task.h"
namespace MMCZip {
class ExportToZipTask : public Task {
Q_OBJECT
public:
ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
: m_output_path(outputPath)
, m_output(outputPath)
, m_dir(dir)
, m_files(files)
, m_destination_prefix(destinationPrefix)
, m_follow_symlinks(followSymlinks)
{
setAbortable(true);
};
ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks) {};
virtual ~ExportToZipTask() = default;
void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; }
void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); }
using ZipResult = std::optional<QString>;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult exportZip();
void finish();
private:
QString m_output_path;
ArchiveWriter m_output;
QDir m_dir;
QFileInfoList m_files;
QString m_destination_prefix;
bool m_follow_symlinks;
QStringList m_exclude_files;
QHash<QString, QByteArray> m_extra_files;
QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher;
};
} // namespace MMCZip

View file

@ -0,0 +1,135 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ExtractZipTask.h"
#include <QtConcurrent>
#include "FileSystem.h"
#include "archive/ArchiveReader.h"
#include "archive/ArchiveWriter.h"
namespace MMCZip {
void ExtractZipTask::executeTask()
{
m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); });
connect(&m_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExtractZipTask::finish);
m_zip_watcher.setFuture(m_zip_future);
}
auto ExtractZipTask::extractZip() -> ZipResult
{
auto target = m_output_dir.absolutePath();
auto target_top_dir = QUrl::fromLocalFile(target);
QStringList extracted;
qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input.getZipName() << "to" << target;
if (!m_input.collectFiles()) {
return ZipResult(tr("Failed to enumerate files in archive"));
}
if (m_input.getFiles().isEmpty()) {
logWarning(tr("Extracting empty archives seems odd..."));
return ZipResult();
}
auto extPtr = ArchiveWriter::createDiskWriter();
auto ext = extPtr.get();
setStatus("Extracting files...");
setProgress(0, m_input.getFiles().count());
ZipResult result;
if (!m_input.parse([this, &result, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) {
if (m_zip_future.isCanceled())
return false;
setProgress(m_progress + 1, m_progressTotal);
QString file_name = f->filename();
if (!file_name.startsWith(m_subdirectory)) {
f->skip();
return true;
}
auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size()));
auto original_name = relative_file_name;
setStatus("Unpacking: " + relative_file_name);
// Fix subdirs/files ending with a / getting transformed into absolute paths
if (relative_file_name.startsWith('/'))
relative_file_name = relative_file_name.mid(1);
// Fix weird "folders with a single file get squashed" thing
QString sub_path;
if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) {
sub_path = relative_file_name.section('/', 0, -2) + '/';
FS::ensureFolderPathExists(FS::PathCombine(target, sub_path));
relative_file_name = relative_file_name.split('/').last();
}
QString target_file_path;
if (relative_file_name.isEmpty()) {
target_file_path = target + '/';
} else {
target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name);
if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/'))
target_file_path += '/';
}
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
result = ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2")
.arg(relative_file_name, target));
return false;
}
if (!f->writeFile(ext, target_file_path)) {
result = ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path));
return false;
}
extracted.append(target_file_path);
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
return true;
})) {
FS::removeFiles(extracted);
return result.has_value() || m_zip_future.isCanceled() ? result
: ZipResult(tr("Failed to parse file %1").arg(m_input.getZipName()));
}
return ZipResult();
}
void ExtractZipTask::finish()
{
if (m_zip_future.isCanceled()) {
emitAborted();
} else if (auto result = m_zip_future.result(); result.has_value()) {
emitFailed(result.value());
} else {
emitSucceeded();
}
}
bool ExtractZipTask::abort()
{
if (m_zip_future.isRunning()) {
m_zip_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
} // namespace MMCZip

View file

@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDir>
#include <QFuture>
#include <QFutureWatcher>
#include "archive/ArchiveReader.h"
#include "tasks/Task.h"
namespace MMCZip {
class ExtractZipTask : public Task {
Q_OBJECT
public:
ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "")
: m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory)
{}
virtual ~ExtractZipTask() = default;
using ZipResult = std::optional<QString>;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult extractZip();
void finish();
private:
ArchiveReader m_input;
QDir m_output_dir;
QString m_subdirectory;
QFuture<ZipResult> m_zip_future;
QFutureWatcher<ZipResult> m_zip_watcher;
};
} // namespace MMCZip

View file

@ -16,12 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "java/download/ArchiveDownloadTask.h"
#include <quazip.h>
#include <memory>
#include "MMCZip.h"
#include "Application.h"
#include "Untar.h"
#include "archive/ArchiveReader.h"
#include "archive/ExtractZipTask.h"
#include "net/ChecksumValidator.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
@ -67,39 +66,19 @@ void ArchiveDownloadTask::executeTask()
void ArchiveDownloadTask::extractJava(QString input)
{
setStatus(tr("Extracting Java"));
if (input.endsWith("tar")) {
setStatus(tr("Extracting Java (Progress is not reported for tar archives)"));
QFile in(input);
if (!in.open(QFile::ReadOnly)) {
emitFailed(tr("Unable to open supplied tar file."));
return;
}
if (!Tar::extract(&in, QDir(m_final_path).absolutePath())) {
emitFailed(tr("Unable to extract supplied tar file."));
return;
}
emitSucceeded();
return;
} else if (input.endsWith("tar.gz") || input.endsWith("taz") || input.endsWith("tgz")) {
setStatus(tr("Extracting Java (Progress is not reported for tar archives)"));
if (!GZTar::extract(input, QDir(m_final_path).absolutePath())) {
emitFailed(tr("Unable to extract supplied tar file."));
return;
}
emitSucceeded();
return;
} else if (input.endsWith("zip")) {
auto zip = std::make_shared<QuaZip>(input);
if (!zip->open(QuaZip::mdUnzip)) {
MMCZip::ArchiveReader zip(input);
if (!zip.collectFiles()) {
emitFailed(tr("Unable to open supplied zip file."));
return;
}
auto files = zip->getFileNameList();
auto files = zip.getFiles();
if (files.isEmpty()) {
emitFailed(tr("No files were found in the supplied zip file."));
return;
}
m_task = makeShared<MMCZip::ExtractZipTask>(zip, m_final_path, files[0]);
auto firstFolderParts = files[0].split('/', Qt::SkipEmptyParts);
m_task = makeShared<MMCZip::ExtractZipTask>(input, m_final_path, firstFolderParts.value(0));
auto progressStep = std::make_shared<TaskStepProgress>();
connect(m_task.get(), &Task::finished, this, [this, progressStep] {
@ -128,9 +107,6 @@ void ArchiveDownloadTask::extractJava(QString input)
return;
}
emitFailed(tr("Could not determine archive type!"));
}
bool ArchiveDownloadTask::abort()
{
auto aborted = canAbort();

View file

@ -37,6 +37,13 @@
int main(int argc, char* argv[])
{
// try to set the utf-8 locale for the libarchive
for (auto name : { ".UTF-8", "en_US.UTF-8", "C.UTF-8" }) {
if (std::setlocale(LC_CTYPE, name)) {
break;
}
}
// initialize Qt
Application app(argc, argv);

View file

@ -43,9 +43,6 @@
#include <FileSystem.h>
#include <MMCZip.h>
#include <io/stream_reader.h>
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <tag_primitive.h>
#include <tag_string.h>
#include <sstream>
@ -57,6 +54,7 @@
#include "FileSystem.h"
#include "PSaveFile.h"
#include "archive/ArchiveReader.h"
using std::nullopt;
using std::optional;
@ -244,36 +242,25 @@ void World::readFromFS(const QFileInfo& file)
void World::readFromZip(const QFileInfo& file)
{
QuaZip zip(file.absoluteFilePath());
m_isValid = zip.open(QuaZip::mdUnzip);
if (!m_isValid) {
return;
MMCZip::ArchiveReader r(file.absoluteFilePath());
m_isValid = false;
r.parse([this](MMCZip::ArchiveReader::File* file, bool& stop) {
const QString levelDat = "level.dat";
auto filePath = file->filename();
QFileInfo fi(filePath);
if (fi.fileName().compare(levelDat, Qt::CaseInsensitive) == 0) {
m_containerOffsetPath = filePath.chopped(levelDat.length());
if (!m_containerOffsetPath.isEmpty()) {
return false;
}
auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat");
m_isValid = !location.isEmpty();
if (!m_isValid) {
return;
m_levelDatTime = file->dateTime();
loadFromLevelDat(file->readAll());
m_isValid = true;
stop = true;
}
m_containerOffsetPath = location;
QuaZipFile zippedFile(&zip);
// read the install profile
m_isValid = zip.setCurrentFile(location + "level.dat");
if (!m_isValid) {
return;
}
m_isValid = zippedFile.open(QIODevice::ReadOnly);
QuaZipFileInfo64 levelDatInfo;
zippedFile.getFileInfo(&levelDatInfo);
auto modTime = levelDatInfo.getNTFSmTime();
if (!modTime.isValid()) {
modTime = levelDatInfo.dateTime;
}
m_levelDatTime = modTime;
if (!m_isValid) {
return;
}
loadFromLevelDat(zippedFile.readAll());
zippedFile.close();
return true;
});
}
bool World::install(const QString& to, const QString& name)
@ -284,10 +271,7 @@ bool World::install(const QString& to, const QString& name)
}
bool ok = false;
if (m_containerFile.isFile()) {
QuaZip zip(m_containerFile.absoluteFilePath());
if (!zip.open(QuaZip::mdUnzip)) {
return false;
}
MMCZip::ArchiveReader zip(m_containerFile.absoluteFilePath());
ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath);
} else if (m_containerFile.isDir()) {
QString from = m_containerFile.filePath();
@ -350,7 +334,7 @@ optional<QString> read_string(nbt::value& parent, const char* name)
return nullopt;
}
auto& tag_str = namedValue.as<nbt::tag_string>();
return QString::fromStdString(tag_str.get());
return QString::fromUtf8(tag_str.get());
} catch ([[maybe_unused]] const std::out_of_range& e) {
// fallback for old world formats
qWarning() << "String NBT tag" << name << "could not be found.";

View file

@ -17,11 +17,10 @@
#include <launch/LaunchTask.h>
#include <minecraft/MinecraftInstance.h>
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <QDir>
#include "FileSystem.h"
#include "MMCZip.h"
#include "archive/ArchiveReader.h"
#include "archive/ArchiveWriter.h"
#ifdef major
#undef major
@ -41,30 +40,21 @@ static QString replaceSuffix(QString target, const QString& suffix, const QStrin
static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack)
{
QuaZip zip(source);
if (!zip.open(QuaZip::mdUnzip)) {
return false;
}
MMCZip::ArchiveReader zip(source);
QDir directory(targetFolder);
if (!zip.goToFirstFile()) {
return false;
}
do {
QString name = zip.getCurrentFileName();
auto extPtr = MMCZip::ArchiveWriter::createDiskWriter();
auto ext = extPtr.get();
return zip.parse([applyJnilibHack, directory, ext](MMCZip::ArchiveReader::File* f) {
QString name = f->filename();
auto lowercase = name.toLower();
if (applyJnilibHack) {
name = replaceSuffix(name, ".jnilib", ".dylib");
}
QString absFilePath = directory.absoluteFilePath(name);
if (!JlCompress::extractFile(&zip, "", absFilePath)) {
return false;
}
} while (zip.goToNextFile());
zip.close();
if (zip.getZipError() != 0) {
return false;
}
return true;
return f->writeFile(ext, absFilePath);
});
}
void ExtractNatives::executeTask()

View file

@ -50,7 +50,7 @@
LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent)
: LaunchStep(parent)
, m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QTextCodec::codecForName("UTF-8") : QTextCodec::codecForLocale())
, m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QStringConverter::Utf8 : QStringConverter::System)
{
if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) {
static const QRegularExpression s_settingUser(".*Setting user.+", QRegularExpression::CaseInsensitiveOption);

View file

@ -44,8 +44,6 @@
#include <QPixmap>
#include <QPixmapCache>
#include <optional>
#include "ModDetails.h"
#include "Resource.h"

View file

@ -35,14 +35,10 @@
#pragma once
#include <memory>
#include <QString>
#include <QStringList>
#include <QUrl>
#include "minecraft/mod/MetadataHandler.h"
struct ModLicense {
QString name = {};
QString id = {};

View file

@ -23,12 +23,9 @@
#include "FileSystem.h"
#include "Json.h"
#include "archive/ArchiveReader.h"
#include "minecraft/mod/ResourcePack.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <QCryptographicHash>
namespace DataPackUtils {
@ -106,68 +103,63 @@ bool processZIP(DataPack* pack, ProcessingLevel level)
{
Q_ASSERT(pack->type() == ResourceType::ZIPFILE);
QuaZip zip(pack->fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
MMCZip::ArchiveReader zip(pack->fileinfo().filePath());
bool metaParsed = false;
bool iconParsed = false;
bool mcmeta_result = false;
bool pack_png_result = false;
if (!zip.parse(
[&metaParsed, &iconParsed, &mcmeta_result, &pack_png_result, pack, level](MMCZip::ArchiveReader::File* f, bool& breakControl) {
bool skip = true;
if (!metaParsed && f->filename() == "pack.mcmeta") {
metaParsed = true;
skip = false;
auto data = f->readAll();
mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data));
if (!mcmeta_result) {
breakControl = true;
return true; // mcmeta invalid
}
}
if (!iconParsed && level != ProcessingLevel::BasicInfoOnly && f->filename() == "pack.png") {
iconParsed = true;
skip = false;
auto data = f->readAll();
pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data));
if (!pack_png_result) {
breakControl = true;
return true; // pack.png invalid
}
}
if (skip) {
f->skip();
}
if (metaParsed && (level == ProcessingLevel::BasicInfoOnly || iconParsed)) {
breakControl = true;
}
return true;
})) {
return false; // can't open zip file
QuaZipFile file(&zip);
auto mcmeta_invalid = [&pack]() {
}
if (!mcmeta_result) {
qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional
};
if (zip.setCurrentFile("pack.mcmeta")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return mcmeta_invalid();
}
auto data = file.readAll();
bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data));
file.close();
if (!mcmeta_result) {
return mcmeta_invalid(); // mcmeta invalid
}
} else {
return mcmeta_invalid(); // could not set pack.mcmeta as current file.
}
if (level == ProcessingLevel::BasicInfoOnly) {
zip.close();
return true; // only need basic info already checked
}
auto png_invalid = [&pack]() {
if (!pack_png_result) {
qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data));
file.close();
zip.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
zip.close();
return true;
}
@ -311,29 +303,18 @@ bool processPackPNG(const DataPack* pack)
return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740
}
case ResourceType::ZIPFILE: {
QuaZip zip(pack->fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file
QuaZipFile file(&zip);
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
MMCZip::ArchiveReader zip(pack->fileinfo().filePath());
auto f = zip.goToFile("pack.png");
if (!f) {
return png_invalid();
}
auto data = file.readAll();
auto data = f->readAll();
bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data));
file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // could not set pack.mcmeta as current file.
}
return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740
}
default:

View file

@ -1,8 +1,6 @@
#include "LocalModParseTask.h"
#include <qdcss.h>
#include <quazip/quazip.h>
#include <quazip/quazipfile.h>
#include <toml++/toml.h>
#include <QJsonArray>
#include <QJsonDocument>
@ -13,6 +11,7 @@
#include "FileSystem.h"
#include "Json.h"
#include "archive/ArchiveReader.h"
#include "minecraft/mod/ModDetails.h"
#include "settings/INIFile.h"
@ -470,32 +469,33 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level)
{
ModDetails details;
QuaZip zip(mod.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false;
MMCZip::ArchiveReader zip(mod.fileinfo().filePath());
QuaZipFile file(&zip);
bool baseForgePopulated = false;
bool isNilMod = false;
bool isValid = false;
QString manifestVersion = {};
QByteArray nilData = {};
QString nilFilePath = {};
if (zip.setCurrentFile("META-INF/mods.toml") || zip.setCurrentFile("META-INF/neoforge.mods.toml")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
return false;
if (!zip.parse([&details, &baseForgePopulated, &manifestVersion, &isValid, &nilData, &isNilMod, &nilFilePath](
MMCZip::ArchiveReader::File* file, bool& stop) {
auto filePath = file->filename();
if (filePath == "META-INF/mods.toml" || filePath == "META-INF/neoforge.mods.toml") {
details = ReadMCModTOML(file->readAll());
isValid = true;
if (details.version == "${file.jarVersion}" && !manifestVersion.isEmpty()) {
details.version = manifestVersion;
}
details = ReadMCModTOML(file.readAll());
file.close();
// to replace ${file.jarVersion} with the actual version, as needed
if (details.version == "${file.jarVersion}") {
if (zip.setCurrentFile("META-INF/MANIFEST.MF")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
return false;
stop = details.version != "${file.jarVersion}";
baseForgePopulated = true;
return true;
}
if (filePath == "META-INF/MANIFEST.MF") {
// quick and dirty line-by-line parser
auto manifestLines = QString(file.readAll()).split(s_newlineRegex);
QString manifestVersion = "";
auto manifestLines = QString(file->readAll()).split(s_newlineRegex);
manifestVersion = "";
for (auto& line : manifestLines) {
if (line.startsWith("Implementation-Version: ", Qt::CaseInsensitive)) {
manifestVersion = line.remove("Implementation-Version: ", Qt::CaseInsensitive);
@ -508,94 +508,64 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level)
if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") {
manifestVersion = "NONE";
}
if (baseForgePopulated) {
details.version = manifestVersion;
file.close();
stop = true;
}
}
zip.close();
mod.setDetails(details);
return true;
} else if (zip.setCurrentFile("mcmod.info")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
return false;
}
details = ReadMCModInfo(file.readAll());
file.close();
zip.close();
mod.setDetails(details);
if (filePath == "mcmod.info") {
details = ReadMCModInfo(file->readAll());
isValid = true;
stop = true;
return true;
} else if (zip.setCurrentFile("quilt.mod.json")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
return false;
}
details = ReadQuiltModInfo(file.readAll());
file.close();
zip.close();
mod.setDetails(details);
if (filePath == "quilt.mod.json") {
details = ReadQuiltModInfo(file->readAll());
isValid = true;
stop = true;
return true;
} else if (zip.setCurrentFile("fabric.mod.json")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
return false;
}
details = ReadFabricModInfo(file.readAll());
file.close();
zip.close();
mod.setDetails(details);
if (filePath == "fabric.mod.json") {
details = ReadFabricModInfo(file->readAll());
isValid = true;
stop = true;
return true;
} else if (zip.setCurrentFile("forgeversion.properties")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
return false;
}
details = ReadForgeInfo(file.readAll());
file.close();
zip.close();
mod.setDetails(details);
if (filePath == "forgeversion.properties") {
details = ReadForgeInfo(file->readAll());
isValid = true;
stop = true;
return true;
} else if (zip.setCurrentFile("META-INF/nil/mappings.json")) {
}
if (filePath == "META-INF/nil/mappings.json") {
// nilloader uses the filename of the metadata file for the modid, so we can't know the exact filename
// thankfully, there is a good file to use as a canary so we don't look for nil meta all the time
QString foundNilMeta;
for (auto& fname : zip.getFileNameList()) {
isNilMod = true;
stop = !nilFilePath.isEmpty();
file->skip();
return true;
}
// nilmods can shade nilloader to be able to run as a standalone agent - which includes nilloader's own meta file
if (fname.endsWith(".nilmod.css") && fname != "nilloader.nilmod.css") {
foundNilMeta = fname;
break;
if (filePath.endsWith(".nilmod.css") && filePath != "nilloader.nilmod.css") {
nilData = file->readAll();
nilFilePath = filePath;
stop = isNilMod;
return true;
}
}
if (zip.setCurrentFile(foundNilMeta)) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
file->skip();
return true;
})) {
return false;
}
details = ReadNilModInfo(file.readAll(), foundNilMeta);
file.close();
zip.close();
if (isNilMod) {
details = ReadNilModInfo(nilData, nilFilePath);
isValid = true;
}
if (isValid) {
mod.setDetails(details);
return true;
}
}
zip.close();
return false; // no valid mod found in archive
}
@ -624,25 +594,14 @@ bool processLitemod(Mod& mod, [[maybe_unused]] ProcessingLevel level)
{
ModDetails details;
QuaZip zip(mod.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false;
MMCZip::ArchiveReader zip(mod.fileinfo().filePath());
QuaZipFile file(&zip);
if (zip.setCurrentFile("litemod.json")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
return false;
}
details = ReadLiteModInfo(file.readAll());
file.close();
if (auto file = zip.goToFile("litemod.json"); file) {
details = ReadLiteModInfo(file->readAll());
mod.setDetails(details);
return true;
}
zip.close();
return false; // no valid litemod.json found in archive
}
@ -700,24 +659,13 @@ bool loadIconFile(const Mod& mod, QPixmap* pixmap)
return png_invalid("file '" + icon_info.filePath() + "' does not exists or is not a file");
}
case ResourceType::ZIPFILE: {
QuaZip zip(mod.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return png_invalid("failed to open '" + mod.fileinfo().filePath() + "' as a zip archive");
QuaZipFile file(&zip);
if (zip.setCurrentFile(mod.iconPath())) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid("Failed to open '" + mod.iconPath() + "' in zip archive");
}
auto data = file.readAll();
MMCZip::ArchiveReader zip(mod.fileinfo().filePath());
auto file = zip.goToFile(mod.iconPath());
if (file) {
auto data = file->readAll();
bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap);
file.close();
if (!icon_result) {
return png_invalid("invalid png image"); // icon png invalid
}

View file

@ -22,10 +22,7 @@
#include "LocalShaderPackParseTask.h"
#include "FileSystem.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include "archive/ArchiveReader.h"
namespace ShaderPackUtils {
@ -63,25 +60,19 @@ bool processZIP(ShaderPack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
MMCZip::ArchiveReader zip(pack.fileinfo().filePath());
if (!zip.collectFiles())
return false; // can't open zip file
QuaZipFile file(&zip);
QuaZipDir zipDir(&zip);
if (!zipDir.exists("/shaders")) {
if (!zip.exists("/shaders")) {
return false; // assets dir does not exists at zip root
}
pack.setPackFormat(ShaderPackFormat::VALID);
if (level == ProcessingLevel::BasicInfoOnly) {
zip.close();
return true; // only need basic info already checked
}
zip.close();
return true;
}

View file

@ -20,9 +20,7 @@
#include "LocalTexturePackParseTask.h"
#include "FileSystem.h"
#include <quazip/quazip.h>
#include <quazip/quazipfile.h>
#include "archive/ArchiveReader.h"
#include <QCryptographicHash>
@ -91,55 +89,26 @@ bool processZIP(TexturePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false;
MMCZip::ArchiveReader zip(pack.fileinfo().filePath());
bool packProcessed = false;
bool iconProcessed = false;
QuaZipFile file(&zip);
if (zip.setCurrentFile("pack.txt")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return false;
return zip.parse([&packProcessed, &iconProcessed, &pack, level](MMCZip::ArchiveReader::File* file, bool& stop) {
if (!packProcessed && file->filename() == "pack.txt") {
packProcessed = true;
auto data = file->readAll();
stop = packProcessed && (iconProcessed || level == ProcessingLevel::BasicInfoOnly);
return TexturePackUtils::processPackTXT(pack, std::move(data));
}
auto data = file.readAll();
bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data));
file.close();
if (!packTXT_result) {
return false;
if (!iconProcessed && file->filename() == "pack.png") {
iconProcessed = true;
auto data = file->readAll();
stop = packProcessed && iconProcessed;
return TexturePackUtils::processPackPNG(pack, std::move(data));
}
}
if (level == ProcessingLevel::BasicInfoOnly) {
zip.close();
return true;
}
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return false;
}
auto data = file.readAll();
bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data));
file.close();
zip.close();
if (!packPNG_result) {
return false;
}
}
zip.close();
file->skip();
return true;
});
}
bool processPackTXT(TexturePack& pack, QByteArray&& raw_data)
@ -189,32 +158,19 @@ bool processPackPNG(const TexturePack& pack)
return false;
}
case ResourceType::ZIPFILE: {
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file
MMCZip::ArchiveReader zip(pack.fileinfo().filePath());
QuaZipFile file(&zip);
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
auto file = zip.goToFile("pack.png");
if (file) {
auto data = file->readAll();
bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data));
file.close();
if (!pack_png_result) {
zip.close();
return png_invalid(); // pack.png invalid
}
} else {
zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
return false;
return png_invalid(); // could not set pack.mcmeta as current file.
}
default:
qWarning() << "Invalid type for resource pack parse task!";

View file

@ -23,13 +23,11 @@
#include "LocalWorldSaveParseTask.h"
#include "FileSystem.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include "archive/ArchiveReader.h"
#include <QDir>
#include <QFileInfo>
#include <tuple>
namespace WorldSaveUtils {
@ -105,22 +103,40 @@ bool processFolder(WorldSave& save, ProcessingLevel level)
/// QString <name of folder containing level.dat>,
/// bool <saves folder found>
/// )
static std::tuple<bool, QString, bool> contains_level_dat(QuaZip& zip)
static std::tuple<bool, QString, bool> contains_level_dat(QString fileName)
{
MMCZip::ArchiveReader zip(fileName);
if (!zip.collectFiles()) {
return std::make_tuple(false, "", false);
}
bool saves = false;
QuaZipDir zipDir(&zip);
if (zipDir.exists("/saves")) {
if (zip.exists("/saves")) {
saves = true;
zipDir.cd("/saves");
}
for (auto const& entry : zipDir.entryList()) {
zipDir.cd(entry);
if (zipDir.exists("level.dat")) {
return std::make_tuple(true, entry, saves);
for (auto file : zip.getFiles()) {
QString relativePath = file;
if (saves) {
if (!relativePath.startsWith("saves/", Qt::CaseInsensitive))
continue;
relativePath = relativePath.mid(QString("saves/").length());
}
zipDir.cd("..");
if (!relativePath.endsWith("/level.dat", Qt::CaseInsensitive))
continue;
int slashIndex = relativePath.indexOf('/');
if (slashIndex == -1)
continue; // malformed: no slash between saves/ and level.dat
QString worldName = relativePath.left(slashIndex);
QString remaining = relativePath.mid(slashIndex + 1);
// Check that there's nothing between worldName/ and level.dat
if (remaining == "level.dat") {
return std::make_tuple(true, worldName, saves);
}
}
return std::make_tuple(false, "", saves);
}
@ -128,19 +144,14 @@ bool processZIP(WorldSave& save, ProcessingLevel level)
{
Q_ASSERT(save.type() == ResourceType::ZIPFILE);
QuaZip zip(save.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file
auto [found, save_dir_name, found_saves_dir] = contains_level_dat(zip);
if (save_dir_name.endsWith("/")) {
save_dir_name.chop(1);
}
auto [found, save_dir_name, found_saves_dir] = contains_level_dat(save.fileinfo().filePath());
if (!found) {
return false;
}
if (save_dir_name.endsWith("/")) {
save_dir_name.chop(1);
}
save.setSaveDirName(save_dir_name);
@ -151,14 +162,11 @@ bool processZIP(WorldSave& save, ProcessingLevel level)
}
if (level == ProcessingLevel::BasicInfoOnly) {
zip.close();
return true; // only need basic info already checked
}
// reserved for more intensive processing
zip.close();
return true;
}

View file

@ -39,8 +39,6 @@
#include <QtConcurrent>
#include <algorithm>
#include <quazip/quazip.h>
#include "FileSystem.h"
#include "Json.h"
#include "MMCZip.h"
@ -675,13 +673,6 @@ void PackInstallTask::extractConfigs()
setStatus(tr("Extracting configs..."));
QDir extractDir(m_stagingPath);
QuaZip packZip(archivePath);
if (!packZip.open(QuaZip::mdUnzip)) {
emitFailed(tr("Failed to open pack configs %1!").arg(archivePath));
return;
}
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath,
extractDir.absolutePath() + "/minecraft");
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, [this]() { downloadMods(); });

View file

@ -30,7 +30,6 @@
#include <memory>
#include "Application.h"
#include "Json.h"
#include "MMCZip.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
@ -38,6 +37,8 @@
#include "modplatform/helpers/HashUtils.h"
#include "tasks/Task.h"
#include "archive/ExportToZipTask.h"
const QString FlamePackExportTask::TEMPLATE = "<li><a href=\"{url}\">{name}{authors}</a></li>\n";
const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });
@ -318,7 +319,7 @@ void FlamePackExportTask::buildZip()
setStatus(tr("Adding files..."));
setProgress(4, 5);
auto zipTask = makeShared<MMCZip::ExportToZipTask>(m_options.output, m_gameRoot, files, "overrides/", true, false);
auto zipTask = makeShared<MMCZip::ExportToZipTask>(m_options.output, m_gameRoot, files, "overrides/", true);
zipTask->addExtraFile("manifest.json", generateIndex());
zipTask->addExtraFile("modlist.html", generateHTML());

View file

@ -74,17 +74,49 @@ Modpack parseDirectory(QString path)
modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion");
modpack.jvmArgs = root["jvmArgs"].toVariant();
modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime");
auto modLoader = Json::requireString(root, "modLoader", "modLoader");
if (!modLoader.isEmpty()) {
const auto parts = modLoader.split('-', Qt::KeepEmptyParts);
if (parts.size() >= 2) {
const auto loader = parts.first().toLower();
modpack.version = parts.at(1).trimmed();
if (loader == "neoforge") {
modpack.loaderType = ModPlatform::NeoForge;
} else if (loader == "forge") {
modpack.loaderType = ModPlatform::Forge;
} else if (loader == "fabric") {
modpack.loaderType = ModPlatform::Fabric;
} else if (loader == "quilt") {
modpack.loaderType = ModPlatform::Quilt;
}
}
} else {
legacyInstanceParsing(path, &modpack.loaderType, &modpack.loaderVersion);
}
} catch (const Exception& e) {
qDebug() << "Couldn't load ftb instance json: " << e.cause();
return {};
}
auto iconFile = QFileInfo(FS::PathCombine(path, "folder.jpg"));
if (iconFile.exists() && iconFile.isFile()) {
modpack.icon = QIcon(iconFile.absoluteFilePath());
} else { // the logo is a file that the first bit denotes the image tipe followed by the actual image data
modpack.icon = loadFTBIcon(FS::PathCombine(path, ".ftbapp", "logo"));
}
return modpack;
}
void legacyInstanceParsing(QString path, std::optional<ModPlatform::ModLoaderType>* loaderType, QString* loaderVersion)
{
auto versionsFile = QFileInfo(FS::PathCombine(path, ".ftbapp", "version.json"));
if (!versionsFile.exists() || !versionsFile.isFile()) {
versionsFile = QFileInfo(FS::PathCombine(path, "version.json"));
}
if (!versionsFile.exists() || !versionsFile.isFile()) {
return {};
qDebug() << "Couldn't find ftb version json";
return;
}
try {
auto doc = Json::requireDocument(versionsFile.absoluteFilePath(), "FTB_APP version JSON file");
@ -96,34 +128,29 @@ Modpack parseDirectory(QString path)
auto name = Json::requireString(obj, "name", "name");
auto version = Json::requireString(obj, "version", "version");
if (name == "neoforge") {
modpack.loaderType = ModPlatform::NeoForge;
modpack.version = version;
*loaderType = ModPlatform::NeoForge;
*loaderVersion = version;
break;
} else if (name == "forge") {
modpack.loaderType = ModPlatform::Forge;
modpack.version = version;
*loaderType = ModPlatform::Forge;
*loaderVersion = version;
break;
} else if (name == "fabric") {
modpack.loaderType = ModPlatform::Fabric;
modpack.version = version;
*loaderType = ModPlatform::Fabric;
*loaderVersion = version;
break;
} else if (name == "quilt") {
modpack.loaderType = ModPlatform::Quilt;
modpack.version = version;
*loaderType = ModPlatform::Quilt;
*loaderVersion = version;
break;
}
}
} catch (const Exception& e) {
}
catch (const Exception& e)
{
qDebug() << "Couldn't load ftb version json: " << e.cause();
return {};
return;
}
auto iconFile = QFileInfo(FS::PathCombine(path, "folder.jpg"));
if (iconFile.exists() && iconFile.isFile()) {
modpack.icon = QIcon(iconFile.absoluteFilePath());
} else { // the logo is a file that the first bit denotes the image tipe followed by the actual image data
modpack.icon = loadFTBIcon(FS::PathCombine(path, ".ftbapp", "logo"));
}
return modpack;
}
} // namespace FTBImportAPP

View file

@ -49,7 +49,7 @@ struct Modpack {
using ModpackList = QList<Modpack>;
Modpack parseDirectory(QString path);
void legacyInstanceParsing(QString path, std::optional<ModPlatform::ModLoaderType>* loaderType, QString* loaderVersion);
} // namespace FTBImportAPP
// We need it for the proxy model

View file

@ -102,12 +102,6 @@ void PackInstallTask::unzip()
QDir extractDir(m_stagingPath);
m_packZip.reset(new QuaZip(archivePath));
if (!m_packZip->open(QuaZip::mdUnzip)) {
emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
return;
}
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath,
extractDir.absolutePath() + "/unzip");
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);

View file

@ -1,6 +1,4 @@
#pragma once
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include "InstanceTask.h"
#include "PackHelpers.h"
#include "meta/Index.h"
@ -39,7 +37,6 @@ class PackInstallTask : public InstanceTask {
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
bool abortable = false;
std::unique_ptr<QuaZip> m_packZip;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
NetJob::Ptr netJobContainer;

View file

@ -25,6 +25,7 @@
#include <QtConcurrentRun>
#include "Json.h"
#include "MMCZip.h"
#include "archive/ExportToZipTask.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/MetadataHandler.h"
#include "minecraft/mod/ModFolderModel.h"
@ -200,7 +201,7 @@ void ModrinthPackExportTask::buildZip()
{
setStatus(tr("Adding files..."));
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true, true);
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true);
zipTask->addExtraFile("modrinth.index.json", generateIndex());
zipTask->setExcludeFiles(resolvedFiles.keys());

View file

@ -71,7 +71,7 @@ auto stringEntry(toml::table table, QString entry_name) -> QString
{
auto node = table[StringUtils::toStdString(entry_name)];
if (!node) {
qWarning() << "Failed to read str property '" + entry_name + "' in mod metadata.";
qDebug() << "Failed to read str property '" + entry_name + "' in mod metadata.";
return {};
}
@ -82,7 +82,7 @@ auto intEntry(toml::table table, QString entry_name) -> int
{
auto node = table[StringUtils::toStdString(entry_name)];
if (!node) {
qWarning() << "Failed to read int property '" + entry_name + "' in mod metadata.";
qDebug() << "Failed to read int property '" + entry_name + "' in mod metadata.";
return {};
}

View file

@ -66,11 +66,7 @@ void Technic::SingleZipPackInstallTask::downloadSucceeded()
qDebug() << "Attempting to create instance from" << m_archivePath;
// open the zip and find relevant files in it
m_packZip.reset(new QuaZip(m_archivePath));
if (!m_packZip->open(QuaZip::mdUnzip)) {
emitFailed(tr("Unable to open supplied modpack zip file."));
return;
}
m_packZip.reset(new MMCZip::ArchiveReader(m_archivePath));
m_extractFuture =
QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath());
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SingleZipPackInstallTask::extractFinished);

View file

@ -16,10 +16,9 @@
#pragma once
#include "InstanceTask.h"
#include "archive/ArchiveReader.h"
#include "net/NetJob.h"
#include <quazip/quazip.h>
#include <QFutureWatcher>
#include <QStringList>
#include <QUrl>
@ -54,7 +53,7 @@ class SingleZipPackInstallTask : public InstanceTask {
QString m_minecraftVersion;
QString m_archivePath;
NetJob::Ptr m_filesNetJob;
std::unique_ptr<QuaZip> m_packZip;
std::unique_ptr<MMCZip::ArchiveReader> m_packZip;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
};

View file

@ -19,12 +19,10 @@
#include <Json.h>
#include <minecraft/MinecraftInstance.h>
#include <minecraft/PackProfile.h>
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <settings/INISettingsObject.h>
#include <memory>
#include "archive/ArchiveReader.h"
void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
const QString& instName,
@ -53,35 +51,30 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json");
QString fmlMinecraftVersion;
if (QFile::exists(modpackJar)) {
QuaZip zipFile(modpackJar);
if (!zipFile.open(QuaZip::mdUnzip)) {
MMCZip::ArchiveReader zipFile(modpackJar);
if (!zipFile.collectFiles()) {
emit failed(tr("Unable to open \"bin/modpack.jar\" file!"));
return;
}
QuaZipDir zipFileRoot(&zipFile, "/");
if (zipFileRoot.exists("/version.json")) {
if (zipFileRoot.exists("/fmlversion.properties")) {
zipFile.setCurrentFile("fmlversion.properties");
QuaZipFile file(&zipFile);
if (!file.open(QIODevice::ReadOnly)) {
if (zipFile.exists("/version.json")) {
if (zipFile.exists("/fmlversion.properties")) {
auto file = zipFile.goToFile("fmlversion.properties");
if (!file) {
emit failed(tr("Unable to open \"fmlversion.properties\"!"));
return;
}
QByteArray fmlVersionData = file.readAll();
file.close();
QByteArray fmlVersionData = file->readAll();
INIFile iniFile;
iniFile.loadFile(fmlVersionData);
// If not present, this evaluates to a null string
fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString();
}
zipFile.setCurrentFile("version.json", QuaZip::csSensitive);
QuaZipFile file(&zipFile);
if (!file.open(QIODevice::ReadOnly)) {
auto file = zipFile.goToFile("version.json");
if (!file) {
emit failed(tr("Unable to open \"version.json\"!"));
return;
}
data = file.readAll();
file.close();
data = file->readAll();
} else {
if (minecraftVersion.isEmpty()) {
emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown"));
@ -93,16 +86,14 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
// Forge for 1.4.7 and for 1.5.2 require extra libraries.
// Figure out the forge version and add it as a component
// (the code still comes from the jar mod installed above)
if (zipFileRoot.exists("/forgeversion.properties")) {
zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive);
QuaZipFile file(&zipFile);
if (!file.open(QIODevice::ReadOnly)) {
if (zipFile.exists("/forgeversion.properties")) {
auto file = zipFile.goToFile("forgeversion.properties");
if (!file) {
// Really shouldn't happen, but error handling shall not be forgotten
emit failed(tr("Unable to open \"forgeversion.properties\""));
return;
}
QByteArray forgeVersionData = file.readAll();
file.close();
auto forgeVersionData = file->readAll();
INIFile iniFile;
iniFile.loadFile(forgeVersionData);
QString major, minor, revision, build;

View file

@ -48,9 +48,12 @@ namespace {
QString getCreditsHtml()
{
QFile dataFile(":/documents/credits.html");
dataFile.open(QIODevice::ReadOnly);
if (!dataFile.open(QIODevice::ReadOnly)) {
qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!";
return {};
}
QString fileContent = QString::fromUtf8(dataFile.readAll());
dataFile.close();
return fileContent.arg(QObject::tr("%1 Developers").arg(BuildConfig.LAUNCHER_DISPLAYNAME), QObject::tr("MultiMC Developers"),
QObject::tr("With special thanks to"));

View file

@ -43,6 +43,7 @@
#include <QMessageBox>
#include "FileIgnoreProxy.h"
#include "QObjectPtr.h"
#include "archive/ExportToZipTask.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui_ExportInstanceDialog.h"
@ -150,7 +151,7 @@ void ExportInstanceDialog::doExport()
return;
}
auto task = makeShared<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true, true);
auto task = makeShared<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true);
connect(task.get(), &Task::failed, this,
[this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });

View file

@ -38,6 +38,7 @@
#include "ModFolderPage.h"
#include "ui/dialogs/ExportToModListDialog.h"
#include "ui/dialogs/InstallLoaderDialog.h"
#include "ui_ExternalResourcesPage.h"
#include <QAbstractItemModel>
@ -145,9 +146,10 @@ void ModFolderPage::downloadMods()
auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
if (!profile->getModLoaders().has_value()) {
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
if (handleNoModLoader()) {
return;
}
}
m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance);
connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close);
@ -201,9 +203,10 @@ void ModFolderPage::updateMods(bool includeDeps)
auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
if (!profile->getModLoaders().has_value()) {
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
if (handleNoModLoader()) {
return;
}
}
if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) {
QMessageBox::critical(this, tr("Error"), tr("Mod updates are unavailable when metadata is disabled!"));
return;
@ -305,9 +308,10 @@ void ModFolderPage::changeModVersion()
auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
if (!profile->getModLoaders().has_value()) {
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
if (handleNoModLoader()) {
return;
}
}
if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) {
QMessageBox::critical(this, tr("Error"), tr("Mod updates are unavailable when metadata is disabled!"));
return;
@ -385,3 +389,34 @@ bool NilModFolderPage::shouldDisplay() const
{
return m_model->dir().exists();
}
// Helper function so this doesn't need to be duplicated 3 times
inline bool ModFolderPage::handleNoModLoader()
{
int resp = QMessageBox::question(this, this->tr("Missing Mod Loader"),
this->tr("You need to install a compatible mod loader before installing mods. Would you like to do so?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
switch (resp) {
case QMessageBox::Yes: {
// Should be safe
auto profile = static_cast<MinecraftInstance*>(this->m_instance)->getPackProfile();
InstallLoaderDialog dialog(profile, QString(), this);
bool ret = dialog.exec();
this->m_container->refreshContainer();
// returning negation of dialog.exec which'll be true if the install loader dialog got canceled/closed
// and false if the user went through and installed a loader
return !ret;
}
case QMessageBox::No: {
// Nothing happens the dialog is already closing
// returning true so the caller doesn't go and continue with opening it's dialog without a mod loader
return true;
}
default: {
// Unreachable
// returning true as a safety measure
return true;
}
}
}

View file

@ -45,6 +45,8 @@
class ModFolderPage : public ExternalResourcesPage {
Q_OBJECT
inline bool handleNoModLoader();
public:
explicit ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> model, QWidget* parent = nullptr);
virtual ~ModFolderPage() = default;

View file

@ -180,5 +180,30 @@ void LogView::scrollToBottom()
void LogView::findNext(const QString& what, bool reverse)
{
find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0));
if (what.isEmpty())
return;
const QTextDocument::FindFlags flags(reverse ? QTextDocument::FindBackward : 0);
if (find(what, flags))
return;
QTextCursor cursor = textCursor();
if (reverse) {
if (cursor.atEnd())
return;
cursor.movePosition(QTextCursor::End);
} else {
if (cursor.atStart())
return;
cursor.movePosition(QTextCursor::Start);
}
cursor = document()->find(what, cursor, flags);
if (!cursor.isNull())
setTextCursor(cursor);
}

View file

@ -809,7 +809,7 @@ bool PrismUpdaterApp::callAppImageUpdate()
auto appimage_path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE"));
QProcess proc = QProcess();
qDebug() << "Calling: AppImageUpdate" << appimage_path;
proc.setProgram(FS::PathCombine(m_rootPath, "bin", "AppImageUpdate-x86_64.AppImage"));
proc.setProgram(FS::PathCombine(m_rootPath, "bin", "AppImageUpdate.AppImage"));
proc.setArguments({ appimage_path });
auto result = proc.startDetached();
if (!result)

View file

@ -2,8 +2,7 @@ cmake_minimum_required(VERSION 3.15)
project(LocalPeer)
if(Launcher_QT_VERSION_MAJOR EQUAL 6)
find_package(Qt6 COMPONENTS Core Network Core5Compat REQUIRED)
list(APPEND LocalPeer_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat)
find_package(Qt6 COMPONENTS Core Network REQUIRED)
endif()
set(SINGLE_SOURCES

View file

@ -2,22 +2,6 @@
This folder has third-party or otherwise external libraries needed for other parts to work.
## gamemode
A performance optimization daemon.
See [github repo](https://github.com/FeralInteractive/gamemode).
BSD-3-Clause licensed
## cmark
The C reference implementation of CommonMark, a standardized Markdown spec.
See [github_repo](https://github.com/commonmark/cmark).
BSD2 licensed.
## javacheck
Simple Java tool that prints the JVM details - version and platform bitness.
@ -99,20 +83,6 @@ Canonical implementation of the murmur2 hash, taken from [SMHasher](https://gith
Public domain (the author disclaimed the copyright).
## QR-Code-generator
A simple library for generating QR codes
See [github repo](https://github.com/nayuki/QR-Code-generator).
MIT
## quazip
A zip manipulation library.
LGPL 2.1 with linking exception.
## rainbow
Color functions extracted from [KGuiAddons](https://inqlude.org/libraries/kguiaddons.html). Used for adaptive text coloring.

@ -1 +0,0 @@
Subproject commit 3460cd809b6dd311b58e92733ece2fc956224fd2

@ -1 +0,0 @@
Subproject commit 1f820dc98d0a520c175433bcbb0098327d82aac6

View file

@ -1,7 +0,0 @@
cmake_minimum_required(VERSION 3.15)
project(gamemode
VERSION 1.6.1)
add_library(gamemode)
target_include_directories(gamemode PUBLIC include)
target_link_libraries(gamemode PUBLIC ${CMAKE_DL_LIBS})

View file

@ -1,337 +0,0 @@
/*
Copyright (c) 2017-2019, Feral Interactive
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Feral Interactive nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef CLIENT_GAMEMODE_H
#define CLIENT_GAMEMODE_H
/*
* GameMode supports the following client functions
* Requests are refcounted in the daemon
*
* int gamemode_request_start() - Request gamemode starts
* 0 if the request was sent successfully
* -1 if the request failed
*
* int gamemode_request_end() - Request gamemode ends
* 0 if the request was sent successfully
* -1 if the request failed
*
* GAMEMODE_AUTO can be defined to make the above two functions apply during static init and
* destruction, as appropriate. In this configuration, errors will be printed to stderr
*
* int gamemode_query_status() - Query the current status of gamemode
* 0 if gamemode is inactive
* 1 if gamemode is active
* 2 if gamemode is active and this client is registered
* -1 if the query failed
*
* int gamemode_request_start_for(pid_t pid) - Request gamemode starts for another process
* 0 if the request was sent successfully
* -1 if the request failed
* -2 if the request was rejected
*
* int gamemode_request_end_for(pid_t pid) - Request gamemode ends for another process
* 0 if the request was sent successfully
* -1 if the request failed
* -2 if the request was rejected
*
* int gamemode_query_status_for(pid_t pid) - Query status of gamemode for another process
* 0 if gamemode is inactive
* 1 if gamemode is active
* 2 if gamemode is active and this client is registered
* -1 if the query failed
*
* const char* gamemode_error_string() - Get an error string
* returns a string describing any of the above errors
*
* Note: All the above requests can be blocking - dbus requests can and will block while the daemon
* handles the request. It is not recommended to make these calls in performance critical code
*/
#include <stdbool.h>
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include <sys/types.h>
static char internal_gamemode_client_error_string[512] = { 0 };
/**
* Load libgamemode dynamically to dislodge us from most dependencies.
* This allows clients to link and/or use this regardless of runtime.
* See SDL2 for an example of the reasoning behind this in terms of
* dynamic versioning as well.
*/
static volatile int internal_libgamemode_loaded = 1;
/* Typedefs for the functions to load */
typedef int (*api_call_return_int)(void);
typedef const char* (*api_call_return_cstring)(void);
typedef int (*api_call_pid_return_int)(pid_t);
/* Storage for functors */
static api_call_return_int REAL_internal_gamemode_request_start = NULL;
static api_call_return_int REAL_internal_gamemode_request_end = NULL;
static api_call_return_int REAL_internal_gamemode_query_status = NULL;
static api_call_return_cstring REAL_internal_gamemode_error_string = NULL;
static api_call_pid_return_int REAL_internal_gamemode_request_start_for = NULL;
static api_call_pid_return_int REAL_internal_gamemode_request_end_for = NULL;
static api_call_pid_return_int REAL_internal_gamemode_query_status_for = NULL;
/**
* Internal helper to perform the symbol binding safely.
*
* Returns 0 on success and -1 on failure
*/
__attribute__((always_inline)) static inline int internal_bind_libgamemode_symbol(void* handle,
const char* name,
void** out_func,
size_t func_size,
bool required)
{
void* symbol_lookup = NULL;
char* dl_error = NULL;
/* Safely look up the symbol */
symbol_lookup = dlsym(handle, name);
dl_error = dlerror();
if (required && (dl_error || !symbol_lookup)) {
snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string), "dlsym failed - %s", dl_error);
return -1;
}
/* Have the symbol correctly, copy it to make it usable */
memcpy(out_func, &symbol_lookup, func_size);
return 0;
}
/**
* Loads libgamemode and needed functions
*
* Returns 0 on success and -1 on failure
*/
__attribute__((always_inline)) static inline int internal_load_libgamemode(void)
{
/* We start at 1, 0 is a success and -1 is a fail */
if (internal_libgamemode_loaded != 1) {
return internal_libgamemode_loaded;
}
/* Anonymous struct type to define our bindings */
struct binding {
const char* name;
void** functor;
size_t func_size;
bool required;
} bindings[] = {
{ "real_gamemode_request_start", (void**)&REAL_internal_gamemode_request_start, sizeof(REAL_internal_gamemode_request_start),
true },
{ "real_gamemode_request_end", (void**)&REAL_internal_gamemode_request_end, sizeof(REAL_internal_gamemode_request_end), true },
{ "real_gamemode_query_status", (void**)&REAL_internal_gamemode_query_status, sizeof(REAL_internal_gamemode_query_status), false },
{ "real_gamemode_error_string", (void**)&REAL_internal_gamemode_error_string, sizeof(REAL_internal_gamemode_error_string), true },
{ "real_gamemode_request_start_for", (void**)&REAL_internal_gamemode_request_start_for,
sizeof(REAL_internal_gamemode_request_start_for), false },
{ "real_gamemode_request_end_for", (void**)&REAL_internal_gamemode_request_end_for, sizeof(REAL_internal_gamemode_request_end_for),
false },
{ "real_gamemode_query_status_for", (void**)&REAL_internal_gamemode_query_status_for,
sizeof(REAL_internal_gamemode_query_status_for), false },
};
void* libgamemode = NULL;
/* Try and load libgamemode */
libgamemode = dlopen("libgamemode.so.0", RTLD_NOW);
if (!libgamemode) {
/* Attempt to load unversioned library for compatibility with older
* versions (as of writing, there are no ABI changes between the two -
* this may need to change if ever ABI-breaking changes are made) */
libgamemode = dlopen("libgamemode.so", RTLD_NOW);
if (!libgamemode) {
snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string), "dlopen failed - %s", dlerror());
internal_libgamemode_loaded = -1;
return -1;
}
}
/* Attempt to bind all symbols */
for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) {
struct binding* binder = &bindings[i];
if (internal_bind_libgamemode_symbol(libgamemode, binder->name, binder->functor, binder->func_size, binder->required)) {
internal_libgamemode_loaded = -1;
return -1;
};
}
/* Success */
internal_libgamemode_loaded = 0;
return 0;
}
/**
* Redirect to the real libgamemode
*/
__attribute__((always_inline)) static inline const char* gamemode_error_string(void)
{
/* If we fail to load the system gamemode, or we have an error string already, return our error
* string instead of diverting to the system version */
if (internal_load_libgamemode() < 0 || internal_gamemode_client_error_string[0] != '\0') {
return internal_gamemode_client_error_string;
}
return REAL_internal_gamemode_error_string();
}
/**
* Redirect to the real libgamemode
* Allow automatically requesting game mode
* Also prints errors as they happen.
*/
#ifdef GAMEMODE_AUTO
__attribute__((constructor))
#else
__attribute__((always_inline)) static inline
#endif
int gamemode_request_start(void)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
if (REAL_internal_gamemode_request_start() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
return 0;
}
/* Redirect to the real libgamemode */
#ifdef GAMEMODE_AUTO
__attribute__((destructor))
#else
__attribute__((always_inline)) static inline
#endif
int gamemode_request_end(void)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
if (REAL_internal_gamemode_request_end() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
return 0;
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_query_status(void)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_query_status == NULL) {
snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string),
"gamemode_query_status missing (older host?)");
return -1;
}
return REAL_internal_gamemode_query_status();
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_request_start_for(pid_t pid)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_request_start_for == NULL) {
snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string),
"gamemode_request_start_for missing (older host?)");
return -1;
}
return REAL_internal_gamemode_request_start_for(pid);
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_request_end_for(pid_t pid)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_request_end_for == NULL) {
snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string),
"gamemode_request_end_for missing (older host?)");
return -1;
}
return REAL_internal_gamemode_request_end_for(pid);
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_query_status_for(pid_t pid)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_query_status_for == NULL) {
snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string),
"gamemode_query_status_for missing (older host?)");
return -1;
}
return REAL_internal_gamemode_query_status_for(pid);
}
#endif // CLIENT_GAMEMODE_H

View file

@ -2,8 +2,7 @@ cmake_minimum_required(VERSION 3.15)
project(qdcss)
if(Launcher_QT_VERSION_MAJOR EQUAL 6)
find_package(Qt6 COMPONENTS Core Core5Compat REQUIRED)
list(APPEND qdcss_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat)
find_package(Qt6 COMPONENTS Core REQUIRED)
endif()
set(QDCSS_SOURCES

@ -1 +0,0 @@
Subproject commit 3fd3b299b875fbd2beac4894b8a870d80022cad7

@ -1 +0,0 @@
Subproject commit c4369ae1d8955cae20c4ab40b9813ef4b60e48be

@ -1 +0,0 @@
Subproject commit 51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf

View file

@ -3,7 +3,6 @@
stdenv,
cmake,
cmark,
apple-sdk_11,
extra-cmake-modules,
gamemode,
jdk17,
@ -16,11 +15,8 @@
tomlplusplus,
zlib,
msaClientID ? null,
gamemodeSupport ? stdenv.hostPlatform.isLinux,
libarchive,
}:
assert lib.assertMsg (
gamemodeSupport -> stdenv.hostPlatform.isLinux
) "gamemodeSupport is only available on Linux.";
let
date =
@ -77,13 +73,12 @@ stdenv.mkDerivation {
cmark
kdePackages.qtbase
kdePackages.qtnetworkauth
kdePackages.quazip
qrencode
libarchive
tomlplusplus
zlib
]
++ lib.optionals stdenv.hostPlatform.isDarwin [ apple-sdk_11 ]
++ lib.optional gamemodeSupport gamemode;
++ lib.optional stdenv.hostPlatform.isLinux gamemode;
cmakeFlags = [
# downstream branding

View file

@ -51,7 +51,7 @@ assert lib.assertMsg (
) "textToSpeechSupport only has an effect on Linux.";
let
prismlauncher' = prismlauncher-unwrapped.override { inherit msaClientID gamemodeSupport; };
prismlauncher' = prismlauncher-unwrapped.override { inherit msaClientID; };
in
symlinkJoin {

View file

@ -29,6 +29,7 @@ set(Launcher_SVGFileName "${Launcher_SVGFileName}" PARENT_SCOPE)
set(Launcher_Desktop "program_info/${Launcher_AppID}.desktop" PARENT_SCOPE)
set(Launcher_mrpack_MIMEInfo "program_info/modrinth-mrpack-mime.xml" PARENT_SCOPE)
set(Launcher_MetaInfo "program_info/${Launcher_AppID}.metainfo.xml" PARENT_SCOPE)
set(Launcher_PNG_256 "program_info/${Launcher_AppID}_256.png" PARENT_SCOPE)
set(Launcher_SVG "program_info/${Launcher_SVGFileName}" PARENT_SCOPE)
set(Launcher_Branding_ICNS "program_info/prismlauncher.icns" PARENT_SCOPE)
set(Launcher_Branding_ICO "program_info/prismlauncher.ico")

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,7 +1,7 @@
{
"default-registry": {
"kind": "git",
"baseline": "1fddddc280dfed63956e15ef74f4321bc6a219c9",
"baseline": "2d6a6cf3ac9a7cc93942c3d289a2f9c661a6f4a7",
"repository": "https://github.com/microsoft/vcpkg"
},
"registries": [

View file

@ -1,7 +1,5 @@
{
"dependencies": [
"bzip2",
"cmark",
{
"name": "ecm",
"host": true
@ -14,6 +12,19 @@
"name": "pkgconf",
"host": true
},
"cmark",
{
"name": "libarchive",
"default-features": false,
"features": [
"bzip2",
"lz4",
"lzma",
"lzo",
"zstd"
]
},
"tomlplusplus",
"zlib"
]