diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..d2723b9 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,132 @@ +name: Build AppDrop binaries + +on: + push: + branches: [ iOS3iThink ] + workflow_dispatch: + +jobs: + ios5_10-armv7: + name: Cross-compile armv7 / iOS 5-10 + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + clang lld llvm llvm-dev \ + cmake make git wget zip dpkg \ + libplist-dev libplist-utils libssl-dev python3 + + - name: Bootstrap build-toolchain + run: | + mkdir -p build-toolchain + if [ ! -d build-toolchain/theos ]; then + git clone --recursive https://github.com/theos/theos.git build-toolchain/theos + fi + if [ ! -x build-toolchain/theos/toolchain/linux/iphone/bin/clang ]; then + curl -fsSL https://github.com/L1ghtmann/llvm-project/releases/latest/download/iOSToolchain-x86_64.tar.xz \ + | tar -xJf - -C build-toolchain/theos/toolchain + fi + if [ ! -d build-toolchain/theos/sdks/iPhoneOS10.3.sdk ]; then + tmpdir="$(mktemp -d)" + git clone --no-checkout --depth 1 --filter=blob:none https://github.com/theos/sdks.git "$tmpdir/sdks" + ( cd "$tmpdir/sdks" && git sparse-checkout set iPhoneOS10.3.sdk && git checkout ) + mkdir -p build-toolchain/theos/sdks + mv "$tmpdir/sdks/iPhoneOS10.3.sdk" build-toolchain/theos/sdks/ + rm -rf "$tmpdir" + fi + + - name: Build armv7 package + run: | + chmod +x build.sh + ./build.sh package FINALPACKAGE=1 + + - name: Upload armv7 artifact + uses: actions/upload-artifact@v4 + with: + name: AppDrop-armv7 + path: IPAInstaller/packages/* + + ios3-armv6: + name: Cross-compile armv6 / iOS 3 (iOS 5.1 SDK) + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + clang lld llvm llvm-dev \ + cmake make git wget zip dpkg \ + libplist-dev libplist-utils libssl-dev python3 + + - name: Build IPA + DEB + run: | + chmod +x ios3/build-ios3.sh + ./ios3/build-ios3.sh + + - name: Upload IPA + uses: actions/upload-artifact@v4 + with: + name: AppDrop-iOS3-armv6-ipa + path: ios3/build/AppDrop.ipa + + - name: Upload DEB + uses: actions/upload-artifact@v4 + with: + name: AppDrop-iOS3-armv6-deb + path: ios3/build/AppDrop.deb + + universal-deb: + name: Assemble universal DEB (armv6 + armv7, separate slices) + runs-on: ubuntu-24.04 + needs: [ ios5_10-armv7, ios3-armv6 ] + steps: + - name: Download armv7 DEB + uses: actions/download-artifact@v4 + with: + name: AppDrop-armv7 + path: artifacts/armv7 + + - name: Download armv6 DEB + uses: actions/download-artifact@v4 + with: + name: AppDrop-iOS3-armv6-deb + path: artifacts/armv6-deb + + - name: Build universal DEB + run: | + set -euo pipefail + rm -rf merge + mkdir -p merge/root + + armv7_deb="$(find artifacts/armv7 -name '*.deb' | head -n1)" + armv6_deb="$(find artifacts/armv6-deb -name '*.deb' | head -n1)" + if [ -z "$armv7_deb" ] || [ -z "$armv6_deb" ]; then + echo "ERROR: missing armv7 or armv6 DEB artifact" >&2 + exit 1 + fi + + dpkg-deb -R "$armv6_deb" merge/root + tmp_armv7="$(mktemp -d)" + dpkg-deb -x "$armv7_deb" "$tmp_armv7" + cp "$tmp_armv7/Applications/AppDrop.app/AppDrop.armv7" merge/root/Applications/AppDrop.app/AppDrop.armv7 + chmod 0755 merge/root/Applications/AppDrop.app/AppDrop.armv7 + + mkdir -p merge/out + dpkg-deb -Zgzip -b merge/root merge/out/AppDrop-universal.deb >/dev/null + mv merge/out/AppDrop-universal.deb merge/AppDrop-universal.deb + echo "Assembled universal DEB:" + dpkg-deb -I merge/AppDrop-universal.deb | sed -n '1,20p' + + - name: Upload universal DEB + uses: actions/upload-artifact@v4 + with: + name: AppDrop-universal-deb + path: merge/AppDrop-universal.deb diff --git a/BUILD-LINUX.md b/BUILD-LINUX.md index d3db05c..ce028f8 100644 --- a/BUILD-LINUX.md +++ b/BUILD-LINUX.md @@ -12,7 +12,7 @@ memes dylibs liees (il demarre donc sur iPad 1 / iOS 5.1.1 comme avant). ./build.sh clean ``` -Le `.deb` (et le `.ipa` si besoin) sort dans `IPAInstaller/packages/`. +Le `.deb` (et l`.ipa` si besoin) sort dans `IPAInstaller/packages/`. ## Contenu de build-toolchain/ diff --git a/IPAInstaller/ADNumberPickerSheet.m b/IPAInstaller/ADNumberPickerSheet.m index 4c4fe88..e5b78a2 100644 --- a/IPAInstaller/ADNumberPickerSheet.m +++ b/IPAInstaller/ADNumberPickerSheet.m @@ -8,13 +8,28 @@ @interface ADNumberPickerSheet () @property (nonatomic, strong) NSArray *values; // NSNumber @property (nonatomic, strong) NSArray *labels; // NSString -@property (nonatomic, copy) void (^onPick)(NSInteger value); @property (nonatomic, strong) UIControl *dim; @property (nonatomic, strong) UIView *panel; @property (nonatomic, strong) UIPickerView *picker; +@property (nonatomic, copy) void (^onPick)(NSInteger value); @end -@implementation ADNumberPickerSheet +@implementation ADNumberPickerSheet { + // iOS 3: blocks aren't ObjC objects, so a synthesized @property(copy) block + // setter calls objc_setProperty(copy=YES) → -copyWithZone: on the block → + // objc_msgSend dereferences a zeroed isa → Bus error (signal 10). This is the + // crash when opening "Apps per row" / "Home tiles per row". Back the block + // manually through the C blocks runtime (_Block_copy/_Block_release) — see + // AppDropBlocks.h (AD_BLOCK_ACCESSORS). + void (^_onPickBlock)(NSInteger value); +} +@dynamic onPick; +AD_BLOCK_ACCESSORS(onPick, setOnPick, _onPickBlock, void(^)(NSInteger value)) + +- (void)dealloc { + if (_onPickBlock) _Block_release((const void *)_onPickBlock); + [super dealloc]; +} + (void)presentInView:(UIView *)host title:(NSString *)title diff --git a/IPAInstaller/AppDelegate.m b/IPAInstaller/AppDelegate.m index 0a05715..3e618ad 100644 --- a/IPAInstaller/AppDelegate.m +++ b/IPAInstaller/AppDelegate.m @@ -441,6 +441,16 @@ - (void)setupAppearance { // harmless belt-and-suspenders for anything not reached directly. if ([IOS6Theme isDefaultTheme]) return; + // The entire UIAppearance proxy system is iOS 5.0+. On iOS 3.x, +[UINavigationBar + // appearance] / +[UITabBar appearance] / +[UIBarButtonItem appearance] are themselves + // unrecognized selectors on the CLASS — sending them throws NSInvalidArgumentException + // before any respondsToSelector: on the (never-returned) proxy can guard us. That is the + // "+[UINavigationBar appearance]: unrecognized selector" launch crash seen after switching + // to a non-default theme. Dark/colour themes already do the real styling per-instance via + // [IOS6Theme applyToNavigationBar:] / applyToTabBar: (ADNavigationController + the tab bar), + // so the proxy here is purely belt-and-suspenders — skip it entirely when unavailable. + if (![UINavigationBar respondsToSelector:@selector(appearance)]) return; + id navProxy = [UINavigationBar appearance]; { UIImage *navBg = [IOS6Theme navBarBackground]; diff --git a/IPAInstaller/AppDetailViewController.m b/IPAInstaller/AppDetailViewController.m index 59ebb2b..78c4369 100644 --- a/IPAInstaller/AppDetailViewController.m +++ b/IPAInstaller/AppDetailViewController.m @@ -110,7 +110,7 @@ - (void)setContentOffset:(CGPoint)offset { CGContextStrokeEllipseInRect(ctx, CGRectMake(4, 4, 18, 18)); CGContextMoveToPoint(ctx, 13, 13); CGContextAddLineToPoint(ctx, 13, 7.5); CGContextStrokePath(ctx); CGContextMoveToPoint(ctx, 13, 13); CGContextAddLineToPoint(ctx, 17.5, 15); CGContextStrokePath(ctx); - img = UIGraphicsGetImageFromCurrentImageContext(); + img = [UIGraphicsGetImageFromCurrentImageContext() retain]; UIGraphicsEndImageContext(); return img; } @@ -128,7 +128,7 @@ - (void)setContentOffset:(CGPoint)offset { CGContextMoveToPoint(ctx, 13, 4); CGContextAddLineToPoint(ctx, 13, 15); CGContextStrokePath(ctx); // shaft CGContextMoveToPoint(ctx, 8, 10); CGContextAddLineToPoint(ctx, 13, 15); CGContextAddLineToPoint(ctx, 18, 10); CGContextStrokePath(ctx); // arrowhead CGContextMoveToPoint(ctx, 6, 20); CGContextAddLineToPoint(ctx, 20, 20); CGContextStrokePath(ctx); // tray - img = UIGraphicsGetImageFromCurrentImageContext(); + img = [UIGraphicsGetImageFromCurrentImageContext() retain]; UIGraphicsEndImageContext(); return img; } @@ -141,7 +141,7 @@ - (void)setContentOffset:(CGPoint)offset { [[UIColor whiteColor] setFill]; [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(8, 7, 3.4, 12) cornerRadius:1.5] fill]; [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(15, 7, 3.4, 12) cornerRadius:1.5] fill]; - img = UIGraphicsGetImageFromCurrentImageContext(); + img = [UIGraphicsGetImageFromCurrentImageContext() retain]; UIGraphicsEndImageContext(); return img; } @@ -158,7 +158,7 @@ - (void)setContentOffset:(CGPoint)offset { [p closePath]; [[UIColor whiteColor] setFill]; [p fill]; - img = UIGraphicsGetImageFromCurrentImageContext(); + img = [UIGraphicsGetImageFromCurrentImageContext() retain]; UIGraphicsEndImageContext(); return img; } @@ -174,7 +174,7 @@ - (void)setContentOffset:(CGPoint)offset { CGContextSetLineCap(ctx, kCGLineCapRound); CGContextMoveToPoint(ctx, 8, 8); CGContextAddLineToPoint(ctx, 18, 18); CGContextStrokePath(ctx); CGContextMoveToPoint(ctx, 18, 8); CGContextAddLineToPoint(ctx, 8, 18); CGContextStrokePath(ctx); - img = UIGraphicsGetImageFromCurrentImageContext(); + img = [UIGraphicsGetImageFromCurrentImageContext() retain]; UIGraphicsEndImageContext(); return img; } @@ -448,12 +448,12 @@ - (void)probeEncryptionForCurrentVersion { self.encryptionProbed = YES; NSString *url = self.app[@"url"]; if (url.length == 0) return; - __weak AppDetailViewController *weakSelf = self; + AD_WEAK AppDetailViewController *weakSelf = self; [MachOInspector inspectURL:url completion:^(MachOInspectionResult r) { AppDetailViewController *s = weakSelf; if (!s) return; if (r != MachOInspectionResultEncrypted) return; // decrypted/unknown → nothing to do - __weak AppDetailViewController *ws = s; + AD_WEAK AppDetailViewController *ws = s; if (s.allowVersionSwitch) { // Auto path (catalog/search): resolve SILENTLY — no transient 🔒 banner. Switch to a // clean build in place if one exists; otherwise say there's none. @@ -498,7 +498,7 @@ - (void)findDecryptedAlternativeThen:(void (^)(NSDictionary *decryptedOrNil))don - (void)probeCandidates:(NSArray *)candidates index:(NSUInteger)i then:(void (^)(NSDictionary *))done { if (i >= candidates.count) { if (done) done(nil); return; } NSDictionary *v = [candidates objectAtIndex:i]; - __weak AppDetailViewController *weakSelf = self; + AD_WEAK AppDetailViewController *weakSelf = self; [MachOInspector inspectURL:v[@"url"] completion:^(MachOInspectionResult r) { AppDetailViewController *s = weakSelf; if (!s) return; @@ -1104,6 +1104,7 @@ - (void)refreshInstallButtonTitle { - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)o { return YES; } diff --git a/IPAInstaller/AppDropLauncher.sh b/IPAInstaller/AppDropLauncher.sh new file mode 100644 index 0000000..631d1e9 --- /dev/null +++ b/IPAInstaller/AppDropLauncher.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# AppDrop launcher: pick the right slice by iOS VERSION only. +# iOS 3-4 -> AppDrop.armv6 +# iOS 5+ -> AppDrop.armv7 +# No external tools (head/awk/defaults/plutil don't exist on stock iOS 3) — +# ProductVersion is parsed from SystemVersion.plist with shell builtins only. +DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" + +SV="/System/Library/CoreServices/SystemVersion.plist" +VERSION="" +found=0 +if [ -r "$SV" ]; then + while IFS= read -r line; do + if [ "$found" = 1 ]; then + case "$line" in + *""*) + VERSION="${line#*}" + VERSION="${VERSION%%*}" + break + ;; + esac + fi + case "$line" in + *ProductVersion*""*) + VERSION="${line#*}" + VERSION="${VERSION%%*}" + break + ;; + *ProductVersion*) found=1 ;; + esac + done < "$SV" +fi + +MAJOR="${VERSION%%.*}" +case "$MAJOR" in + ''|*[!0-9]*) MAJOR=0 ;; +esac + +if [ "$MAJOR" -ge 5 ]; then + [ -x "$DIR/AppDrop.armv7" ] && exec "$DIR/AppDrop.armv7" "$@" + [ -x "$DIR/AppDrop.armv6" ] && exec "$DIR/AppDrop.armv6" "$@" + exit 1 +fi +# iOS 3-4, or version unknown: armv6 first. +[ -x "$DIR/AppDrop.armv6" ] && exec "$DIR/AppDrop.armv6" "$@" +[ -x "$DIR/AppDrop.armv7" ] && exec "$DIR/AppDrop.armv7" "$@" +exit 1 diff --git a/IPAInstaller/AppRowCell.m b/IPAInstaller/AppRowCell.m index e61b57b..4acdd05 100644 --- a/IPAInstaller/AppRowCell.m +++ b/IPAInstaller/AppRowCell.m @@ -7,7 +7,23 @@ @interface AppRowCell () @property (nonatomic, copy) NSArray *appsCache; @end -@implementation AppRowCell +@implementation AppRowCell { + void (^_onTileTapBlock)(NSDictionary *); + BOOL (^_isAppSelectedBlock)(NSDictionary *); +} + +// iOS 3: blocks aren't ObjC objects, so the synthesized copy setters crash in +// objc_msgSend. Back them manually via the C blocks runtime — see +// AppDropBlocks.h (AD_BLOCK_ACCESSORS). +@dynamic onTileTap, isAppSelectedBlock; +AD_BLOCK_ACCESSORS(onTileTap, setOnTileTap, _onTileTapBlock, void(^)(NSDictionary *)) +AD_BLOCK_ACCESSORS(isAppSelectedBlock, setIsAppSelectedBlock, _isAppSelectedBlock, BOOL(^)(NSDictionary *)) + +- (void)dealloc { + if (_onTileTapBlock) _Block_release((const void *)_onTileTapBlock); + if (_isAppSelectedBlock) _Block_release((const void *)_isAppSelectedBlock); + [super dealloc]; +} // v3.0: the catalogue grid is configured by an explicit COLUMN COUNT (chosen via the native wheel // picker in Settings → Affichage), not a 0–1 density. Idiom-aware default: iPhone 1 = single-column @@ -91,7 +107,7 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr - (void)ensureTileCount { while ((NSInteger)self.tiles.count < self.tilesPerRow) { AppTileView *t = [[AppTileView alloc] initWithFrame:CGRectZero]; - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; t.onTap = ^(NSDictionary *app) { if (ws.onTileTap) ws.onTileTap(app); }; diff --git a/IPAInstaller/AppTileView.m b/IPAInstaller/AppTileView.m index 2f3392e..120f1ba 100644 --- a/IPAInstaller/AppTileView.m +++ b/IPAInstaller/AppTileView.m @@ -20,7 +20,20 @@ @interface AppTileView () @property (nonatomic, strong) UIImageView *selectionBadge; @end -@implementation AppTileView +@implementation AppTileView { + void (^_onTapBlock)(NSDictionary *); +} + +// iOS 3: blocks aren't ObjC objects, so the synthesized copy setter crashes in +// objc_msgSend. Back onTap manually via the C blocks runtime — see +// AppDropBlocks.h (AD_BLOCK_ACCESSORS). +@dynamic onTap; +AD_BLOCK_ACCESSORS(onTap, setOnTap, _onTapBlock, void(^)(NSDictionary *)) + +- (void)dealloc { + if (_onTapBlock) _Block_release((const void *)_onTapBlock); + [super dealloc]; +} + (void)setSuppressTileText:(BOOL)suppress { _suppressTileText = suppress; } @@ -36,7 +49,7 @@ + (NSString *)humanSize:(long long)bytes { + (UIImage *)cardImageForSize:(CGSize)size { if (size.width < 2 || size.height < 2) return nil; static NSMutableDictionary *cache = nil; - if (!cache) cache = [NSMutableDictionary dictionary]; + if (!cache) cache = [[NSMutableDictionary dictionary] retain]; NSString *k = [NSString stringWithFormat:@"%@|%.0fx%.0f", [IOS6Theme currentThemeID], size.width, size.height]; UIImage *hit = cache[k]; if (hit) return hit; @@ -61,7 +74,7 @@ + (UIImage *)cardImageForSize:(CGSize)size { // accent ring. The white ring/disc keep the badge legible on top of ANY app icon. + (UIImage *)checkGlyphOn { static NSMutableDictionary *cache = nil; - if (!cache) cache = [NSMutableDictionary dictionary]; + if (!cache) cache = [[NSMutableDictionary dictionary] retain]; NSString *k = [IOS6Theme currentThemeID] ?: @"_"; UIImage *hit = cache[k]; if (hit) return hit; @@ -88,7 +101,7 @@ + (UIImage *)checkGlyphOn { + (UIImage *)checkGlyphOff { static NSMutableDictionary *cache = nil; - if (!cache) cache = [NSMutableDictionary dictionary]; + if (!cache) cache = [[NSMutableDictionary dictionary] retain]; NSString *k = [IOS6Theme currentThemeID] ?: @"_"; UIImage *hit = cache[k]; if (hit) return hit; @@ -176,7 +189,7 @@ - (void)setApp:(NSDictionary *)app { UIImage *cached = iconUrl.length ? [[IconLoader shared] cachedImageForURL:iconUrl targetSize:sz] : nil; self.iconView.image = cached; // already force-decoded by IconLoader → pure composite if (iconUrl.length && !cached) { - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; self.currentIconReq = [[IconLoader shared] loadImageForURL:iconUrl targetSize:sz via:nil completion:^(UIImage *img) { if (!img) return; __strong typeof(self) s = ws; @@ -289,10 +302,10 @@ - (void)tapped { // Normal browse: brief press flash, then open the detail screen. self.alpha = 0.5; NSDictionary *appCopy = self.app; - void (^onTap)(NSDictionary *) = [self.onTap copy]; + void (^onTap)(NSDictionary *) = _onTapBlock; // already heap-copied by our setter; do NOT send -copy (iOS 3) [UIView animateWithDuration:0.18 animations:^{ self.alpha = 1.0; } completion:^(BOOL done) { onTap(appCopy); }]; } -@end +@end \ No newline at end of file diff --git a/IPAInstaller/CatalogAppCell.m b/IPAInstaller/CatalogAppCell.m index cd1f725..9642644 100644 --- a/IPAInstaller/CatalogAppCell.m +++ b/IPAInstaller/CatalogAppCell.m @@ -71,6 +71,7 @@ - (void)layoutSubviews { - (void)dealloc { // Cell destroyed (e.g. table emptied / screen popped) → drop any still-pending icon request. if (_iconReq) [[IconLoader shared] cancelRequest:_iconReq]; + [super dealloc]; } - (void)prepareForReuse { diff --git a/IPAInstaller/CatalogViewController.m b/IPAInstaller/CatalogViewController.m index 2f29fd3..936c5df 100644 --- a/IPAInstaller/CatalogViewController.m +++ b/IPAInstaller/CatalogViewController.m @@ -244,7 +244,11 @@ - (void)performSearch { - (NSInteger)pageSize { if (_pageSize <= 0) { NSUInteger cores = [[NSProcessInfo processInfo] activeProcessorCount]; - NSUInteger ram = (NSUInteger)[[NSProcessInfo processInfo] physicalMemory]; + // -[NSProcessInfo physicalMemory] is iOS 4.0+; on iOS 3.1.3 assume 256 MB + // (all 3.x-era hardware) so the mono-core branch below stays correct. + NSUInteger ram = 256UL * 1024 * 1024; + if ([[NSProcessInfo processInfo] respondsToSelector:@selector(physicalMemory)]) + ram = (NSUInteger)[[NSProcessInfo processInfo] physicalMemory]; // Mono-core A4 (iPad 1 / iPhone 4): a modest 50 — bigger runway, still negligible cost // (off-main query, only visible cells are built). Dual-core 512 MB → 100, 1 GB+ → 200 so a // hard fling on iPad 4 / iPhone 5 essentially never outruns the loader. @@ -503,7 +507,7 @@ - (void)addSelectedToLater { - (void)addSelectedToFolder { NSArray *batch = [self.selectedAppsByPk.allValues copy]; if (!batch.count) return; - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; [FolderPicker presentAddToFolderFrom:self completion:^(NSString *cid) { if (!cid) return; for (NSDictionary *app in batch) [[CollectionStore shared] addApp:app toCollection:cid]; @@ -558,6 +562,7 @@ - (void)gridDensityDidChange { - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; } - (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection:(NSInteger)s { @@ -586,7 +591,7 @@ - (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexP NSInteger n = [self tilesPerRowForWidth:tv.bounds.size.width]; row.tilesPerRow = n; [row setContentRasterized:!self.gridScrolling]; // rasterize at rest, plain while scrolling - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; row.selectionMode = self.selectionMode; // Selection lookup callback — runs for each tile during layout so the // check overlay reflects the source-of-truth dict, not stale visuals. @@ -661,7 +666,7 @@ - (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexP // Capture self WEAKLY: the returned token is now stored on the cell (cell.iconReq), which the // table retains, so a strong capture here would form a permanent VC↔block retain cycle and // leak the whole controller after leaving the screen. (AppTileView already captures weakly.) - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; cell.iconReq = [[IconLoader shared] loadImageForURL:iconUrl targetSize:sz via:nil diff --git a/IPAInstaller/CategorySuggestViewController.m b/IPAInstaller/CategorySuggestViewController.m index 8634c35..ba1d6cb 100644 --- a/IPAInstaller/CategorySuggestViewController.m +++ b/IPAInstaller/CategorySuggestViewController.m @@ -29,7 +29,20 @@ @interface CategorySuggestViewController () @property (nonatomic, assign) BOOL sending; @end -@implementation CategorySuggestViewController +@implementation CategorySuggestViewController { + // iOS 3: blocks aren't ObjC objects, so a synthesized @property(copy) block + // setter calls objc_setProperty(copy=YES) → -copyWithZone: on the block → + // Bus error (signal 10). Back the block manually via the C blocks runtime — + // see AppDropBlocks.h (AD_BLOCK_ACCESSORS). + void (^_onPickBlock)(NSString *category, NSString *subgenre); +} +@dynamic onPick; +AD_BLOCK_ACCESSORS(onPick, setOnPick, _onPickBlock, void(^)(NSString *category, NSString *subgenre)) + +- (void)dealloc { + if (_onPickBlock) _Block_release((const void *)_onPickBlock); + [super dealloc]; +} - (instancetype)initWithBundleId:(NSString *)bid name:(NSString *)name { if ((self = [super initWithStyle:UITableViewStyleGrouped])) { diff --git a/IPAInstaller/CategoryTileView.m b/IPAInstaller/CategoryTileView.m index 2a81f71..ab2ac08 100644 --- a/IPAInstaller/CategoryTileView.m +++ b/IPAInstaller/CategoryTileView.m @@ -2,6 +2,26 @@ #import "IconLoader.h" #import "IOS6Theme.h" +// Pick up to n distinct random elements from arr (Fisher–Yates partial shuffle). +// Returns a new autoreleased NSArray; if arr has <= n items, returns a shuffled copy of all of them. +static NSArray *pickN(NSArray *arr, NSUInteger n) { + if (arr.count == 0) return @[]; + NSMutableArray *m = [arr mutableCopy]; + NSUInteger count = m.count; + NSUInteger take = (n < count) ? n : count; + for (NSUInteger i = 0; i < take; i++) { + NSUInteger j = i + arc4random_uniform((uint32_t)(count - i)); + [m exchangeObjectAtIndex:i withObjectAtIndex:j]; + } + return [m subarrayWithRange:NSMakeRange(0, take)]; +} + +// iOS 3 has no ObjC NSBlock class: sending a block any ObjC message (e.g. +// -copyWithZone: from a @property(copy) setter, or -copy) crashes in +// objc_msgSend. onTap/onDelete are therefore backed by manual accessors that +// copy/release through the C blocks runtime — see AppDropBlocks.h +// (AD_BLOCK_ACCESSORS), force-included via AppDropCompat.h. + @interface CategoryTileView () @property (nonatomic, strong) UIImageView *bgView; @property (nonatomic, strong) UIImageView *iconView; @@ -22,18 +42,20 @@ @interface CategoryTileView () @property (nonatomic, strong) UIButton *deleteBadge; // top-left ⊗ (edit mode, folders only) @end -// Pick up to n random items from a pool (partial Fisher–Yates) — gives the mosaic variety per visit. -static NSArray *pickN(NSArray *pool, NSUInteger n) { - if (pool.count <= n) return pool ?: @[]; - NSMutableArray *m = [pool mutableCopy]; - for (NSUInteger i = 0; i < n; i++) { - NSUInteger j = i + arc4random_uniform((uint32_t)(m.count - i)); - [m exchangeObjectAtIndex:i withObjectAtIndex:j]; - } - return [m subarrayWithRange:NSMakeRange(0, n)]; +@implementation CategoryTileView { + void (^_onTapBlock)(void); + void (^_onDeleteBlock)(void); } -@implementation CategoryTileView +@dynamic onTap, onDelete; +AD_BLOCK_ACCESSORS(onTap, setOnTap, _onTapBlock, void(^)(void)) +AD_BLOCK_ACCESSORS(onDelete, setOnDelete, _onDeleteBlock, void(^)(void)) + +- (void)dealloc { + if (_onTapBlock) _Block_release((const void *)_onTapBlock); + if (_onDeleteBlock) _Block_release((const void *)_onDeleteBlock); + [super dealloc]; +} #pragma mark - Drawing helpers @@ -42,7 +64,7 @@ @implementation CategoryTileView + (UIImage *)cardBgForSize:(CGSize)size { if (size.width < 2 || size.height < 2) return nil; static NSMutableDictionary *cache = nil; - if (!cache) cache = [NSMutableDictionary dictionary]; + if (!cache) cache = [[NSMutableDictionary dictionary] retain]; // Key includes the theme id so cards are re-drawn (not served stale) after a theme switch. NSString *key = [NSString stringWithFormat:@"%@|%.0fx%.0f", [IOS6Theme currentThemeID], size.width, size.height]; UIImage *cached = cache[key]; @@ -137,7 +159,7 @@ + (UIImage *)resizeGripGlyph { CGContextSetLineCap(ctx, kCGLineCapRound); CGContextMoveToPoint(ctx, 9, 17); CGContextAddLineToPoint(ctx, 17, 9); CGContextStrokePath(ctx); CGContextMoveToPoint(ctx, 12.5, 18.5); CGContextAddLineToPoint(ctx, 18.5, 12.5); CGContextStrokePath(ctx); - img = UIGraphicsGetImageFromCurrentImageContext(); + img = [UIGraphicsGetImageFromCurrentImageContext() retain]; UIGraphicsEndImageContext(); return img; } @@ -161,7 +183,7 @@ + (UIImage *)deleteBadgeGlyph { CGContextSetLineCap(ctx, kCGLineCapRound); CGContextMoveToPoint(ctx, 9, 9); CGContextAddLineToPoint(ctx, 17, 17); CGContextStrokePath(ctx); CGContextMoveToPoint(ctx, 17, 9); CGContextAddLineToPoint(ctx, 9, 17); CGContextStrokePath(ctx); - img = UIGraphicsGetImageFromCurrentImageContext(); + img = [UIGraphicsGetImageFromCurrentImageContext() retain]; UIGraphicsEndImageContext(); return img; } @@ -181,7 +203,7 @@ + (UIImage *)chevronGlyph { CGContextAddLineToPoint(ctx, 9, 7); CGContextAddLineToPoint(ctx, 4, 11); CGContextStrokePath(ctx); - img = UIGraphicsGetImageFromCurrentImageContext(); + img = [UIGraphicsGetImageFromCurrentImageContext() retain]; UIGraphicsEndImageContext(); return img; } @@ -313,7 +335,7 @@ - (void)configureWithLabel:(NSString *)label } else { self.iconView.image = nil; // placeholder applied in layoutSubviews [self setNeedsLayout]; - __weak typeof(self) weakSelf = self; + AD_WEAK typeof(self) weakSelf = self; [[IconLoader shared] loadImageForURL:iconURL targetSize:sz via:nil completion:^(UIImage *img) { __strong typeof(self) s = weakSelf; @@ -331,7 +353,7 @@ - (void)reshuffleIconURL:(NSString *)iconURL { UIImage *cached = [[IconLoader shared] cachedImageForURL:iconURL targetSize:sz]; if (cached) { self.iconView.image = cached; return; } // Keep the current image visible (no placeholder flash) until the new one loads. - __weak typeof(self) weakSelf = self; + AD_WEAK typeof(self) weakSelf = self; [[IconLoader shared] loadImageForURL:iconURL targetSize:sz via:nil completion:^(UIImage *img) { __strong typeof(self) s = weakSelf; @@ -395,7 +417,7 @@ - (void)startMosaic { - (void)loadMosaicSlots { CGSize sz = CGSizeMake(64, 64); NSInteger gen = self.mosaicGen; - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; for (NSUInteger i = 0; i < self.mosaicSlots.count; i++) { if ([self.mosaicImgs[i] isKindOfClass:[UIImage class]]) continue; id slotURL = self.mosaicSlots[i]; @@ -428,7 +450,7 @@ - (void)backfillSlot:(NSUInteger)slot gen:(NSInteger)gen { CGSize sz = CGSizeMake(64, 64); UIImage *c = [[IconLoader shared] cachedImageForURL:next targetSize:sz]; if (c) { self.mosaicImgs[slot] = c; [self compositeMosaic]; return; } - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; [[IconLoader shared] loadImageForURL:next targetSize:sz via:nil completion:^(UIImage *img) { __strong typeof(self) s = ws; if (!s || s.mosaicGen != gen) return; if (img) { s.mosaicImgs[slot] = img; [s compositeMosaic]; } @@ -537,7 +559,7 @@ - (void)applyTheme { - (void)tapped { if (!self.onTap) return; - void (^cb)(void) = [self.onTap copy]; + void (^cb)(void) = _onTapBlock; // already a heap block (our setter Block_copy'd it); do NOT send -copy (iOS 3 blocks aren't ObjC objects) self.alpha = 0.55; [UIView animateWithDuration:0.16 animations:^{ self.alpha = 1.0; } completion:^(BOOL done) { cb(); }]; diff --git a/IPAInstaller/CategoryViewController.m b/IPAInstaller/CategoryViewController.m index 14f99cc..14d5423 100644 --- a/IPAInstaller/CategoryViewController.m +++ b/IPAInstaller/CategoryViewController.m @@ -51,7 +51,7 @@ @interface CategoryViewController () 0) { NSString *m = [NSString stringWithUTF8String:buf]; - cached = m.length ? m : @"?"; + cached = [(m.length ? m : @"?") retain]; } else { - cached = @"?"; + cached = [@"?" retain]; } }); return cached; diff --git a/IPAInstaller/FeedbackViewController.m b/IPAInstaller/FeedbackViewController.m index b9affee..09ed6f4 100644 --- a/IPAInstaller/FeedbackViewController.m +++ b/IPAInstaller/FeedbackViewController.m @@ -123,7 +123,7 @@ - (void)viewDidLoad { name:UIKeyboardWillHideNotification object:nil]; } -- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; [self applyLayout]; } @@ -155,7 +155,14 @@ - (void)applyLayout { #pragma mark - Keyboard - (void)kbShow:(NSNotification *)n { - CGRect f = [n.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + // iOS 3 backport: UIKeyboardFrameEndUserInfoKey is weak-imported (iOS 3.2+) and resolves to + // NULL on 3.1.3 — referencing the symbol crashes, and userInfo[NULL] would throw. Look the + // key up by its literal string (its value equals its name) and fall back to the iOS-2 + // UIKeyboardBoundsUserInfoKey, which 3.x actually posts. + NSValue *fv = [n.userInfo objectForKey:@"UIKeyboardFrameEndUserInfoKey"]; + if (!fv) fv = [n.userInfo objectForKey:@"UIKeyboardBoundsUserInfoKey"]; + if (!fv) return; + CGRect f = [fv CGRectValue]; f = [self.view convertRect:f fromView:nil]; self.kbCutoff = f.origin.y; // top of the keyboard, in this view's coords [self applyLayout]; diff --git a/IPAInstaller/FilePickerViewController.m b/IPAInstaller/FilePickerViewController.m index 3c3a4e3..cea6454 100644 --- a/IPAInstaller/FilePickerViewController.m +++ b/IPAInstaller/FilePickerViewController.m @@ -8,7 +8,20 @@ @interface FilePickerViewController () @property (nonatomic, strong) NSArray *ipas; // .ipa file names @end -@implementation FilePickerViewController +@implementation FilePickerViewController { + void (^_onPickBlock)(NSString *); +} + +// iOS 3: blocks aren't ObjC objects, so the synthesized copy setter crashes in +// objc_msgSend. Back onPick manually via the C blocks runtime — see +// AppDropBlocks.h (AD_BLOCK_ACCESSORS). +@dynamic onPick; +AD_BLOCK_ACCESSORS(onPick, setOnPick, _onPickBlock, void(^)(NSString *)) + +- (void)dealloc { + if (_onPickBlock) _Block_release((const void *)_onPickBlock); + [super dealloc]; +} - (instancetype)initWithDirectory:(NSString *)dir { if ((self = [super initWithStyle:UITableViewStyleGrouped])) { _directory = [dir copy]; } diff --git a/IPAInstaller/FilterViewController.h b/IPAInstaller/FilterViewController.h index 89076ec..c46d9b9 100644 --- a/IPAInstaller/FilterViewController.h +++ b/IPAInstaller/FilterViewController.h @@ -5,7 +5,7 @@ @interface FilterViewController : UIViewController @property (nonatomic, strong) CatalogFilter *filter; -@property (nonatomic, weak) id delegate; +@property (nonatomic, assign) id delegate; // MRC: non-zeroing weak (was weak under ARC) @end @protocol FilterViewControllerDelegate diff --git a/IPAInstaller/FolderPicker.m b/IPAInstaller/FolderPicker.m index 3d4dd97..7c979a1 100644 --- a/IPAInstaller/FolderPicker.m +++ b/IPAInstaller/FolderPicker.m @@ -3,13 +3,26 @@ #import "Localization.h" @interface FolderPicker () -@property (nonatomic, copy) void (^completion)(NSString *); @property (nonatomic, strong) NSArray *folderIds; // parallel to the action-sheet folder buttons @property (nonatomic, assign) NSInteger newFolderIndex; +@property (nonatomic, copy) void (^completion)(NSString *); @property (nonatomic, strong) FolderPicker *selfRef; // keep alive across the async sheet/alert @end -@implementation FolderPicker +@implementation FolderPicker { + // iOS 3: blocks aren't ObjC objects, so a synthesized @property(copy) block + // setter calls objc_setProperty(copy=YES) → -copyWithZone: on the block → + // Bus error (signal 10). Back the block manually through the C blocks runtime + // (_Block_copy/_Block_release) — see AppDropBlocks.h (AD_BLOCK_ACCESSORS). + void (^_completionBlock)(NSString *); +} +@dynamic completion; +AD_BLOCK_ACCESSORS(completion, setCompletion, _completionBlock, void(^)(NSString *)) + +- (void)dealloc { + if (_completionBlock) _Block_release((const void *)_completionBlock); + [super dealloc]; +} + (void)presentAddToFolderFrom:(UIViewController *)vc completion:(void (^)(NSString *))completion { FolderPicker *p = [[FolderPicker alloc] init]; diff --git a/IPAInstaller/HTTPSClient.m b/IPAInstaller/HTTPSClient.m index e83e4a5..30259bd 100644 --- a/IPAInstaller/HTTPSClient.m +++ b/IPAInstaller/HTTPSClient.m @@ -73,7 +73,7 @@ static int ad_locked_drbg_random(void *p, unsigned char *out, size_t len) { static NSMutableDictionary *cache = nil; static dispatch_once_t once; - dispatch_once(&once, ^{ cache = [NSMutableDictionary dictionary]; }); + dispatch_once(&once, ^{ cache = [[NSMutableDictionary dictionary] retain]; }); @synchronized (cache) { NSString *hit = [cache objectForKey:host]; if (hit) return hit; } NSString *u = [NSString stringWithFormat:@"https://8.8.8.8/resolve?name=%@&type=A", host]; diff --git a/IPAInstaller/IOS6Theme.m b/IPAInstaller/IOS6Theme.m index 17ffa9f..3329f13 100644 --- a/IPAInstaller/IOS6Theme.m +++ b/IPAInstaller/IOS6Theme.m @@ -73,7 +73,7 @@ static CGFloat ad_lum(UIColor *c) { // on their bars; dark accents are bright so they pop on dark. Both are auto-contrasted at runtime. static NSArray *ad_themeList(void) { static NSArray *list = nil; - if (!list) list = @[ + if (!list) list = [@[ @{@"id":@"default", @"nameKey":@"theme.default", @"dark":@NO, @"color":[UIColor colorWithRed:0.118 green:0.435 blue:0.902 alpha:1.0]}, // ── Light colour themes ──────────────────────────────────────────────── @{@"id":@"c_rouge", @"nameKey":@"theme.red", @"dark":@NO, @"color":[UIColor colorWithRed:0.84 green:0.22 blue:0.22 alpha:1.0]}, @@ -93,7 +93,7 @@ static CGFloat ad_lum(UIColor *c) { @{@"id":@"orange", @"nameKey":@"theme.orange", @"dark":@YES, @"color":[UIColor colorWithRed:0.98 green:0.58 blue:0.20 alpha:1.0]}, @{@"id":@"vert", @"nameKey":@"theme.green", @"dark":@YES, @"color":[UIColor colorWithRed:0.30 green:0.74 blue:0.44 alpha:1.0]}, @{@"id":@"turquoise", @"nameKey":@"theme.teal", @"dark":@YES, @"color":[UIColor colorWithRed:0.20 green:0.74 blue:0.74 alpha:1.0]}, - ]; + ] retain]; return list; } static NSDictionary *ad_themeForID(NSString *tid){ @@ -165,7 +165,7 @@ @implementation IOS6Theme + (void)initialize { if (self != [IOS6Theme class]) return; - gCache = [NSMutableDictionary dictionary]; + gCache = [[NSMutableDictionary dictionary] retain]; NSString *t = [[NSUserDefaults standardUserDefaults] stringForKey:@"IPAInstall.Theme"]; NSDictionary *d = ad_themeForID(t ?: @"default"); gThemeID = d[@"id"]; @@ -304,12 +304,12 @@ + (UIColor *)chatBackgroundColor { + (UIImage *)cellBackground { static UIImage *img = nil; - if (!img) img = stretchable(@"cell-bg", 1, 1); + if (!img) img = [stretchable(@"cell-bg", 1, 1) retain]; return img; } + (UIImage *)cellSelectedBackground { static UIImage *img = nil; - if (!img) img = stretchable(@"cell-selected", 1, 1); + if (!img) img = [stretchable(@"cell-selected", 1, 1) retain]; return img; } + (UIImage *)cardBackground { @@ -323,7 +323,7 @@ + (UIImage *)cardBackground { + (UIImage *)linenPattern { static UIImage *img = nil; - if (!img) img = [UIImage imageNamed:@"linen"]; + if (!img) img = [[UIImage imageNamed:@"linen"] retain]; return img; } + (UIImage *)linenBackground { return [self linenPattern]; } @@ -570,7 +570,8 @@ + (void)styleTextField:(UITextField *)tf { // (the black bands the user saw — even in the default theme after a live switch). We force a CLEAR // backgroundView so the table backdrop shows through, and recolour the label light on dark themes. + (void)styleGroupedHeaderFooter:(UIView *)view { - if (![view isKindOfClass:[UITableViewHeaderFooterView class]]) return; + Class hfClass = NSClassFromString(@"UITableViewHeaderFooterView"); + if (!hfClass || ![view isKindOfClass:hfClass]) return; UITableViewHeaderFooterView *hf = (UITableViewHeaderFooterView *)view; UIView *clear = [[UIView alloc] initWithFrame:hf.bounds]; clear.backgroundColor = [UIColor clearColor]; diff --git a/IPAInstaller/IconLoader.m b/IPAInstaller/IconLoader.m index 27da6b9..c6dd3bf 100644 --- a/IPAInstaller/IconLoader.m +++ b/IPAInstaller/IconLoader.m @@ -1,7 +1,48 @@ #import "IconLoader.h" #import "HTTPSClient.h" #import "IOS5Compat.h" -#import +#import // header only (types/CGImageSourceRef) — NOT linked; see dlopen below +#import + +// --------------------------------------------------------------------------- +// Runtime-loaded ImageIO (dlopen/dlsym) — one binary for both iOS 3 and iOS 4. +// +// ImageIO lives at DIFFERENT paths depending on the OS: +// iOS 4.0+ : /System/Library/Frameworks/ImageIO.framework (public) +// iOS 3.x : /System/Library/PrivateFrameworks/ImageIO.framework (private) +// A hard LC_LOAD_DYLIB on either path makes dyld abort at launch on the OTHER +// OS. So the binary is NOT linked against ImageIO at all: we dlopen() the +// public path first (iOS 4+), fall back to the private path (iOS 3), and +// resolve every function/key via dlsym(). If both fail (or any symbol is +// missing) decodeAndResize: falls back to the iOS 2.0 UIImage path below. +// --------------------------------------------------------------------------- +typedef CGImageSourceRef (*ADCGImageSourceCreateWithDataFn)(CFDataRef, CFDictionaryRef); +typedef CGImageRef (*ADCGImageSourceCreateThumbnailAtIndexFn)(CGImageSourceRef, size_t, CFDictionaryRef); + +static void *ADImageIOHandle(void) { + static void *handle = NULL; + static dispatch_once_t once; + dispatch_once(&once, ^{ + handle = dlopen("/System/Library/Frameworks/ImageIO.framework/ImageIO", + RTLD_LAZY | RTLD_LOCAL); // iOS 4+ + if (!handle) + handle = dlopen("/System/Library/PrivateFrameworks/ImageIO.framework/ImageIO", + RTLD_LAZY | RTLD_LOCAL); // iOS 3.x + }); + return handle; +} + +static void *ADImageIOSym(const char *name) { + void *h = ADImageIOHandle(); + return h ? dlsym(h, name) : NULL; +} + +// kCGImageSource* keys are CFStringRef GLOBALS, so dlsym returns a POINTER to +// the CFStringRef — dereference it (guarded) to get the actual key. +static CFStringRef ADImageIOKey(const char *name) { + CFStringRef *p = (CFStringRef *)ADImageIOSym(name); + return p ? *p : NULL; +} // One pending icon request. Returned to the caller (AppTileView) as an opaque cancel token so a // reused cell can drop the icon it no longer needs. Several requests can share one URL key (dedup): @@ -9,11 +50,24 @@ // request short-circuits instead of decoding — this is what drains the stale backlog during a // fast fling so the currently-visible tiles' icons resolve immediately. @interface ADIconReq : NSObject -@property (nonatomic, copy) void (^completion)(UIImage *); @property (nonatomic, copy) NSString *key; @property (nonatomic, assign) BOOL cancelled; +// iOS 3: blocks aren't ObjC objects, so a synthesized copy setter would crash in +// objc_msgSend. Back the completion manually via the C blocks runtime — see +// AppDropBlocks.h (AD_BLOCK_ACCESSORS). +- (void (^)(UIImage *))completion; +- (void)setCompletion:(void (^)(UIImage *))blk; +@end +@implementation ADIconReq { + void (^_completionBlock)(UIImage *); +} +AD_BLOCK_ACCESSORS(completion, setCompletion, _completionBlock, void(^)(UIImage *)) +- (void)dealloc { + if (_completionBlock) _Block_release((const void *)_completionBlock); + [_key release]; + [super dealloc]; +} @end -@implementation ADIconReq @end @interface IconLoader () @property (nonatomic, strong) NSCache *cache; // decoded UIImages (RAM) @@ -81,7 +135,11 @@ - (instancetype)init { // iPad 1 (256 MB). Anything evicted is re-loaded from disk in ~ms, not re-downloaded. // Scale the RAM cache to the device: A4 256 MB devices get a tiny cap, 512 MB the // (unchanged) 240/28 MB, newer devices a larger one. Eviction just re-reads disk. - NSUInteger ram = (NSUInteger)[[NSProcessInfo processInfo] physicalMemory]; + // -[NSProcessInfo physicalMemory] is iOS 4.0+; on iOS 3.1.3 fall back to the + // smallest bucket (every 3.x device has 128-256 MB anyway). + NSUInteger ram = 0; + if ([[NSProcessInfo processInfo] respondsToSelector:@selector(physicalMemory)]) + ram = (NSUInteger)[[NSProcessInfo processInfo] physicalMemory]; if (ram <= 300 * 1024 * 1024) { _cache.countLimit = 120; _cache.totalCostLimit = 14 * 1024 * 1024; @@ -96,11 +154,18 @@ - (instancetype)init { // ivar instead of touching UIScreen off-main (same value, just thread-safe). CGFloat s = [UIScreen mainScreen].scale; _screenScale = s > 0 ? s : 1.0; - _pending = [NSMutableDictionary dictionary]; - _failedAt = [NSMutableDictionary dictionary]; + // MRC (-fno-objc-arc): these are singleton-lifetime ivars, so they must be OWNED. + // Convenience constructors (+dictionary/+array) return autoreleased objects; assigning + // them straight to an ivar leaves a dangling pointer once the pool drains → the download + // threads later message freed memory (SIGBUS in objc_msgSend). alloc/init gives us +1. + _pending = [[NSMutableDictionary alloc] init]; + _failedAt = [[NSMutableDictionary alloc] init]; _downloadQueue = [[NSOperationQueue alloc] init]; - _downloadQueue.name = @"icon-download"; + // -[NSOperationQueue setName:] is iOS 4.0+; iOS 3.x throws unrecognized + // selector. The name is debug-only (Instruments label), so guard it. + if ([_downloadQueue respondsToSelector:@selector(setName:)]) + _downloadQueue.name = @"icon-download"; // Bounded concurrency now ACTUALLY applies to the HTTPS path (icons run as // operations here). 8 fills the visible page a bit faster without the old // "90 simultaneous TLS handshakes" thrash that spiked CPU+RAM on old devices. @@ -108,13 +173,14 @@ - (instancetype)init { // thrash the one core. >=2 cores keeps the unchanged 8. NSUInteger cores = ADRecommendedConcurrency(); _downloadQueue.maxConcurrentOperationCount = (cores <= 1) ? 2 : MIN((NSUInteger)8, cores * 4); - _queuedOps = [NSMutableArray array]; + _queuedOps = [[NSMutableArray alloc] init]; // MRC: owned ivar (see note above) // Separate queue for disk read+decode. Kept apart from the network queue so that // suspend/resume (scroll gating) only pauses NETWORK fetches — already-cached icons // keep decoding off disk during a scroll. Tiny concurrency to avoid disk thrash. _diskDecodeQueue = [[NSOperationQueue alloc] init]; - _diskDecodeQueue.name = @"icon-disk"; + if ([_diskDecodeQueue respondsToSelector:@selector(setName:)]) + _diskDecodeQueue.name = @"icon-disk"; // Disk read + decode runs on a LOW-priority background queue (never starves the UI thread). // Mono-core A4 → 2: while one op blocks on a slow cold NAND read, the other decodes an // already-read icon (real overlap on slow storage, negligible context-switch vs NAND latency). @@ -124,7 +190,7 @@ - (instancetype)init { // Persistent on-disk thumbnail cache (survives eviction AND app relaunch → an // icon downloaded once is instant forever; the #1 real-world speedup). NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; - _diskDir = [caches stringByAppendingPathComponent:@"appdrop-icons"]; + _diskDir = [[caches stringByAppendingPathComponent:@"appdrop-icons"] copy]; // owned (+1) under MRC [[NSFileManager defaultManager] createDirectoryAtPath:_diskDir withIntermediateDirectories:YES attributes:nil error:NULL]; [self pruneDiskCacheAsync]; @@ -146,6 +212,7 @@ - (void)onMemoryWarning { - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; } - (NSString *)keyForURL:(NSString *)url size:(CGSize)size { @@ -175,7 +242,9 @@ - (id)loadImageForURL:(NSString *)url // One request token (returned as an opaque cancel handle). Dedup: if a request for this key is // already in flight, attach this token to the existing waiter list; otherwise start a new op. - ADIconReq *req = [[ADIconReq alloc] init]; + // MRC: autorelease the token — the waiter array retains it; the caller's + // `strong` property retains it again if it keeps the handle. + ADIconReq *req = [[[ADIconReq alloc] init] autorelease]; req.completion = completion; req.key = key; @synchronized (self.pending) { @@ -226,7 +295,7 @@ - (void)enqueueNetworkFetch:(NSString *)finalURL key:(NSString *)key size:(CGSiz BOOL isHTTPS = [[finalURL lowercaseString] hasPrefix:@"https://"]; NSTimeInterval timeout = 30; // mbedTLS handshake on iPad 1 can take several seconds. - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ __strong typeof(ws) self = ws; if (!self) return; if ([self keyAbandoned:key]) return; // every requester scrolled away → skip the network fetch @@ -282,7 +351,12 @@ - (void)handleFetched:(NSData *)d key:(NSString *)key size:(CGSize)size { - (void)fireWaiters:(NSString *)key withImage:(UIImage *)img { NSArray *waiters = nil; @synchronized (self.pending) { - waiters = self.pending[key]; + // MRC (-fno-objc-arc): the pending dict holds the ONLY +1 on this array. + // -removeObjectForKey: below releases it → refcount 0 → it deallocs while we + // still hold a dangling `waiters` pointer, and the for-loop then enumerates + // freed memory → EXC_BAD_ACCESS (SIGBUS) in objc_msgSend (the "random" crash + // on the icon-download thread). retain+autorelease keeps it alive for the loop. + waiters = [[self.pending[key] retain] autorelease]; [self.pending removeObjectForKey:key]; } for (ADIconReq *r in waiters) { @@ -318,22 +392,51 @@ - (UIImage *)decodeAndResize:(NSData *)data targetSize:(CGSize)targetSize { CGFloat scale = self.screenScale; CGSize px = CGSizeMake(targetSize.width * scale, targetSize.height * scale); - CGImageSourceRef src = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); - if (!src) return nil; - NSDictionary *opts = @{ - (__bridge id)kCGImageSourceCreateThumbnailFromImageAlways: (__bridge id)kCFBooleanTrue, - (__bridge id)kCGImageSourceCreateThumbnailWithTransform: (__bridge id)kCFBooleanTrue, - (__bridge id)kCGImageSourceThumbnailMaxPixelSize: @((int)MAX(px.width, px.height)), - }; - CGImageRef thumb = CGImageSourceCreateThumbnailAtIndex(src, 0, (__bridge CFDictionaryRef)opts); - CFRelease(src); + CGImageRef thumb = NULL; // the source image we draw (then downscale) + BOOL ownThumb = NO; // YES only if WE created `thumb` (must release it) + + // Preferred path: ImageIO thumbnailing — decodes + downsamples in one pass. + // Every function and key is resolved via dlsym() from the runtime-dlopen()ed + // ImageIO (public path on iOS 4+, private path on iOS 3 — see ADImageIOHandle). + // The binary itself carries NO ImageIO load command and NO ImageIO imports, so + // dyld is happy on both OSes. If the library or any symbol is missing, every + // pointer below is NULL and we fall through to the UIImage path. + ADCGImageSourceCreateWithDataFn srcCreate = + (ADCGImageSourceCreateWithDataFn)ADImageIOSym("CGImageSourceCreateWithData"); + ADCGImageSourceCreateThumbnailAtIndexFn thumbCreate = + (ADCGImageSourceCreateThumbnailAtIndexFn)ADImageIOSym("CGImageSourceCreateThumbnailAtIndex"); + if (srcCreate && thumbCreate) { + CGImageSourceRef src = srcCreate((__bridge CFDataRef)data, NULL); + if (src) { + CFStringRef kAlways = ADImageIOKey("kCGImageSourceCreateThumbnailFromImageAlways"); + CFStringRef kTransform = ADImageIOKey("kCGImageSourceCreateThumbnailWithTransform"); + CFStringRef kMaxPx = ADImageIOKey("kCGImageSourceThumbnailMaxPixelSize"); + NSMutableDictionary *opts = [NSMutableDictionary dictionary]; + if (kAlways) opts[(__bridge id)kAlways] = (__bridge id)kCFBooleanTrue; + if (kTransform) opts[(__bridge id)kTransform] = (__bridge id)kCFBooleanTrue; + if (kMaxPx) opts[(__bridge id)kMaxPx] = @((int)MAX(px.width, px.height)); + thumb = thumbCreate(src, 0, (__bridge CFDictionaryRef)opts); + if (thumb) ownThumb = YES; + CFRelease(src); + } + } + + // Fallback (iOS 3.1.3, or any ImageIO miss above): full-decode with the rock-solid + // iOS 2.0 API. We still downscale by drawing into the px-sized rounded context + // below, so the on-screen result is identical — just a little more transient RAM. + UIImage *full = nil; + if (!thumb) { + full = [UIImage imageWithData:data]; + thumb = full.CGImage; // owned by `full`; do NOT release it ourselves + ownThumb = NO; + } if (!thumb) return nil; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef ctx = CGBitmapContextCreate(NULL, (size_t)px.width, (size_t)px.height, 8, (size_t)px.width * 4, colorSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little); - if (!ctx) { CGColorSpaceRelease(colorSpace); CGImageRelease(thumb); return nil; } + if (!ctx) { CGColorSpaceRelease(colorSpace); if (ownThumb) CGImageRelease(thumb); return nil; } CGContextSetInterpolationQuality(ctx, kCGInterpolationMedium); CGFloat radius = targetSize.width * 0.21 * scale; @@ -346,7 +449,7 @@ - (UIImage *)decodeAndResize:(NSData *)data targetSize:(CGSize)targetSize { CGImageRef cg = CGBitmapContextCreateImage(ctx); UIImage *out = [UIImage imageWithCGImage:cg scale:scale orientation:UIImageOrientationUp]; CGImageRelease(cg); - CGImageRelease(thumb); + if (ownThumb) CGImageRelease(thumb); CGContextRelease(ctx); CGColorSpaceRelease(colorSpace); return out; diff --git a/IPAInstaller/InProcessInstaller.h b/IPAInstaller/InProcessInstaller.h new file mode 100644 index 0000000..f070b2d --- /dev/null +++ b/IPAInstaller/InProcessInstaller.h @@ -0,0 +1,36 @@ +#import + +// In-process IPA installer. +// +// Installs an .ipa by calling the private C function -MobileInstallationInstall- +// directly inside AppDrop, with no external helper binary. This is exactly what +// the «IPA Installer Console» (autopear) and AppSync's «appinst» do under the +// hood — they dlopen MobileInstallation.framework and call the same symbol. The +// difference is that those *packages* declare a firmware (>= 4.0) dependency, so +// Cydia refuses to install them on iOS 3.1.3, leaving the device with no +// /usr/bin/ipainstaller at all (the "ipainstaller not found" failure). +// +// MobileInstallationInstall itself ships in +// /System/Library/PrivateFrameworks/MobileInstallation.framework +// since iOS 2.0, with an unchanged signature, so calling it in-process works on +// iOS 3.1.3 the same way it does on iOS 5-7. On iOS 8+ this private C entry point +// is gone (replaced by LSApplicationWorkspace); we never reach this path there +// because an external installer is used / the modern-iOS branch saves the .ipa. +// +// Compiles under both ARC (Theos build) and MRC (-fno-objc-arc, the iOS 3 +// armv6 backport): it touches Foundation objects only through ordinary message +// sends and uses __bridge casts, which are valid (and no-ops) in both modes. +@interface InProcessInstaller : NSObject + +// YES if MobileInstallationInstall can be resolved on this device (iOS 2.0-7.x). +// On iOS 8+ the symbol is absent and this returns NO. ++ (BOOL)isAvailable; + +// Install the .ipa at -ipaPath-. Returns 0 on success, non-zero on failure. +// -outOutput- (optional) receives a human-readable status/error line shaped like +// ipainstaller's stdout ("Installed successfully." / "Install failed: …") +// so the existing success/Failed parsing in InstallManager keeps working +// unchanged. Synchronous — call OFF the main thread. ++ (int)installIPAAtPath:(NSString *)ipaPath capturedOutput:(NSString **)outOutput; + +@end diff --git a/IPAInstaller/InProcessInstaller.m b/IPAInstaller/InProcessInstaller.m new file mode 100644 index 0000000..2bdaa35 --- /dev/null +++ b/IPAInstaller/InProcessInstaller.m @@ -0,0 +1,120 @@ +#import "InProcessInstaller.h" +#import + +// MobileInstallation private framework path (since iOS 2.0). +static NSString *const kMIPath = + @"/System/Library/PrivateFrameworks/MobileInstallation.framework/MobileInstallation"; + +// MobileInstallationInstall(CFStringRef path, CFDictionaryRef params, +// MobileInstallationCallback callback, CFStringRef backpath) -> int (0 = OK) +// Signature is identical from iOS 2.0 through 7.x. The 3rd arg is a progress +// callback (PercentComplete / Status dict); we pass NULL — we only need the +// blocking result. The 4th arg ("backpath") is the source path again, matching +// what autopear's ipainstaller and AppSync's appinst pass. +typedef void (*MICallback)(CFDictionaryRef information); +typedef int (*MIInstallFn)(CFStringRef path, CFDictionaryRef parameters, + MICallback callback, CFStringRef backpath); + +// Resolve MobileInstallationInstall once. Returns NULL on iOS 8+ (symbol gone) +// or if the framework can't be loaded. +static MIInstallFn ResolveInstallFn(void) { + static MIInstallFn fn = NULL; + static BOOL tried = NO; + if (!tried) { + tried = YES; + void *image = dlopen([kMIPath fileSystemRepresentation], RTLD_LAZY); + if (image) { + fn = (MIInstallFn)dlsym(image, "MobileInstallationInstall"); + // Intentionally leak the handle: the framework stays mapped for the + // process lifetime and we may install more than once. + } + } + return fn; +} + +@implementation InProcessInstaller + ++ (BOOL)isAvailable { + return ResolveInstallFn() != NULL; +} + ++ (int)installIPAAtPath:(NSString *)ipaPath capturedOutput:(NSString **)outOutput { + NSString *name = [[ipaPath lastPathComponent] stringByDeletingPathExtension]; + if (!name.length) name = @"app"; + + MIInstallFn install = ResolveInstallFn(); + if (!install) { + if (outOutput) + *outOutput = @"Install failed: MobileInstallationInstall is unavailable on this iOS version."; + return -1; + } + + NSFileManager *fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath:ipaPath]) { + if (outOutput) + *outOutput = [NSString stringWithFormat:@"Install failed: %@ not found.", ipaPath]; + return -1; + } + + // MobileInstallation extracts (and may move/consume) the source it's handed, + // so install from a private staging copy. This keeps the caller's original + // localPath intact for the "Keep IPA after install" / save-to-Documents + // logic that runs after a successful install in InstallManager. + NSString *stageDir = [NSTemporaryDirectory() + stringByAppendingPathComponent:@"appdrop-inproc-install"]; + [fm removeItemAtPath:stageDir error:nil]; + NSError *mkErr = nil; + if (![fm createDirectoryAtPath:stageDir + withIntermediateDirectories:YES + attributes:nil + error:&mkErr]) { + if (outOutput) + *outOutput = [NSString stringWithFormat:@"Install failed: could not create staging dir (%@).", + mkErr.localizedDescription ?: @"unknown"]; + return -1; + } + + NSString *stagePath = [stageDir stringByAppendingPathComponent:@"install.ipa"]; + [fm removeItemAtPath:stagePath error:nil]; + NSError *copyErr = nil; + if (![fm copyItemAtPath:ipaPath toPath:stagePath error:©Err]) { + [fm removeItemAtPath:stageDir error:nil]; + if (outOutput) + *outOutput = [NSString stringWithFormat:@"Install failed: could not stage .ipa (%@).", + copyErr.localizedDescription ?: @"unknown"]; + return -1; + } + + // ApplicationType = User → installs as a normal home-screen app (not a + // system app). Same parameter dict autopear/appinst use for the C path. + NSDictionary *params = [NSDictionary dictionaryWithObject:@"User" + forKey:@"ApplicationType"]; + + int rc = -1; + @try { + rc = install((__bridge CFStringRef)stagePath, + (__bridge CFDictionaryRef)params, + NULL, + (__bridge CFStringRef)stagePath); + } @catch (NSException *ex) { + if (outOutput) + *outOutput = [NSString stringWithFormat:@"Install failed: %@", [ex reason] ?: [ex name]]; + [fm removeItemAtPath:stageDir error:nil]; + return -1; + } + + // Clean up staging (MobileInstallation copies what it needs out of it). + [fm removeItemAtPath:stageDir error:nil]; + + if (outOutput) { + // Shape the message like ipainstaller's stdout so InstallManager's + // existing success-detection ("successfully" / "Installed (.+?) successfully") + // and failure formatting keep working without changes. + *outOutput = (rc == 0) + ? [NSString stringWithFormat:@"Installed %@ successfully.", name] + : [NSString stringWithFormat:@"Install failed: MobileInstallationInstall returned %d.", rc]; + } + return rc; +} + +@end diff --git a/IPAInstaller/InstallManager.m b/IPAInstaller/InstallManager.m index ab45398..966536f 100644 --- a/IPAInstaller/InstallManager.m +++ b/IPAInstaller/InstallManager.m @@ -4,6 +4,7 @@ #import "ParallelDownloader.h" #import "Localization.h" #import "MachOInspector.h" +#import "InProcessInstaller.h" #import "IPAPackage.h" // #163 — read the .ipa's real bundle id to verify the install on-device #include #include @@ -96,9 +97,16 @@ - (instancetype)init { userInfo:nil repeats:YES]; CPLog(@" InstallManager.init: notification observers"); + // iOS 3 backport: UIApplicationDidEnterBackgroundNotification is a weak-imported + // iOS 4.0+ symbol. On 3.1.3 it resolves to NULL, and reading its value to pass as + // the `name:` argument dereferences 0 -> EXC_BAD_ACCESS (SIGBUS) at launch, the moment + // InstallManager is first touched (opening any app detail page). iOS 3 has no + // multitasking / background state anyway, so we register with the constant's literal + // string value: identical behaviour on iOS 4+ (the observer fires on backgrounding), + // and a harmless no-op observer on iOS 3 (the notification is simply never posted). [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveJobsToDisk) - name:UIApplicationDidEnterBackgroundNotification + name:@"UIApplicationDidEnterBackgroundNotification" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveJobsToDisk) @@ -374,7 +382,7 @@ - (void)startInstallWithURL:(NSString *)url - (void)startAutonomousInstallWithURL:(NSString *)url completion:(void (^)(NSString *, NSError *))completion { NSString *jobId = [NSString stringWithFormat:@"local-%@", - [[NSUUID UUID] UUIDString] ?: [NSString stringWithFormat:@"%lu", (unsigned long)[NSDate date].timeIntervalSince1970]]; + [[NSUUID UUID] UUIDString] ?: [NSString stringWithFormat:@"%lu", (unsigned long)[[NSDate date] timeIntervalSince1970]]]; InstallJob *job = [[InstallJob alloc] init]; job.jobId = jobId; @@ -562,11 +570,18 @@ - (void)attemptDownloadForJob:(InstallJob *)job __block long long lastReceived = 0; __block long long lastTotal = 0; // #171: latest known total size, for the near-done guard below - __block NSDate *lastTick = [NSDate date]; + // NOTE (iOS 3 / MRC): under -fno-objc-arc, object-typed __block variables are + // NOT retained when the block is copied. An autoreleased `[NSDate date]` stored + // here is dead as soon as the enclosing autorelease pool drains — and the + // download blocks below run 30s+ later on a background thread, so any message + // to it (e.g. timeIntervalSinceNow) crashes in objc_msgSend (EXC_BAD_ACCESS). + // Store wall-clock as a primitive NSTimeInterval (double) instead — no object + // lifetime to manage, identical behaviour on iOS 3–10. + __block NSTimeInterval lastTick = CFAbsoluteTimeGetCurrent(); // Slow-mirror detection state. We sample the byte counter every kSlowCheckWindow // seconds; if avg throughput in that window is under the threshold AND we have // retry budget, we trip slowAbort which the isCancelled block returns YES for. - __block NSDate *windowStart = [NSDate date]; + __block NSTimeInterval windowStart = CFAbsoluteTimeGetCurrent(); __block long long windowStartBytes = 0; __block BOOL slowAbort = NO; // #169: tripped on the first progress tick if free disk < kDLSpaceFactor × file size. @@ -578,7 +593,9 @@ - (void)attemptDownloadForJob:(InstallJob *)job // at "0%" forever). We trip slowAbort so it retries on a fresh mirror (and, out of retry budget, // fails cleanly) instead of hanging indefinitely. __block long long stallBytes = 0; - __block NSDate *stallSince = [NSDate date]; + // iOS3/MRC: primitive NSTimeInterval, same reasoning as lastTick/windowStart above — + // an autoreleased NSDate captured by these long-lived blocks dangles under MRC. + __block NSTimeInterval stallSince = CFAbsoluteTimeGetCurrent(); // Stream count from Settings (default 4). 1 disables parallelism and // ParallelDownloader transparently falls back to the legacy single-stream @@ -594,7 +611,7 @@ - (void)attemptDownloadForJob:(InstallJob *)job BOOL autoSwitchMirror = ([prefs objectForKey:@"IPAInstall.AutoSwitchMirror"] == nil) ? YES : [prefs boolForKey:@"IPAInstall.AutoSwitchMirror"]; - __weak InstallJob *weakJob = job; + AD_WEAK InstallJob *weakJob = job; [ParallelDownloader downloadURL:job.url toFile:localPath streamCount:streams @@ -608,11 +625,11 @@ - (void)attemptDownloadForJob:(InstallJob *)job // #278: no NEW bytes for 90 s → the connection is dead. Trip slowAbort to retry/fail cleanly // rather than sit at "0%" forever (the slow-mirror window above is skipped when the user // turned AutoSwitchMirror OFF, so this absolute guard must be unconditional). - if (-[stallSince timeIntervalSinceNow] > 90.0) { slowAbort = YES; return YES; } + if (CFAbsoluteTimeGetCurrent() - stallSince > 90.0) { slowAbort = YES; return YES; } // Only consider slow-mirror abort if the user left auto-switch ON (#171 level 2) AND we // still have retry budget — otherwise there's no point dropping the connection. if (autoSwitchMirror && attempt < kMaxMirrorAttempts - 1) { - NSTimeInterval elapsed = -[windowStart timeIntervalSinceNow]; + NSTimeInterval elapsed = CFAbsoluteTimeGetCurrent() - windowStart; if (elapsed >= kSlowCheckWindow) { long long delta = lastReceived - windowStartBytes; double bps = elapsed > 0 ? delta / elapsed : 0; @@ -628,7 +645,7 @@ - (void)attemptDownloadForJob:(InstallJob *)job return YES; } // Healthy speed (or a near-done mirror we're letting finish) — reset the window. - windowStart = [NSDate date]; + windowStart = CFAbsoluteTimeGetCurrent(); windowStartBytes = lastReceived; } } @@ -647,8 +664,8 @@ - (void)attemptDownloadForJob:(InstallJob *)job // already occupies space) so we don't falsely abort a nearly-finished resume. if (freeB >= 0 && (freeB + received) < need) { spaceNeedBytes = need; spaceAbort = YES; } } - NSDate *now = [NSDate date]; - NSTimeInterval dt = [now timeIntervalSinceDate:lastTick]; + NSTimeInterval now = CFAbsoluteTimeGetCurrent(); + NSTimeInterval dt = now - lastTick; double bps = dt > 0 ? (received - lastReceived) / dt : 0; lastReceived = received; lastTotal = total; @@ -980,6 +997,21 @@ - (void)attemptDownloadForJob:(InstallJob *)job // exit code, or -1 if no installer binary is found (outOutput then holds a friendly message). // Synchronous (spawns + waits) — call OFF the main thread. - (int)runIpainstallerArgs:(NSArray *)args capturedOutput:(NSString **)outOutput { + // v3 / iOS 3 support: prefer installing in-process via MobileInstallationInstall + // (no external helper binary needed). This is the "install an .ipa" case: + // a single path argument (NOT the "-i " version query, which has no + // MobileInstallation equivalent and must fall through to the CLI tool below). + // On iOS 3.1.3 the «IPA Installer Console» package can't be installed at all + // (it requires firmware >= 4.0), so this in-process path is the ONLY way to + // install there — and on iOS 5-7 it also lets AppDrop work with no ipainstaller + // present, matching what autopear's ipainstaller / AppSync's appinst do internally. + if (args.count == 1 && ![args[0] isEqualToString:@"-i"]) { + NSString *path = [args[0] description]; + if (path.length && [path hasPrefix:@"/"] && [InProcessInstaller isAvailable]) { + return [InProcessInstaller installIPAAtPath:path capturedOutput:outOutput]; + } + } + // Candidate installer paths, in priority order: // - /usr/bin/ipainstaller (legacy / iOS 5-9 jailbreaks: autopear's package) // - /usr/bin/appinst (newer alias used by some repos) @@ -1080,6 +1112,17 @@ - (int)spawnInstaller:(const char *)exec args:(NSArray *)args capturedOutput:(NS // (`-l` / `-i` queries still go through runIpainstallerArgs:, which just uses the first installer.) - (int)runIpainstallerOnFile:(NSString *)path capturedOutput:(NSString **)outOutput { NSArray *args = (path.length ? @[path] : @[]); + // v3 / iOS 3 support: prefer installing in-process via MobileInstallationInstall + // (no external helper binary needed), exactly like runIpainstallerArgs: does for + // the non-autonomous path. This is the AUTONOMOUS-download install path; without + // this it skips InProcessInstaller and goes straight to the CLI tools below, which + // don't exist on iOS 3.1.3 (the «IPA Installer Console» package needs firmware + // >= 4.0), so the install failed with "ipainstaller not found" even though the + // in-process installer was available. On iOS 5-9 the CLI tools (if present) still + // run via the fall-through below when MobileInstallationInstall is unavailable. + if (path.length && [path hasPrefix:@"/"] && [InProcessInstaller isAvailable]) { + return [InProcessInstaller installIPAAtPath:path capturedOutput:outOutput]; + } static const char *kIpaFamily[] = { "/usr/bin/ipainstaller", "/var/jb/usr/bin/ipainstaller", "/opt/procursus/bin/ipainstaller", NULL }; static const char *kAppFamily[] = { "/usr/bin/appinst", "/var/jb/usr/bin/appinst", NULL }; const char *primary = NULL, *alternate = NULL; diff --git a/IPAInstaller/JobCell.h b/IPAInstaller/JobCell.h index 0470f9f..18b342b 100644 --- a/IPAInstaller/JobCell.h +++ b/IPAInstaller/JobCell.h @@ -4,6 +4,8 @@ @interface JobCell : UITableViewCell @property (nonatomic, strong, readonly) UILabel *nameLabel; @property (nonatomic, strong, readonly) UILabel *messageLabel; -@property (nonatomic, strong, readonly) UIProgressView *progressBar; +// iOS 3.1.3 has no UIProgressView track/progress tint (those are iOS 5+ APIs), +// so the progress bar is a custom track+fill made of plain UIViews. See JobCell.m. +@property (nonatomic, strong, readonly) UIView *progressBar; - (void)configureWithJob:(InstallJob *)job; @end diff --git a/IPAInstaller/JobCell.m b/IPAInstaller/JobCell.m index 6c0e5e5..dd840d7 100644 --- a/IPAInstaller/JobCell.m +++ b/IPAInstaller/JobCell.m @@ -5,7 +5,14 @@ @interface JobCell () @property (nonatomic, strong) UILabel *nameLabel; @property (nonatomic, strong) UILabel *messageLabel; -@property (nonatomic, strong) UIProgressView *progressBar; +// Custom 2-view progress bar (track + fill). The system UIProgressView's +// -setTrackTintColor:/-setProgressTintColor: are iOS 5+ selectors and DON'T +// exist on iOS 3.1.3 → calling them threw NSInvalidArgumentException +// ("unrecognized selector") and crashed the app. This mirrors the custom bar +// already used in AppDetailViewController and is fully theme-driven on every OS. +@property (nonatomic, strong) UIView *progressBar; // track +@property (nonatomic, strong) UIView *progressFill; // themed fill +@property (nonatomic, assign) CGFloat progressFrac; // 0..1, re-applied on layout @end @implementation JobCell @@ -36,8 +43,14 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr _messageLabel.shadowOffset = CGSizeMake(0, 1); [self.contentView addSubview:_messageLabel]; - _progressBar = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar]; + _progressBar = [[UIView alloc] init]; // track + _progressBar.clipsToBounds = YES; + if ([_progressBar.layer respondsToSelector:@selector(setCornerRadius:)]) + _progressBar.layer.cornerRadius = 4.5; [self.contentView addSubview:_progressBar]; + + _progressFill = [[UIView alloc] init]; // themed fill + [_progressBar addSubview:_progressFill]; } return self; } @@ -50,6 +63,15 @@ - (void)layoutSubviews { _nameLabel.frame = CGRectMake(pad, 6, w, 16); _messageLabel.frame = CGRectMake(pad, 24, w, 28); _progressBar.frame = CGRectMake(pad, b.size.height - 14, w, 9); + // Re-apply the fill width after track resize (rotation / reuse). + CGFloat trackW = _progressBar.bounds.size.width; + _progressFill.frame = CGRectMake(0, 0, trackW * MAX(0, MIN(1, self.progressFrac)), 9); +} + +- (void)setProgressFrac:(CGFloat)frac { + _progressFrac = MAX(0, MIN(1, frac)); + CGFloat trackW = self.progressBar.bounds.size.width; + self.progressFill.frame = CGRectMake(0, 0, trackW * _progressFrac, 9); } - (void)configureWithJob:(InstallJob *)job { @@ -77,7 +99,7 @@ - (void)configureWithJob:(InstallJob *)job { } self.progressBar.hidden = NO; // Dark-aware track (the default groove is near-white → glares in dark mode). - self.progressBar.trackTintColor = dk ? [UIColor colorWithWhite:0.30 alpha:1.0] + self.progressBar.backgroundColor = dk ? [UIColor colorWithWhite:0.30 alpha:1.0] : [UIColor colorWithWhite:0.82 alpha:1.0]; NSString *etaStr = @""; @@ -96,23 +118,23 @@ - (void)configureWithJob:(InstallJob *)job { NSString *detail = [NSString stringWithFormat:@"%@ • %ld%%%@", job.message ?: job.state, (long)job.progress, etaStr]; self.messageLabel.text = detail; - self.progressBar.progress = MAX(0, MIN(1, job.progress / 100.0)); + self.progressFrac = job.progress / 100.0; if ([job.state isEqualToString:@"completed"]) { - self.progressBar.progressTintColor = [UIColor colorWithRed:0.20 green:0.65 blue:0.22 alpha:1.0]; + self.progressFill.backgroundColor = [UIColor colorWithRed:0.20 green:0.65 blue:0.22 alpha:1.0]; self.messageLabel.textColor = dk ? [UIColor colorWithRed:0.42 green:0.82 blue:0.48 alpha:1.0] : [UIColor colorWithRed:0.10 green:0.45 blue:0.10 alpha:1.0]; } else if ([job.state isEqualToString:@"failed"]) { - self.progressBar.progressTintColor = [UIColor colorWithRed:0.85 green:0.15 blue:0.15 alpha:1.0]; + self.progressFill.backgroundColor = [UIColor colorWithRed:0.85 green:0.15 blue:0.15 alpha:1.0]; self.messageLabel.textColor = dk ? [UIColor colorWithRed:0.96 green:0.46 blue:0.46 alpha:1.0] : [UIColor colorWithRed:0.65 green:0.10 blue:0.10 alpha:1.0]; } else if ([job.state isEqualToString:@"cancelled"]) { // Orange: not an error, but the download isn't going to finish. - self.progressBar.progressTintColor = [UIColor colorWithRed:0.95 green:0.60 blue:0.10 alpha:1.0]; + self.progressFill.backgroundColor = [UIColor colorWithRed:0.95 green:0.60 blue:0.10 alpha:1.0]; self.messageLabel.textColor = dk ? [UIColor colorWithRed:0.98 green:0.72 blue:0.34 alpha:1.0] : [UIColor colorWithRed:0.65 green:0.40 blue:0.05 alpha:1.0]; } else { - self.progressBar.progressTintColor = [UIColor colorWithRed:0.22 green:0.47 blue:0.85 alpha:1.0]; + self.progressFill.backgroundColor = [UIColor colorWithRed:0.22 green:0.47 blue:0.85 alpha:1.0]; self.messageLabel.textColor = [IOS6Theme labelGray]; } } diff --git a/IPAInstaller/LocalCatalog.m b/IPAInstaller/LocalCatalog.m index 4531dcf..4f93f6f 100644 --- a/IPAInstaller/LocalCatalog.m +++ b/IPAInstaller/LocalCatalog.m @@ -39,7 +39,11 @@ // machines more cache. Purely a perf knob — it changes no query RESULT. static void ADApplyAdaptiveCacheSize(sqlite3 *db) { if (!db) return; - unsigned long long ram = [[NSProcessInfo processInfo] physicalMemory]; + // -[NSProcessInfo physicalMemory] is iOS 4.0+; on iOS 3.1.3 assume the smallest + // tier (every 3.x device has <= 256 MB anyway). + unsigned long long ram = 256ULL * 1024 * 1024; + if ([[NSProcessInfo processInfo] respondsToSelector:@selector(physicalMemory)]) + ram = [[NSProcessInfo processInfo] physicalMemory]; const char *pragma; if (ram <= 320ULL * 1024 * 1024) pragma = "PRAGMA cache_size = -8000"; // ≤256 MB → 8 MB (unchanged) else if (ram <= 640ULL * 1024 * 1024) pragma = "PRAGMA cache_size = -16000"; // ≤512 MB → 16 MB @@ -51,12 +55,21 @@ static void ADApplyAdaptiveCacheSize(sqlite3 *db) { // data-protection class so iOS can't kill the app for holding the file open while the device is // locked and the app is suspended. No-op on devices without a passcode (no data protection anyway), // and safe on jailbroken systems. Covers the main file + any SQLite sidecars. +// iOS3: NSFileProtectionKey/NSFileProtectionNone are iOS 4.0+. They're weak-imported here, so on +// 3.x dyld binds them to NULL — touching them was a guaranteed SIGBUS at 0x0 (crash in +// loadWithProgress's _searchQueue block, thread 4). Guard on the symbol ADDRESS before use; +// data protection doesn't exist on 3.x anyway, so bailing out is the correct behaviour. +extern NSString * const NSFileProtectionKey __attribute__((weak_import)); +extern NSString * const NSFileProtectionNone __attribute__((weak_import)); static void ADSetNoFileProtection(NSString *path) { if (!path.length) return; + if (&NSFileProtectionKey == NULL || &NSFileProtectionNone == NULL) return; // iOS < 4.0 NSFileManager *fm = [NSFileManager defaultManager]; - NSDictionary *attr = @{ NSFileProtectionKey: NSFileProtectionNone }; + NSDictionary *attr = [NSDictionary dictionaryWithObject:NSFileProtectionNone + forKey:NSFileProtectionKey]; [fm setAttributes:attr ofItemAtPath:path error:NULL]; - for (NSString *sfx in @[@"-wal", @"-shm", @"-journal"]) { + NSArray *sidecars = [NSArray arrayWithObjects:@"-wal", @"-shm", @"-journal", nil]; + for (NSString *sfx in sidecars) { NSString *p = [path stringByAppendingString:sfx]; if ([fm fileExistsAtPath:p]) [fm setAttributes:attr ofItemAtPath:p error:NULL]; } @@ -103,6 +116,9 @@ - (void)dealloc { sqlite3_close(_db); _db = NULL; } + [_dbPath release]; + [_countCacheKey release]; + [super dealloc]; } - (BOOL)isReady { return self.loaded; } @@ -356,7 +372,9 @@ - (void)performCatalogUpdateExpectingGzSize:(long long)expectedGzSize { sqlite3 *old = self.db; self.db = newdb; self.urls = urls; + [_dbPath release]; _dbPath = [cached copy]; + [_countCacheKey release]; // MRC: release the previously copied key _countCacheKey = nil; // SQL-01: new DB → discard the memoized COUNT (still on _searchQueue) if (old) sqlite3_close(old); [self applyStoredCategoryOverrides]; // #156: re-apply category corrections to the fresh DB @@ -401,6 +419,7 @@ - (void)loadWithProgress:(void (^)(NSString *))progressBlock }); return; } + [_dbPath release]; _dbPath = [path copy]; ADSetNoFileProtection(_dbPath); // 0xdead10cc: keep the file out of any data-protection class @@ -471,6 +490,7 @@ - (void)loadWithProgress:(void (^)(NSString *))progressBlock [fm removeItemAtPath:_dbPath error:NULL]; [fm removeItemAtPath:[_dbPath stringByAppendingString:@".gz"] error:NULL]; [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCatalogGzSizeKey]; + [_dbPath release]; _dbPath = nil; dispatch_async(dispatch_get_main_queue(), ^{ [self loadWithProgress:progressBlock completion:completion]; // loaded is still NO → re-resolves + re-downloads @@ -502,6 +522,7 @@ - (void)closeForBackgroundCompletion:(void (^)(void))completion { self.loaded = NO; dispatch_async(_searchQueue, ^{ if (self.db) { sqlite3_close(self.db); self.db = NULL; } + [self->_countCacheKey release]; // MRC: release the previously copied key self->_countCacheKey = nil; if (completion) completion(); }); @@ -559,6 +580,12 @@ - (NSDictionary *)searchWithQuery:(NSString *)q if (!self.loaded || !self.db) { return @{@"error": @"catalog not loaded", @"results": @[], @"total": @0}; } + if (!q) q = @""; + if (!minIOSStr) minIOSStr = @""; + if (!maxIOSStr) maxIOSStr = @""; + if (!deviceClass) deviceClass = @"all"; + if (!category) category = @""; + if (!subgenre) subgenre = @""; NSString *table = unique ? @"entries_unique" : @"entries"; NSString *qLower = [[q stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] lowercaseString]; @@ -633,7 +660,8 @@ - (NSDictionary *)searchWithQuery:(NSString *)q if (sqlite3_step(st) == SQLITE_ROW) total = sqlite3_column_int64(st, 0); } sqlite3_finalize(st); - _countCacheKey = countKey; + [_countCacheKey release]; // MRC: countKey is autoreleased — we must own our copy, + _countCacheKey = [countKey copy]; // otherwise _countCacheKey dangles and the next search crashes _countCacheTotal = total; } @@ -1122,6 +1150,7 @@ - (void)refreshCategoryOverrides { [data writeToFile:[self categoryOverridesCachePath] atomically:YES]; dispatch_async(self->_searchQueue, ^{ [self applyStoredCategoryOverrides]; + [self->_countCacheKey release]; // MRC: release the previously copied key self->_countCacheKey = nil; // SQL-01: category/subgenre changed → counts may differ dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:LocalCatalogDidUpdateNotification object:nil]; @@ -1350,6 +1379,7 @@ - (void)refreshCatalogExtras { [data writeToFile:[self catalogExtrasCachePath] atomically:YES]; dispatch_async(self->_searchQueue, ^{ [self applyCatalogExtras]; + [self->_countCacheKey release]; // MRC: release the previously copied key self->_countCacheKey = nil; // SQL-01: extras inserted/removed rows → counts may differ dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:LocalCatalogDidUpdateNotification object:nil]; diff --git a/IPAInstaller/Localization.m b/IPAInstaller/Localization.m index 4bd8061..dc49279 100644 --- a/IPAInstaller/Localization.m +++ b/IPAInstaller/Localization.m @@ -133,21 +133,21 @@ + (NSBundle *)currentBundle { path = [[NSBundle mainBundle] pathForResource:base ofType:@"lproj"]; } if (!path) path = [[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"]; - _cachedBundle = [NSBundle bundleWithPath:path] ?: [NSBundle mainBundle]; + _cachedBundle = [([NSBundle bundleWithPath:path] ?: [NSBundle mainBundle]) retain]; return _cachedBundle; } + (NSBundle *)englishBundle { if (_enBundle) return _enBundle; NSString *path = [[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"]; - _enBundle = (path ? [NSBundle bundleWithPath:path] : nil) ?: [NSBundle mainBundle]; + _enBundle = [((path ? [NSBundle bundleWithPath:path] : nil) ?: [NSBundle mainBundle]) retain]; return _enBundle; } + (NSDictionary *)stringsForCode:(NSString *)code { if (!code.length) code = @"en"; static NSMutableDictionary *cache = nil; - if (!cache) cache = [NSMutableDictionary dictionary]; + if (!cache) cache = [[NSMutableDictionary dictionary] retain]; NSDictionary *cached = cache[code]; if (cached) return cached; // per-code, immutable → safe to keep across switches NSString *lproj = [[NSBundle mainBundle] pathForResource:code ofType:@"lproj"]; @@ -222,7 +222,7 @@ + (NSArray *)availableLanguageCodes { + (NSString *)displayNameForLanguageCode:(NSString *)code { static NSDictionary *names = nil; if (!names) { - names = @{ + names = [@{ @"en": @"English", @"fr": @"Français", @"es": @"Español", @@ -238,7 +238,7 @@ + (NSString *)displayNameForLanguageCode:(NSString *)code { @"it": @"Italiano", @"ko": @"한국어", @"ru": @"Русский", - }; + } retain]; } return names[code] ?: code; } diff --git a/IPAInstaller/Makefile b/IPAInstaller/Makefile index 87afdce..a945e14 100644 --- a/IPAInstaller/Makefile +++ b/IPAInstaller/Makefile @@ -20,7 +20,7 @@ include $(THEOS)/makefiles/common.mk APPLICATION_NAME = AppDrop -AppDrop_FILES = main.m AppDelegate.m RootViewController.m InstallManager.m CatalogViewController.m CatalogFilter.m FilterViewController.m AppDetailViewController.m IconLoader.m JobCell.m AppTileView.m AppRowCell.m VersionsViewController.m IOS6Theme.m SettingsViewController.m HTTPSClient.m LocalCatalog.m Localization.m CatalogAppCell.m SearchViewController.m CategoryViewController.m CategoryTileView.m FeedbackViewController.m MachOInspector.m ParallelDownloader.m UpdateChecker.m UpdateNotesViewController.m DeviceInfo.m IOS5Compat.m CheckpointLog.m RevivalCatalog.m RevivalListViewController.m ThemePickerViewController.m CollectionStore.m CollectionViewController.m HomeLayoutStore.m FolderPicker.m ModdedCatalog.m FilePickerViewController.m UploadViewController.m ADNumberPickerSheet.m CategorySuggestViewController.m IPAPackage.m CrashReporter.m StatsClient.m +AppDrop_FILES = main.m AppDelegate.m RootViewController.m InstallManager.m InProcessInstaller.m CatalogViewController.m CatalogFilter.m FilterViewController.m AppDetailViewController.m IconLoader.m JobCell.m AppTileView.m AppRowCell.m VersionsViewController.m IOS6Theme.m SettingsViewController.m NetworkClient.m HTTPSClient.m LocalCatalog.m Localization.m CatalogAppCell.m SearchViewController.m CategoryViewController.m CategoryTileView.m FeedbackViewController.m MachOInspector.m ParallelDownloader.m UpdateChecker.m UpdateNotesViewController.m DeviceInfo.m IOS5Compat.m CheckpointLog.m RevivalCatalog.m RevivalListViewController.m ThemePickerViewController.m CollectionStore.m CollectionViewController.m HomeLayoutStore.m FolderPicker.m ModdedCatalog.m FilePickerViewController.m UploadViewController.m ADNumberPickerSheet.m CategorySuggestViewController.m IPAPackage.m CrashReporter.m StatsClient.m # CoreFoundation is listed explicitly so it always has a stable LC_LOAD_DYLIB ordinal — the # post-link iOS-5 fix (build.sh) rebinds NSObject's class/metaclass symbols to it. See CFSHIM note above. AppDrop_FRAMEWORKS = UIKit CoreGraphics Foundation CoreFoundation QuartzCore ImageIO CFNetwork @@ -30,7 +30,10 @@ AppDrop_FRAMEWORKS = UIKit CoreGraphics Foundation CoreFoundation QuartzCore Ima # fault in on a cold start). AppDrop_STRIP_FLAGS=-x -S strips locals + residual debug # symbols while KEEPING global symbols so the on-device crash reporter still resolves # backtraces. -Os is already applied by Theos for FINALPACKAGE builds. -AppDrop_CFLAGS = -fobjc-arc -Wall -fvisibility=hidden -fvisibility-inlines-hidden -I$(MBEDTLS_DIR)/include +AppDrop_CFLAGS = -Wall -fvisibility=hidden -fvisibility-inlines-hidden -I../ios3/compat -I$(MBEDTLS_DIR)/include +# armv7 builds against the modern SDK, so we only need the shared block helpers and the +# AD_WEAK alias; the full ios3 compat prefix header redefines UIKit/Foundation symbols. +AppDrop_OBJCFLAGS = -fno-objc-arc -DAD_WEAK=__unsafe_unretained -include ../ios3/compat/AppDropBlocks.h $(AppDrop_CFLAGS) AppDrop_LDFLAGS = -L$(MBEDTLS_LIB) -lmbedtls -lmbedx509 -lmbedcrypto -lsqlite3 -lz -Wl,-dead_strip AppDrop_STRIP_FLAGS = -x -S AppDrop_CODESIGN_FLAGS = -Sentitlements.plist @@ -45,12 +48,21 @@ after-stage:: cp -f Layout/DEBIAN/postinst $(THEOS_STAGING_DIR)/DEBIAN/postinst cp -f Layout/DEBIAN/postrm $(THEOS_STAGING_DIR)/DEBIAN/postrm chmod 0755 $(THEOS_STAGING_DIR)/DEBIAN/postinst $(THEOS_STAGING_DIR)/DEBIAN/postrm + mv $(THEOS_STAGING_DIR)/Applications/AppDrop.app/AppDrop $(THEOS_STAGING_DIR)/Applications/AppDrop.app/AppDrop.armv7 + cp -f AppDropLauncher.sh $(THEOS_STAGING_DIR)/Applications/AppDrop.app/AppDrop + chmod 0755 $(THEOS_STAGING_DIR)/Applications/AppDrop.app/AppDrop @echo "Generating Localizable.json per language (Theos compiles .strings to binary plist, which iOS 6 mis-parses for some Unicode e.g. Turkish; JSON + NSJSONSerialization is bulletproof)…" @for d in $(THEOS_STAGING_DIR)/Applications/AppDrop.app/*.lproj; do \ lang=`basename "$$d" .lproj`; \ src="Resources/$$lang.lproj/Localizable.strings"; \ - if [ -f "$$src" ]; then plutil -convert json -o "$$d/Localizable.json" "$$src" && echo " $$lang ok" || echo " $$lang FAILED"; fi; \ + if [ -f "$$src" ]; then \ + if command -v plutil >/dev/null 2>&1; then \ + plutil -convert json -o "$$d/Localizable.json" "$$src" && echo " $$lang ok" || echo " $$lang FAILED"; \ + else \ + python3 "$(PWD)/../tools/strings2json.py" "$$src" "$$d/Localizable.json" && echo " $$lang ok" || echo " $$lang FAILED"; \ + fi; \ + fi; \ done @echo "iOS 5 fix: rebinding NSObject (libobjc -> CoreFoundation) so the app launches on iOS 5.x, then re-signing…" - @python3 "$(PWD)/../tools/ios5_rebind.py" "$(THEOS_STAGING_DIR)/Applications/AppDrop.app/AppDrop" - @"$(THEOS)/toolchain/linux/iphone/bin/ldid" -Sentitlements.plist "$(THEOS_STAGING_DIR)/Applications/AppDrop.app/AppDrop" && echo " re-signed ok" + @python3 "$(PWD)/../tools/ios5_rebind.py" "$(THEOS_STAGING_DIR)/Applications/AppDrop.app/AppDrop.armv7" + @"$(THEOS)/toolchain/linux/iphone/bin/ldid" -Sentitlements.plist "$(THEOS_STAGING_DIR)/Applications/AppDrop.app/AppDrop.armv7" && echo " re-signed ok" diff --git a/IPAInstaller/NetworkClient.m b/IPAInstaller/NetworkClient.m index 87104ed..532e334 100644 --- a/IPAInstaller/NetworkClient.m +++ b/IPAInstaller/NetworkClient.m @@ -4,11 +4,24 @@ @interface NCRequest : NSObject @property (nonatomic, strong) NSMutableData *buffer; @property (nonatomic, strong) NSHTTPURLResponse *response; -@property (nonatomic, copy) void (^completion)(NSData *, NSHTTPURLResponse *, NSError *); +@property (nonatomic, copy) void (^completion)(NSData *, NSHTTPURLResponse *, NSError *); @property (nonatomic, strong) NSURLConnection *connection; @end -@implementation NCRequest +@implementation NCRequest { + // iOS 3: blocks aren't ObjC objects, so a synthesized @property(copy) block + // setter calls objc_setProperty(copy=YES) → -copyWithZone: on the block → + // Bus error (signal 10). Back the block manually through the C blocks runtime + // (_Block_copy/_Block_release) — see AppDropBlocks.h (AD_BLOCK_ACCESSORS). + void (^_completionBlock)(NSData *, NSHTTPURLResponse *, NSError *); +} +@dynamic completion; +AD_BLOCK_ACCESSORS(completion, setCompletion, _completionBlock, void(^)(NSData *, NSHTTPURLResponse *, NSError *)) + +- (void)dealloc { + if (_completionBlock) _Block_release((const void *)_completionBlock); + [super dealloc]; +} - (void)start:(NSURLRequest *)req { self.buffer = [NSMutableData data]; @@ -73,7 +86,7 @@ @implementation NetworkClient + (void)initialize { if (self == [NetworkClient class]) { - _inflight = [NSMutableSet set]; + _inflight = [[NSMutableSet set] retain]; } } @@ -86,7 +99,7 @@ + (void)getURL:(NSString *)url [req setHTTPMethod:@"GET"]; NCRequest *r = [[NCRequest alloc] init]; @synchronized (_inflight) { [_inflight addObject:r]; } - __weak NCRequest *weakR = r; + AD_WEAK NCRequest *weakR = r; r.completion = ^(NSData *d, NSHTTPURLResponse *resp, NSError *err) { if (cb) { dispatch_async(dispatch_get_main_queue(), ^{ cb(d, resp, err); }); @@ -112,7 +125,7 @@ + (void)postURL:(NSString *)url if (body) [req setHTTPBody:body]; NCRequest *r = [[NCRequest alloc] init]; @synchronized (_inflight) { [_inflight addObject:r]; } - __weak NCRequest *weakR = r; + AD_WEAK NCRequest *weakR = r; r.completion = ^(NSData *d, NSHTTPURLResponse *resp, NSError *err) { if (cb) { dispatch_async(dispatch_get_main_queue(), ^{ cb(d, resp, err); }); diff --git a/IPAInstaller/ParallelDownloader.m b/IPAInstaller/ParallelDownloader.m index f18ee66..b68a929 100644 --- a/IPAInstaller/ParallelDownloader.m +++ b/IPAInstaller/ParallelDownloader.m @@ -1,7 +1,6 @@ #import "ParallelDownloader.h" #import "HTTPSClient.h" #import "Localization.h" -#import @implementation ParallelDownloader @@ -101,7 +100,12 @@ + (void)runChunkedDownload:(NSString *)url [chunkErrArr addObject:[NSNull null]]; [chunkCodeArr addObject:@0]; } - __block CFAbsoluteTime lastAggregateFire = 0; // 0 = "never fired"; first tick always passes the 0.3s gate + // NOTE (iOS 3 / MRC): under -fno-objc-arc, object-typed __block variables are + // NOT retained on block copy, so an autoreleased NSDate stored here would be + // freed before the chunk progress blocks fire on their background threads + // (crash in objc_msgSend). Use a primitive NSTimeInterval; 0 acts as + // "distant past" so the first tick always fires. + __block NSTimeInterval lastAggregateFire = 0; NSLock *lock = [[NSLock alloc] init]; __block BOOL anyChunkFailed = NO; @@ -134,7 +138,7 @@ + (void)runChunkedDownload:(NSString *)url long long aggregateReceived = 0; [lock lock]; chunkBytesArr[idx] = @(chunkReceived); - CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); + NSTimeInterval now = CFAbsoluteTimeGetCurrent(); if (now - lastAggregateFire > 0.3) { shouldFire = YES; lastAggregateFire = now; diff --git a/IPAInstaller/Resources/Default.png b/IPAInstaller/Resources/Default.png new file mode 100644 index 0000000..a6cf647 Binary files /dev/null and b/IPAInstaller/Resources/Default.png differ diff --git a/IPAInstaller/Resources/Info.plist b/IPAInstaller/Resources/Info.plist index 35fb008..de45a3a 100644 --- a/IPAInstaller/Resources/Info.plist +++ b/IPAInstaller/Resources/Info.plist @@ -9,7 +9,7 @@ CFBundleExecutable AppDrop CFBundleIconFile - Icon-72.png + Icon-57.png CFBundleIconFiles Icon-57.png @@ -103,7 +103,7 @@ LSRequiresIPhoneOS MinimumOSVersion - 5.0 + 3.0 UIDeviceFamily 1 @@ -111,10 +111,6 @@ UIPrerenderedIcon - UIRequiredDeviceCapabilities - - armv7 - UIStatusBarStyle UIStatusBarStyleDefault UISupportedInterfaceOrientations diff --git a/IPAInstaller/Resources/ru.lproj/Localizable.strings b/IPAInstaller/Resources/ru.lproj/Localizable.strings new file mode 100644 index 0000000..74e947f --- /dev/null +++ b/IPAInstaller/Resources/ru.lproj/Localizable.strings @@ -0,0 +1,576 @@ +/* AppDrop — Русский */ + +/* ===== Tab bar ===== */ +"tab.catalog" = "Каталог"; +"tab.search" = "Поиск"; +"search.placeholder" = "Поиск 43,000+ приложений..."; +"search.hint_empty" = "Введите текст для поиска в каталоге"; +"search.searching" = "Идёт поиск..."; +"search.no_results" = "Приложения не найдены"; +"search.results_count" = "%lu из %ld найдены"; +"tab.ai" = "ИИ"; +"tab.install" = "Установить"; +"tab.settings" = "Настройки"; + +/* ===== Catalog screen ===== */ +"catalog.title" = "Каталог"; +"catalog.filters" = "Фильтры"; +"catalog.search_placeholder" = "Поиск (название или bundle ID)"; +"catalog.loading" = "Загрузка каталога..."; +"catalog.retry" = "Каталог недоступен — нажмите для повтора."; +"suggestcat.title" = "Предложить категорию"; +"suggestcat.action" = "Неверная категория? Предложите свою"; +"suggestcat.current" = "Текущий: %@"; +"suggestcat.pick_category" = "Выберите категорию"; +"suggestcat.pick_subgenre" = "Выберите секцию"; +"suggestcat.subgenre_required" = "Пожалуйста выберите секцию."; +"suggestcat.none" = "Основные"; +"suggestcat.submit" = "Отправить"; +"suggestcat.thanks" = "Спасибо! Ваше предложение отправлено на рассмотрение."; +"versions.load_failed" = "Не удалось: %@"; +"versions.available_header" = "%lu версий доступно"; +"versions.count" = "%lu версии"; +"versions.unknown_size" = "неизвестный размер"; +"catalog.decompress_failed" = "ошибка распаковки"; +"catalog.unavailable" = "каталог недоступен"; +"catalog.loading_more" = "Загрузка..."; +"catalog.network_error" = "Ошибка сети: %@"; +"catalog.tap_retry" = "Нажмите для повтора"; +"catalog.server_invalid" = "Неверный ответ сервера"; +"catalog.apps_count" = "%lu из %ld приложений%@"; +"catalog.end" = " — конец списка"; +"catalog.select" = "Выбрать"; +"catalog.install_n" = "Установить (%lu)"; +"select.count" = "%lu выбрано"; +"catalog.install_started_n" = "%lu загрузок запущено"; + +/* ===== AI Chat screen ===== */ +"chat.title" = "ИИ Чат"; +"chat.placeholder" = "Задайте свой вопрос..."; +"chat.tap_to_install" = "Нажми для установки"; +"chat.send" = "Отправить"; + +/* ===== App detail screen ===== */ +"app.install" = "Установить"; +"app.starting" = "Начинается..."; +"app.btn.downloading" = "Скачивание %ld %%"; +"app.btn.installing" = "Устанавливается…"; +"app.btn.queued" = "В очереди"; +"app.btn.installed" = "Установлено"; +"app.btn.retry" = "Повторить попытку"; +"app.versions" = "Версии"; +"app.no_url" = "Нет URL"; +"app.no_url_msg" = "У этого приложения нет действительного URL-адреса."; + +/* ===== Install (root) screen ===== */ +"install.title" = "Установ. IPA"; +"install.clear" = "Очистить"; +"install.url_label" = "IPA URL (https:// или itms-services://)"; +"install.url_placeholder" = "https://archive.org/.../app.ipa"; +"install.button" = "Установить"; +"install.installations" = "Установки (%lu)"; +"install.count_active" = "%ld скачивается"; +"install.count_waiting" = "%ld в ожидании"; + +/* ===== Settings screen ===== */ +"settings.title" = "Настройки"; +"settings.section_diagnostics" = "Диагностики"; +"settings.section_diagnostics_footer" = "Проверяет доступ по HTTPS к archive.org и наличие ipainstaller."; +"settings.section_cache" = "Кеш"; +"settings.section_about" = "О программе"; +"settings.test_https" = "Проверить HTTPS до archive.org"; +"settings.test_ipainstaller" = "Проверить вызов ipainstaller"; +"settings.clear_icons" = "Очистить кеш иконок"; +"settings.cache_cleared" = "Кеш очищен"; +"settings.cache_cleared_msg" = "Все иконки каталога удалены из памяти."; +"settings.about_app" = "Приложение"; +"settings.about_version" = "Версия"; +"settings.about_bundle" = "Bundle ID"; +"settings.about_min_ios" = "Мин. iOS для прил."; +"settings.about_device_ios" = "iOS устройства"; +"settings.about_device_model" = "Модель устройства"; +"settings.section_display" = "Модель устройства"; +"settings.hide_ai_tab" = "Скрыть вкладку ИИ"; +"settings.theme" = "Тема"; +"theme.default" = "По умолчанию (синий)"; +"theme.dark_gray" = "Чёрный"; +"theme.dark_blue" = "Тёмно-синий"; +"theme.section_light" = "Светлый режим"; +"theme.section_dark" = "Тёмный режим"; +"theme.graphite" = "Графит"; +"theme.noir" = "Чёрный"; +"theme.slate" = "Сланец"; +"theme.indigo" = "Индиго"; +"theme.purple" = "Фиолетовый"; +"theme.pink" = "Розовый"; +"theme.red" = "Красный"; +"theme.orange" = "Оранжевый"; +"theme.green" = "Зелёный"; +"theme.teal" = "Бирюзовый"; +"theme.header" = "Цвет темы"; +"theme.footer" = "Применяется мгновенно ко всему приложению. Вариант «По умолчанию» сохраняет классический синий дизайн iOS 6."; +"settings.theme_default" = "По умолчанию (синий)"; +"settings.theme_graphite" = "Графит"; +"settings.theme_apply_msg" = "Тема оформления будет применена при следующем открытии AppDrop."; +"settings.grid_density" = "Приложений в ряду"; +"settings.home_grid_density" = "Иконок на Главной в ряду"; +"settings.cols_list" = "Список"; +"settings.show_favorites_home" = "Показать избранное на Главной Странице"; +"settings.section_display_footer" = "Настройка количества плиток в одном ряду для каждой сетки."; +"settings.about_chip" = "Процессор"; +"settings.about_ram" = "Память (ОЗУ)"; +"settings.language" = "Язык"; +"settings.language_auto" = "Автоматич. (системная)"; + +/* ===== Common buttons ===== */ +"common.ok" = "ОК"; +"common.cancel" = "Отменить"; +"common.ios_unknown" = "неизвестно"; + +"common.done" = "Готово"; +/* ===== Catalog filter ===== */ +"filter.title" = "Фильтры"; +"filter.all_versions" = "Все версии iOS"; +"filter.unique" = "Уникальные"; +"filter.sort_label" = "сортировка: %@"; + +"chat.found_apps" = "Найдно %lu приложений по твоему запросу"; +"chat.not_found" = "Не удалось найти это приложение в каталоге."; +"chat.found_named" = "Вот что удалось найти: %@."; +"chat.device_answer" = "Ваше устройство: %@."; +"chat.search_results_neutral" = "Вот некоторые результаты из каталога, которые могут подойти:"; + +"chat.llm_error" = "ИИ сервис недоступен. Проверьте свою сеть и попробуйте снова."; + +/* ===== Install screen actions (v1.8.7) ===== */ +"install.cancel_all" = "Отменить всё"; +"install.pause_all" = "Приостановить всё"; +"install.resume_all" = "Продолжить всё"; +"install.empty" = "Установок пока нет"; +"install.cancelled_title" = "Отменено"; +"install.cancelled_msg" = "%ld загрузка(-ок) отменено."; +"install.swipe_cancel" = "Отменить"; +"install.swipe_delete" = "Удалить"; + +/* ===== Install errors / common URL validation (v1.8.7) ===== */ +"common.empty_url" = "Пустой URL"; +"common.empty_url_msg" = "Вставьте или введите IPA URL."; +"common.invalid_url" = "Неверный URL"; +"common.invalid_url_msg" = "URL должен быть https://...ipa или itms-services://..."; +"common.error" = "Ошибка"; +"common.cancelled" = "Отменено"; +"common.too_many_redirects" = "Слишком много перенаправлений"; + +/* ===== Install job states (v1.8.7) ===== */ +"install.state.queued" = "Ожидание..."; +"install.state.preparing" = "Автономный режим — подготовка"; +"install.state.connecting" = "Подключение к HTTPS..."; +"install.state.downloading_full" = "Скачивание: %.1f МБ / %.1f МБ (%ld%%)"; +"install.state.downloading_partial" = "Скачивание: %.1f МБ"; +"install.state.installing" = "Локальная установка..."; +"install.state.installed_prefix" = "Установлено: %@"; +"install.state.cancelling" = "Отмена..."; +"install.state.paused" = "Пауза"; +"install.state.cancelled_user" = "Отменено пользователем"; +"install.state.interrupted" = "Прервано (приложение закрыли) — скачайте заново для повтора."; + +/* ===== Install errors (v1.8.7) ===== */ +"install.error.failed_prefix" = "Ошибка: %@"; +"install.error.404" = "Файл не найден на сервере (404). IPA был удален или перемещен."; +"install.error.403" = "Доступ заблокирован сервером (403)."; +"install.error.5xx_archive" = "Сервер временно недоступен (HTTP %ld). CDN archive.org может ограничивать ваш IP — подождите 1-2 минуты и повторите попытку."; +"install.error.5xx_generic" = "Ошибка сервера HTTP %ld"; +"install.error.http_generic" = "HTTP %ld"; +"install.error.network" = "Сбой сети"; +"unit.b" = "Б"; +"unit.kb" = "КБ"; +"unit.mb" = "МБ"; +"unit.gb" = "ГБ"; +"install.modern_ios_msg" = "Эта версия iOS не поддерживает ipainstaller. Файл .ipa сохранен по пути:\n%@\nОткройте его в Filza или iFile для ручной установки."; +"install.kept_after_fail" = "Ошибка установки. Файл .ipa сохранен по пути:\n%@\nВы можете установить его вручную через Filza или iFile."; +"install.state.saved_for_manual" = "Сохранено (открыть в Filza)"; +"chat.llm_unavailable" = "ИИ сервис недоступен. Pollinations.ai может быть временно отключен — попробуйте еще раз через несколько минут."; +"install.error.no_ipainstaller" = "ipainstaller не найден. Установите пакет « IPA Installer Console » из Cydia (поиск по: appinst или ipainstaller)."; +"install.error.spawn_failed" = "Сбой posix_spawn(%s): %s"; +"install.error.install_failed" = "Ошибка установки. Такое бывает с тяжелыми приложениями, либо при нехватке памяти — повторите попытку, освободите место или выберите другую версию."; +"install.error.no_space" = "Недостаточно свободного места для установки (требуется около %@). Освободите место и повторите попытку."; + +/* ===== App detail extras (v1.8.7) ===== */ +"app.fallback_title" = "Приложение"; +"app.subtitle_with_size" = "v%@ — %@"; +"app.size_unknown" = "неизвестный размер"; +"app.info_bundle_id" = "Bundle ID"; +"app.info_version" = "Версия"; +"app.info_min_ios" = "Мин iOS"; +"app.info_project" = "Страница проекта"; +"app.info_platform" = "Платформа"; +"app.info_size" = "Размер"; +"app.info_file" = "Файл"; +"app.info_mirror" = "Зеркало"; +"app.info_url" = "Полный URL"; + +/* ===== Filter screen extras (v1.8.7) ===== */ +"filter.section.version" = "Версия iOS приложения"; +"filter.section.device" = "Совместимость с устройством"; +"filter.section.options" = "Параметры"; +"filter.section.sort" = "Сортировка"; +"filter.footer.version" = "Фильтрация приложений по требуемой версии iOS. Установите «Макс iOS» на версию вашего устройства, чтобы видеть только совместимый софт."; +"filter.footer.device" = "iPhone: только приложения для iPhone (включая универсальные). iPad: только приложения для iPad. Все: без фильтрации."; +"filter.footer.options" = "Уникальные: скрывает дубликаты Bundle ID, оставляя в списке только самую свежую версию приложения."; +"filter.device.all" = "Все приложения"; +"filter.device.iphone" = "iPhone / iPod touch"; +"filter.device.ipad" = "iPad"; +"filter.sort.recent_long" = "По новизне"; +"filter.sort.name_long" = "Название A-Z"; +"filter.sort.size_long" = "Размер"; +"filter.sort.minos_long" = "Мин iOS"; +"filter.sort.downloads_long" = "По загрузкам"; +"catalog.short.downloads" = "загрузки"; +"categories.top_downloads" = "Популярные"; +"app.downloads_count" = "%@ загрузок"; +"filter.none" = "нет"; +"filter.choose_none" = "Нет"; +"filter.reset" = "Сбросить настройки"; +"filter.unique_switch" = "Уникальные (без дубликатов)"; +"filter.min_ios_row" = "Мин iOS"; +"filter.max_ios_row" = "Макс iOS"; + +/* ===== Versions screen (v1.8.7) ===== */ +"versions.invalid_response" = "Неверный ответ сервера"; +"versions.footer" = "Каждая строка — это отдельное зеркало. Выбирайте источник, опираясь на нужную версию iOS приложения."; + +/* ===== CatalogFilter short labels (v1.8.7) ===== */ +"catalog.short.recent" = "новые"; +"catalog.short.name" = "имя"; +"catalog.short.size" = "размер"; +"catalog.short.minos" = "iOS"; + +/* ===== Onboarding + sort direction (v2.0.8) ===== */ +"onboarding.ipainstaller_title" = "Важное требование"; +"onboarding.ipainstaller_msg" = "Для установки IPA необходим пакет « IPA Installer Console » из Cydia (поиск по: appinst или ipainstaller). Без него локальная установка приложений невозможна. Вы уже установили его?"; +"onboarding.ipainstaller_continue" = "Да, продолжить"; +"onboarding.ipainstaller_cancel" = "Ещё нет"; +"onboarding.catalog_quality_title" = "Перед установкой приложения"; +"onboarding.catalog_quality_msg" = "Список приложений взят из открытого источника, поэтому у некоторых программ имя или иконка могут слегка не совпадать. Перед установкой проверяйте имя файла под названием приложения, чтобы убедиться, что это то, что нужно."; +"onboarding.dont_show_again" = "Больше не показывать"; +"common.understood" = "Понятно"; +"filter.footer.sort" = "Нажмите на выбранный параметр еще раз, чтобы изменить направление сортировки (по возрастанию/убыванию)."; + +/* ===== v1.2 — Mirror retry on slow download ===== */ +"install.state.retrying_mirror" = "Медленное зеркало — повтор (%d/%d)…"; + +/* ===== v1.2 build 8 — Pre-install FairPlay encryption check ===== */ +"install.error.fairplay" = "Этот .ipa зашифрован FairPlay — привязан к Apple ID, который купил его в App Store, поэтому не запустится на вашем устройстве. Попробуйте другое зеркало в списке Версий (в каталоге часто есть несколько источников для одного приложения)."; +"install.error.arch64" = "Это 64-битное приложение (arm64) — оно не запустится на вашем 32-битном устройстве. Собрано для новых iPhone/iPad (iOS 11+). Здесь нечего устанавливать."; + +/* ===== v1.2 build 9 — Parallel chunked downloads ===== */ +"settings.section_download" = "Загрузка"; +"settings.parallel_streams" = "Параллельных потоков"; +"settings.max_downloads" = "Одновременных загрузок"; +"settings.parallel_streams_footer" = "archive.org ограничивает скорость на каждое TCP-соединение. Разбивка на несколько параллельных потоков даёт ускорение в 3–4 раза. 4 — оптимально: больше почти не помогает, а могут зарезать все потоки разом. Поставьте 1, чтобы отключить."; +"settings.streams_off" = "1 (один поток)"; +"settings.streams_n" = "%ld потока(-ов)"; + +/* ===== v1.2 build 9 — Download error messages (previously hardcoded) ===== */ +"install.error.open_final" = "Не удалось создать финальный .ipa файл (диск заполнен или ограничения sandbox?)"; +"install.error.open_chunk" = "Не удалось прочитать часть файла %@"; +"install.error.concat_write" = "Ошибка записи при сборке файла (диск заполнен?)"; +"install.error.no_headers" = "Сервер не вернул HTTP-заголовки"; +"install.error.redirect_no_location" = "Редирект HTTP %ld без заголовка Location"; + +/* ===== v1.2 build 13 — In-app updater (Settings → Updates section) ===== */ +"settings.section_updates" = "Обновления"; +"settings.installed_version" = "Установлена"; +"settings.latest_release" = "Последний релиз"; +"settings.checking" = "Проверка…"; +"settings.tap_to_check" = "Нажмите для проверки"; +"settings.check_failed" = "Не удалось проверить — нажмите ещё раз"; +"settings.install_update_to" = "Обновить до v%@ через Cydia"; +"settings.install_update_title" = "Установить обновление?"; +"settings.install_update_msg" = "Скачать и установить AppDrop v%@ (релиз %@)? Загрузка пройдёт во вкладке «Установить». Приложение закроется во время обновления — после этого запустите его заново с рабочего стола."; +"settings.install_action" = "Установить"; +"settings.update_started_title" = "Обновление запущено"; +"settings.update_started_msg" = "Откройте вкладку «Установить», чтобы следить за загрузкой. AppDrop закроется, когда ipainstaller возьмёт управление."; +"settings.updates_footer_initial" = "Нажмите «Последний релиз», чтобы проверить репозиторий GitHub на наличие новой версии."; +"settings.updates_footer_checked" = "Последняя проверка: %@. Нажмите «Последний релиз» для обновления."; +"settings.last_checked_just_now" = "только что"; +"settings.last_checked_minutes" = "%ld мин. назад"; +"settings.last_checked_hours" = "%ld ч. назад"; + +/* ===== v1.2 build 14 — Update release-notes modal ===== */ +"update_notes.title" = "Что нового"; +"update_notes.install" = "Установить"; +"update_notes.open_in_cydia" = "Открыть Cydia"; +"update_notes.cydia_banner_title" = "Установить через Cydia"; +"update_notes.cydia_banner_body" = "Это обновление доступно из источника AdrienRL в Cydia. Нажмите «Открыть Cydia» в правом верхнем углу для установки."; +"update_notes.header_with_date" = "v%@ — вышло %@"; +"update_notes.header_no_date" = "v%@"; +"update_notes.empty" = "Описание изменений для этой версии отсутствует."; + +/* ===== v1.2 — archive.org S3 login (optional, helps with throttling) ===== */ +"settings.section_archive" = "Аккаунт Archive.org"; +"settings.section_archive_footer" = "Необязательно. Снижает ограничение скорости по IP. Ключи: archive.org/account/s3.php"; +"settings.archive_email" = "Email"; +"settings.archive_access_key" = "Access Key"; +"settings.archive_secret_key" = "Secret Key"; +"settings.archive_not_set" = "Не задан"; +"settings.archive_help" = "Ключи доступны на archive.org/account/s3.php. Оставьте пустым, чтобы удалить."; +"settings.archive_save" = "Сохранить"; + +/* ===== v1.3.1 — Configurable download folder + keep-IPA toggle + Filza launch ===== */ +"settings.section_download_footer" = "Параллельные потоки: 4 — оптимум для archive.org. Папка сохранения: куда попадают .ipa после загрузки. Хранить IPA: сохранять копию после установки (iOS 5–9; iOS 10+ сохраняет автоматически)."; +"settings.download_folder" = "Папка сохранения"; +"settings.download_folder_default" = "По умолчанию (sandbox)"; +"settings.download_folder_custom" = "Свой путь…"; +"settings.download_folder_custom_msg" = "Укажите абсолютный путь, например /var/mobile/Documents/AppDrop. Оставьте пустым, чтобы вернуться к стандартному. Папка создаётся при первом сохранении."; +"settings.keep_ipa" = "Хранить IPA после установки"; +"settings.allow_encrypted" = "Разрешить зашифрованные IPA (продвинутое)"; +"settings.auto_switch_mirror" = "Авто-замена медленных зеркал"; +"install.saved_at_suffix" = "saved at %@"; +"install.saved_alert_title" = "IPA сохранён"; +"install.saved_alert_msg" = "Сохранено в:\n%@\n\nОткройте в Filza для установки. Если Filza выдаёт ошибку подписи (0xe800B001), установите пакет «appinst» из Cydia и попробуйте снова."; +"install.open_in_filza" = "Открыть в Filza"; +"install.no_filza_title" = "Filza не найдена"; +"install.no_filza_msg" = "Путь к файлу скопирован в буфер обмена — вставьте его в любой файловый менеджер."; + +/* v1.6 — AI description device verdict */ +"app.verdict.perfect" = "Совместимо с вашим %@"; +"app.verdict.maybe" = "Может работать нестабильно на вашем %@"; +"app.verdict.ram" = "Может требовать больше памяти, чем есть у вашего %@ — возможна нестабильная работа"; +"app.verdict.ios" = "Требует iOS %@ — на вашем устройстве iOS %@"; +"app.verdict.ipad_only" = "Только для iPad — не запустится на вашем %@"; +"app.verdict.issues_label" = "Внимание:"; + +/* v1.7 category menu */ +"categories.title" = "Категории"; +"categories.all" = "Все приложения"; +"categories.all_in_cat" = "Все"; +"catalog.browse_categories" = "Категории"; +"cat.Games" = "Игры"; +"cat.Entertainment" = "Развлечения"; +"cat.Social Networking" = "Социальные сети"; +"cat.Photo & Video" = "Фото и видео"; +"cat.Music" = "Музыка"; +"cat.Productivity" = "Продуктивность"; +"cat.Utilities" = "Утилиты"; +"cat.Travel" = "Путешествия"; +"cat.Navigation" = "Навигация"; +"cat.News" = "Новости"; +"cat.Sports" = "Спорт"; +"cat.Education" = "Образование"; +"cat.Finance" = "Финансы"; +"cat.Health & Fitness" = "Здоровье и фитнес"; +"cat.Lifestyle" = "Образ жизни"; +"cat.Business" = "Бизнес"; +"cat.Reference" = "Справочники"; +"cat.Books" = "Книги"; +"cat.Weather" = "Погода"; +"cat.Food & Drink" = "Еда и напитки"; +"cat.Medical" = "Медицина"; +"cat.Shopping" = "Покупки"; +"sub.Action" = "Экшен"; +"sub.Adventure" = "Приключения"; +"sub.Arcade" = "Аркады"; +"sub.Board" = "Настольные"; +"sub.Card" = "Карточные"; +"sub.Casino" = "Казино"; +"sub.Dice" = "Кости"; +"sub.Educational" = "Обучающие"; +"sub.Family" = "Семейные"; +"sub.Kids" = "Детские"; +"sub.Music" = "Музыкальные"; +"sub.Puzzle" = "Головоломки"; +"sub.Racing" = "Гонки"; +"sub.Role Playing" = "Ролевые"; +"sub.Simulation" = "Симуляторы"; +"sub.Sports" = "Спорт"; +"sub.Strategy" = "Стратегии"; +"sub.Trivia" = "Викторины"; +"sub.Word" = "Слова"; +"sub.Casual" = "Казуальные"; + +/* v1.7 category home */ +"categories.welcome" = "Привет в AppDrop!"; +"categories.welcome_sub" = "%@ iOS приложений и игр"; +"categories.napps" = "%@ приложений"; + +/* v1.7 support/donate */ +"settings.support_row" = "Поддержать AppDrop"; +"support.action" = "Поддержать AppDrop"; +"settings.support_footer" = "AppDrop бесплатен. Если приложение вам полезно, небольшой донат через PayPal будет приятен — но не обязателен."; +"settings.support_copied" = "Ссылка скопирована. Откройте её на более новом устройстве или компьютере, чтобы задонатить через PayPal."; + +/* v1.7 version compatibility */ +"app.picked_compatible" = "Показана последняя версия, совместимая с вашей iOS — более новые версии требуют свежей iOS."; +"versions.incompatible" = "✗ нужна свежее iOS"; + +/* v1.7 catalog download */ +"catalog.downloading" = "Загрузка каталога…"; +"catalog.downloading_pct" = "Загрузка каталога… %d%%"; +"catalog.decompressing" = "Распаковка…"; + +/* v1.7 feedback */ +"feedback.title" = "Обратная связь"; +"feedback.section" = "Обратная связь"; +"feedback.section_footer" = "Сообщите об ошибке или предложите улучшение. Отправляется прямо разработчику."; +"feedback.row" = "💬 Сообщить об ошибке / предложить"; +"feedback.intro" = "Нашли баг или есть идея? Опишите ниже. Можно прикрепить скриншоты."; +"feedback.placeholder" = "Опишите баг или улучшение…"; +"feedback.add_photo" = "📷 Добавить фото"; +"feedback.remove_photo" = "Удалить"; +"feedback.send" = "Отправить"; +"feedback.sent_title" = "Спасибо!"; +"feedback.sent" = "Отзыв отправлен. Спасибо, что помогаете улучшать AppDrop!"; +"feedback.error" = "Не отправлено — проверьте соединение"; +"feedback.empty" = "Сначала напишите что-нибудь."; +"feedback.not_configured" = "Обратная связь ещё не настроена — попробуйте после следующего обновления."; + +/* v3.1 crash reporter */ +"crash.prompt_title" = "AppDrop упал"; +"crash.prompt_msg" = "AppDrop завис в прошлый раз. Отправить анонимный отчёт, чтобы помочь исправить проблему? Личные данные не включены."; +"crash.send" = "Отправить отчёт"; +"crash.decline" = "Не сейчас"; +"crash.thanks_title" = "Спасибо!"; +"crash.thanks_msg" = "Отчёт отправлен. Это реально помогает исправить проблему."; + +"tab.home" = "Главная"; + +/* ===== Revival / Works today (issue #4) ===== */ +"categories.revival" = "Работает сегодня"; +"categories.revival_sub" = "Приложения, которые до сих пор работают на старой iOS"; +"categories.modded" = "Моды"; +"categories.modded_sub" = "Изменённые приложения и игры"; +"modded.intro" = "Изменённые приложения и игры для старой iOS."; +"revival.title" = "Работает сегодня"; +"revival.intro" = "Приложения, которые реально работают на старой iOS сегодня — пропатченные, оживлённые или сделанные сообществом."; +"revival.empty" = "Для вашего устройства пока ничего нет."; +"revival.install" = "Установить"; +"revival.open" = "Открыть"; +"revival.install_started" = "Установка запущена — смотрите вкладку «Установить»."; +"revival.needs_server" = "нужен сервер"; +"revival.active" = "Активно"; +"revival.beta" = "Бета"; +"revival.discontinued" = "Заброшено"; +"revival.update" = "Обновить до v%@"; +"revival.open_page" = "Открыть страницу проекта"; + +/* ── Collections / Favorites (Phase 1) ── */ +"collections.favorites" = "Избранное"; +"collections.section" = "Избранное и папки"; +"categories.section" = "Категории"; +"collections.empty" = "Избранного пока нет. Нажмите ☆ на странице приложения, чтобы добавить."; +"collections.select" = "Выбрать"; +"collections.select_all" = "Выбрать всё"; +"collections.select_none" = "Снять выделение"; +"collections.remove" = "Убрать"; +"collections.download_n" = "Скачать (%ld)"; +"collections.image" = "Изображение"; +"collections.image_title" = "Превью"; +"collections.image_pick" = "Выбрать приложение как изображение…"; +"collections.image_auto" = "Автоматически (сменяется)"; +"collections.image_hint" = "Нажмите на приложение, чтобы закрепить его как изображение"; +"collections.image_pinned_title" = "Изображение закреплено"; +"collections.image_pinned_msg" = "Это приложение всегда будет отображаться как изображение коллекции."; +"collections.dl_started_title" = "Загрузки запущены"; +"collections.dl_started_msg" = "%ld загрузка(-ок) запущена."; + +/* ── Home: reset layout (5c) ── */ +"home.reset" = "Сбросить"; +"home.reset_title" = "Сбросить раскладку?"; +"home.reset_msg" = "Вернуть Главную к автоматической раскладке (порядок, закрепления и размеры по умолчанию)."; + +/* ── Download later (Phase 2) ── */ +"collections.later" = "Скачать позже"; +"collections.download_all" = "Скачать все"; +"later.added" = "Добавлено в «Скачать позже»."; +"later.removed" = "Убрано из «Скачать позже»."; + +/* ── Multi-select actions (Phase 3) ── */ +"later.short" = "Позже"; +"select.added_fav" = "%lu добавлено в Избранное"; +"select.added_later" = "%lu добавлено в «Скачать позже»"; + +/* ── Folders (Phase 4) ── */ +"folder.short" = "Папка"; +"folder.add_title" = "Добавить в папку"; +"folder.new" = "Новая папка"; +"folder.untitled" = "Без названия"; +"folder.create" = "Создать"; +"folder.edit" = "Редактировать"; +"folder.rename" = "Перемеиновать"; +"folder.delete" = "Удалить папку"; +"folder.delete_title" = "Удалить папку?"; +"folder.delete_title_named" = "Удалить \"%@\"?"; +"folder.delete_msg" = "Папка будет удалена. Уже установленные приложения останутся."; +"select.added_folder" = "%lu добавлено в папку"; + + +/* v3.0 — Encrypted-version (FairPlay) warning + decrypted-version recommendation */ +"app.encrypted_warning" = "Эта версия зашифрована (FairPlay) — не установится: привязана к исходному Apple ID."; +"app.encrypted_use_version" = "Расшифрованная версия: %@ — нажмите, чтобы открыть"; + +/* v3.0 — badge version chiffrée (liste des versions) */ +"versions.encrypted" = "Зашифрована — не установится"; + +/* v3.0 — chiffré : aucune alternative + ouverture auto de la version non chiffrée */ +"app.encrypted_none" = "Эта версия зашифрована, и незашифрованной альтернативы нет."; + +/* v3.0 — Share an app (moderated upload) */ +"upload.title" = "Поделиться приложением"; +"upload.send" = "Отправить"; +"upload.share_button" = "+ Share an app"; +"upload.pick_title" = "Выберите .ipa"; +"upload.pick_shortcuts" = "Места"; +"upload.pick_folders" = "Папки"; +"upload.pick_files" = ".ipa files"; +"upload.pick_empty" = "Здесь нет .ipa файлов."; +"upload.pick_button" = ".ipa file"; +"upload.pick_none" = "Файл не выбран"; +"upload.section_file" = "Файл"; +"upload.section_category" = "Категория"; +"upload.section_details" = "Подробности"; +"upload.section_rights" = "Права"; +"upload.intro" = "Поделитесь приложением, которое вы имеете право распространять: своё, homebrew, open-source, бесплатное, общественное достояние."; +"upload.legal" = "Отправляя, вы подтверждаете, что имеете право делиться этим приложением. Каждая заявка проходит модерацию перед публикацией и может быть удалена."; +"upload.attest" = "Я подтверждаю, что имею право делиться этим приложением"; +"upload.name_ph" = "Название приложения"; +"upload.desc_ph" = "Описание (что это, что изменено…)"; +"upload.minios_ph" = "Минимальная iOS (например 5.0)"; +"upload.version_ph" = "Версия (например 1.2)"; +"upload.mod_ph" = "Что изменено (если есть)"; +"upload.bid_ph" = "Bundle ID (необязательно)"; +"upload.credit_ph" = "Ваше имя / ник (в титрах, необязательно)"; +"upload.sent_title" = "Спасибо!"; +"upload.sent" = "Приложение отправлено на модерацию. Спасибо за вклад!"; +"upload.err_nofile" = "Сначала выберите .ipa файл."; +"upload.err_noname" = "Дайте приложению название."; +"upload.err_attest" = "Нужно подтвердить, что вы имеете право делиться этим приложением."; +"upload.err_toobig" = "Файл слишком большой (макс. %d МБ)."; +"upload.err_read" = "Не удалось прочитать файл."; +"upload.err_send" = "Ошибка отправки"; +// --- v3.1 uploads overhaul --- +"upload.section_type" = "Тип"; +"upload.type_catalog" = "Приложение/Игра"; +"upload.type_revival" = "Работает"; +"upload.type_mods" = "Мод"; +"upload.cat_choose" = "Выберите категорию"; +"upload.cat_required" = "Выберите категорию для этого приложения."; +"upload.cat_pick_title" = "Категория"; +"upload.cat_inherited" = "Приложение уже есть в AppDrop: категория берётся из каталога."; +"upload.cat_footer" = "Обязательно для приложений, которых ещё нет в AppDrop."; +"upload.err_encrypted" = "Это приложение зашифровано (FairPlay DRM) и не может быть опубликовано. Выберите расшифрованную версию."; +"upload.err_nodesc" = "Добавить описание."; +"upload.desc_ph_req" = "Описание (обязательно)"; +"upload.desc_ph_opt" = "Описание (необязательно)"; +"upload.analyzing" = "Анализирование…"; + +/* v3.1.3 — respring + same-category guard */ +"settings.respring" = "Респринг"; +"settings.respring_msg" = "Перезапустить рабочий стол (SpringBoard)? Используйте, если иконка установленного приложения не появилась. Экран на несколько секунд погаснет."; +"suggestcat.same_category" = "Это приложение уже в этой категории. Выберите другую категорию или секцию, чтобы предложить изменение."; +"settings.clear_catalog" = "Очистить кеш каталога (перезагрузить)"; +"settings.catalog_cleared_msg" = "Кеш каталога очищен. AppDrop загружает свежую версию прямо сейчас (или при следующем запуске, если вы офлайн)."; +"cat.Uncategorized" = "Разное"; +"home.active" = "%@ в сети"; +"home.active_one" = "%@ пользователь в сети"; +"home.active_other" = "%@ пользователей в сети"; diff --git a/IPAInstaller/RevivalListViewController.m b/IPAInstaller/RevivalListViewController.m index 65ba83f..e516a42 100644 --- a/IPAInstaller/RevivalListViewController.m +++ b/IPAInstaller/RevivalListViewController.m @@ -88,6 +88,7 @@ - (void)viewDidLoad { - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; } // #142: the hosted Works-Today / Modded list was refreshed mid-session → reload our data + view. @@ -218,7 +219,7 @@ - (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection:(NSInteger)s { } - (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)ip { - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; // ===== Grid: multi-tile row (iPad always; iPhone when density > list) — exactly the catalogue ===== if ([AppRowCell tilesPerRowForWidth:tv.bounds.size.width] > 1) { static NSString *rid = @"revrow"; diff --git a/IPAInstaller/RootViewController.m b/IPAInstaller/RootViewController.m index 6973d1e..52b37a4 100644 --- a/IPAInstaller/RootViewController.m +++ b/IPAInstaller/RootViewController.m @@ -99,23 +99,27 @@ - (void)refreshLeftBarButton { BOOL active = [[InstallManager shared] hasActiveJobs]; // includes paused (non-terminal) BOOL paused = [[InstallManager shared] hasPausedJobs]; if (!active && !paused) { self.navigationItem.leftBarButtonItems = nil; return; } - UIBarButtonItem *cancelAll = [[UIBarButtonItem alloc] initWithTitle:T(@"install.cancel_all") - style:UIBarButtonItemStyleBordered target:self action:@selector(cancelAllTapped)]; - cancelAll.tintColor = [UIColor colorWithRed:0.85 green:0.15 blue:0.15 alpha:1.0]; // destructive-ish + UIBarButtonItem *cancelAll = [[[UIBarButtonItem alloc] initWithTitle:T(@"install.cancel_all") + style:UIBarButtonItemStyleBordered target:self action:@selector(cancelAllTapped)] autorelease]; + // -[UIBarButtonItem setTintColor:] is iOS 5.0+. On iOS 3.x it's an unrecognized + // selector → NSInvalidArgumentException → SIGABRT (the Install-tab crash). Guard it. + if ([cancelAll respondsToSelector:@selector(setTintColor:)]) + cancelAll.tintColor = [UIColor colorWithRed:0.85 green:0.15 blue:0.15 alpha:1.0]; // destructive-ish NSMutableArray *items = [NSMutableArray arrayWithObject:cancelAll]; // Resume-all if anything is paused; otherwise Pause-all when downloads are active. if (paused) { - [items addObject:[[UIBarButtonItem alloc] initWithTitle:T(@"install.resume_all") - style:UIBarButtonItemStyleBordered target:self action:@selector(resumeAllTapped)]]; + [items addObject:[[[UIBarButtonItem alloc] initWithTitle:T(@"install.resume_all") + style:UIBarButtonItemStyleBordered target:self action:@selector(resumeAllTapped)] autorelease]]; } else { - [items addObject:[[UIBarButtonItem alloc] initWithTitle:T(@"install.pause_all") - style:UIBarButtonItemStyleBordered target:self action:@selector(pauseAllTapped)]]; + [items addObject:[[[UIBarButtonItem alloc] initWithTitle:T(@"install.pause_all") + style:UIBarButtonItemStyleBordered target:self action:@selector(pauseAllTapped)] autorelease]]; } self.navigationItem.leftBarButtonItems = items; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; } - (void)buildHeader { diff --git a/IPAInstaller/SearchViewController.m b/IPAInstaller/SearchViewController.m index acf833d..774e05c 100644 --- a/IPAInstaller/SearchViewController.m +++ b/IPAInstaller/SearchViewController.m @@ -255,7 +255,7 @@ - (void)addSelectedToLater { - (void)addSelectedToFolder { NSArray *batch = [self.selectedAppsByPk.allValues copy]; if (!batch.count) return; - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; [FolderPicker presentAddToFolderFrom:self completion:^(NSString *cid) { if (!cid) return; for (NSDictionary *app in batch) [[CollectionStore shared] addApp:app toCollection:cid]; @@ -431,7 +431,7 @@ - (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexP row.tilesPerRow = n; [row setContentRasterized:!self.gridScrolling]; // rasterize at rest, plain while scrolling row.selectionMode = self.selectionMode; - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; row.isAppSelectedBlock = ^BOOL(NSDictionary *app) { NSNumber *pk = app[@"id"]; return pk && ws.selectedAppsByPk[pk] != nil; @@ -636,6 +636,7 @@ - (void)viewWillLayoutSubviews { - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)o { return YES; } diff --git a/IPAInstaller/SettingsViewController.m b/IPAInstaller/SettingsViewController.m index b8faa11..8837360 100644 --- a/IPAInstaller/SettingsViewController.m +++ b/IPAInstaller/SettingsViewController.m @@ -161,6 +161,7 @@ - (void)viewDidAppear:(BOOL)animated { - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; } - (void)updateCheckerChanged:(NSNotification *)note { @@ -811,7 +812,7 @@ - (void)handleUpdatesRowTap { vc.version = uc.latestVersion; vc.releaseDate = uc.latestReleaseDate; vc.notesMarkdown = uc.latestReleaseNotes; - __weak typeof(self) weakSelf = self; + AD_WEAK typeof(self) weakSelf = self; vc.installHandler = ^{ __strong typeof(self) s = weakSelf; if (s) [s openCydiaForUpdate]; @@ -908,7 +909,7 @@ - (void)showGridColumnsPicker { [labels addObject:(v.integerValue <= 1) ? T(@"settings.cols_list") : [NSString stringWithFormat:@"%ld", (long)v.integerValue]]; } - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; [ADNumberPickerSheet presentInView:self.view title:T(@"settings.grid_density") values:vals @@ -935,7 +936,7 @@ - (void)showHomeColumnsPicker { NSArray *vals = pad ? @[@2,@3,@4,@5,@6,@7,@8] : @[@2,@3]; NSMutableArray *labels = [NSMutableArray array]; for (NSNumber *v in vals) [labels addObject:[NSString stringWithFormat:@"%ld", (long)v.integerValue]]; - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; [ADNumberPickerSheet presentInView:self.view title:T(@"settings.home_grid_density") values:vals @@ -966,7 +967,7 @@ - (void)showMaxDownloadsPicker { NSArray *vals = @[@1,@2,@3,@4,@5,@6,@7,@8]; NSMutableArray *labels = [NSMutableArray array]; for (NSNumber *v in vals) [labels addObject:[NSString stringWithFormat:@"%ld", (long)v.integerValue]]; - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; [ADNumberPickerSheet presentInView:self.view title:T(@"settings.max_downloads") values:vals @@ -995,7 +996,7 @@ - (void)showParallelStreamsPicker { } NSInteger current = [[NSUserDefaults standardUserDefaults] integerForKey:kPrefParallelStreams]; if (current <= 0) current = 4; - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; [ADNumberPickerSheet presentInView:self.view title:T(@"settings.parallel_streams") values:vals diff --git a/IPAInstaller/StatsClient.m b/IPAInstaller/StatsClient.m index c56a75f..ff782b8 100644 --- a/IPAInstaller/StatsClient.m +++ b/IPAInstaller/StatsClient.m @@ -9,7 +9,7 @@ static NSString *const kStatsBase = @"https://appdrop-stats.adrienruestlorquet.workers.dev"; @interface StatsClient () -@property (nonatomic, strong) NSMutableDictionary *counts; // bid_lower (NSString) -> NSNumber +@property (nonatomic, retain) NSMutableDictionary *counts; // bid_lower (NSString) -> NSNumber @property (nonatomic, assign) NSInteger activeUsers; // -1 = inconnu @property (nonatomic, assign) BOOL downloadsInFlight; @end @@ -26,7 +26,7 @@ - (instancetype)init { if ((self = [super init])) { _activeUsers = -1; NSDictionary *cached = [NSDictionary dictionaryWithContentsOfFile:[self countsCachePath]]; - _counts = cached ? [cached mutableCopy] : [NSMutableDictionary dictionary]; + _counts = cached ? [cached mutableCopy] : [[NSMutableDictionary alloc] init]; // Re-pousser les compteurs en cache dans la base après chaque hot-swap du catalogue // (le fichier catalog.db est remplacé → la table downloads y est recréée vide). [[NSNotificationCenter defaultCenter] addObserver:self @@ -36,6 +36,12 @@ - (instancetype)init { return self; } +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_counts release]; + [super dealloc]; +} + - (NSString *)countsCachePath { NSString *dir = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; return [dir stringByAppendingPathComponent:@"appdrop_downloads.plist"]; @@ -48,7 +54,9 @@ - (NSString *)deviceID { NSString *did = [d stringForKey:@"AppDropDeviceID"]; if (did.length) return did; CFUUIDRef u = CFUUIDCreate(NULL); - did = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, u); + CFStringRef cfDid = CFUUIDCreateString(NULL, u); + did = [[(NSString *)cfDid retain] autorelease]; + CFRelease(cfDid); CFRelease(u); [d setObject:did forKey:@"AppDropDeviceID"]; [d synchronize]; return did; @@ -61,7 +69,7 @@ - (NSInteger)cachedActiveUsers { return self.activeUsers; } - (void)sendHeartbeatWithCompletion:(void (^)(NSInteger active))completion { NSData *body = [[NSString stringWithFormat:@"{\"id\":\"%@\"}", [self deviceID]] dataUsingEncoding:NSUTF8StringEncoding]; - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; [HTTPSClient postURL:[kStatsBase stringByAppendingString:@"/heartbeat"] headers:@{ @"Content-Type": @"application/json" } body:body @@ -95,7 +103,7 @@ - (void)refreshDownloads { // Réinjecte d'abord les compteurs en cache dans la base (le tri/top marche même hors-ligne au démarrage). if (self.counts.count) [[LocalCatalog shared] mergeDownloadCounts:self.counts]; @synchronized (self) { if (self.downloadsInFlight) return; self.downloadsInFlight = YES; } - __weak typeof(self) ws = self; + AD_WEAK typeof(self) ws = self; [HTTPSClient getURL:[kStatsBase stringByAppendingString:@"/downloads"] timeout:15 completion:^(NSData *body, NSInteger status, NSError *err) { @@ -105,15 +113,16 @@ - (void)refreshDownloads { if (status != 200 || !body.length) return; id j = [NSJSONSerialization JSONObjectWithData:body options:0 error:NULL]; if (![j isKindOfClass:[NSDictionary class]]) return; - NSMutableDictionary *m = [NSMutableDictionary dictionaryWithCapacity:[(NSDictionary *)j count]]; + NSMutableDictionary *m = [[NSMutableDictionary alloc] initWithCapacity:[(NSDictionary *)j count]]; for (NSString *bid in (NSDictionary *)j) { if (![bid isKindOfClass:[NSString class]]) continue; NSNumber *c = ((NSDictionary *)j)[bid]; if ([c isKindOfClass:[NSNumber class]]) m[[bid lowercaseString]] = c; } self.counts = m; - [m writeToFile:[self countsCachePath] atomically:YES]; - [[LocalCatalog shared] mergeDownloadCounts:m]; + [m release]; + [self.counts writeToFile:[self countsCachePath] atomically:YES]; + [[LocalCatalog shared] mergeDownloadCounts:self.counts]; [[NSNotificationCenter defaultCenter] postNotificationName:StatsDownloadsChangedNotification object:self]; }]; } diff --git a/IPAInstaller/UpdateNotesViewController.m b/IPAInstaller/UpdateNotesViewController.m index 3cb092c..9005e81 100644 --- a/IPAInstaller/UpdateNotesViewController.m +++ b/IPAInstaller/UpdateNotesViewController.m @@ -7,7 +7,20 @@ @interface UpdateNotesViewController () @property (nonatomic, strong) UILabel *headerLabel; @end -@implementation UpdateNotesViewController +@implementation UpdateNotesViewController { + void (^_installHandlerBlock)(void); +} + +// iOS 3: blocks aren't ObjC objects, so the synthesized copy setter crashes in +// objc_msgSend. Back installHandler manually via the C blocks runtime — see +// AppDropBlocks.h (AD_BLOCK_ACCESSORS). +@dynamic installHandler; +AD_BLOCK_ACCESSORS(installHandler, setInstallHandler, _installHandlerBlock, void(^)(void)) + +- (void)dealloc { + if (_installHandlerBlock) _Block_release((const void *)_installHandlerBlock); + [super dealloc]; +} #pragma mark - Lifecycle @@ -80,7 +93,7 @@ - (void)cancelTapped { } - (void)installTapped { - void (^handler)(void) = [self.installHandler copy]; + void (^handler)(void) = _installHandlerBlock; // already heap block; never send -copy on iOS 3 [self dismissViewControllerAnimated:YES completion:^{ if (handler) handler(); }]; diff --git a/IPAInstaller/UploadViewController.m b/IPAInstaller/UploadViewController.m index ee90386..d8ee889 100644 --- a/IPAInstaller/UploadViewController.m +++ b/IPAInstaller/UploadViewController.m @@ -117,7 +117,7 @@ - (void)viewDidLoad { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kbHide:) name:UIKeyboardWillHideNotification object:nil]; } -- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } - (void)rebuildSections { NSMutableArray *k = [@[ @(K_FILE), @(K_TYPE) ] mutableCopy]; @@ -134,7 +134,14 @@ - (NSInteger)kindForSection:(NSInteger)s { #pragma mark - Keyboard - (void)kbShow:(NSNotification *)n { - CGRect f = [[n.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + // iOS 3 backport: UIKeyboardFrameEndUserInfoKey is weak-imported (iOS 3.2+) and resolves to + // NULL on 3.1.3 — referencing the symbol crashes, and userInfo[NULL] would throw. Look the + // key up by its literal string (its value equals its name) and fall back to the iOS-2 + // UIKeyboardBoundsUserInfoKey, which 3.x actually posts. + NSValue *fv = [n.userInfo objectForKey:@"UIKeyboardFrameEndUserInfoKey"]; + if (!fv) fv = [n.userInfo objectForKey:@"UIKeyboardBoundsUserInfoKey"]; + if (!fv) return; + CGRect f = [fv CGRectValue]; f = [self.view convertRect:f fromView:nil]; CGFloat overlap = MAX(0, self.view.bounds.size.height - f.origin.y); UIEdgeInsets in = self.tableView.contentInset; in.bottom = overlap; @@ -376,7 +383,7 @@ - (void)pickCategory { [self.view endEditing:YES]; CategorySuggestViewController *p = [[CategorySuggestViewController alloc] initForPickingCategory:self.pickedCategory subgenre:self.pickedSubgenre]; - __weak UploadViewController *weakSelf = self; + AD_WEAK UploadViewController *weakSelf = self; p.onPick = ^(NSString *category, NSString *subgenre) { UploadViewController *s = weakSelf; if (!s) return; s.pickedCategory = category; @@ -421,7 +428,7 @@ - (void)refreshBidInCatalog { - (void)pickFile { [self.view endEditing:YES]; FilePickerViewController *fp = [[FilePickerViewController alloc] initWithDirectory:nil]; - __weak UploadViewController *weakSelf = self; + AD_WEAK UploadViewController *weakSelf = self; fp.onPick = ^(NSString *path) { UploadViewController *s = weakSelf; if (!s) return; s.chosenPath = path; @@ -438,7 +445,7 @@ - (void)pickFile { - (void)analyzePickedIPA:(NSString *)path { self.analyzing = YES; self.iconB64 = nil; - __weak UploadViewController *weakSelf = self; + AD_WEAK UploadViewController *weakSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ MachOInspectionResult enc = [MachOInspector inspectIPA:path]; NSDictionary *meta = (enc == MachOInspectionResultEncrypted) ? nil : [IPAPackage metadataForIPA:path]; diff --git a/IPAInstaller/VersionsViewController.m b/IPAInstaller/VersionsViewController.m index d9fcb3f..e785ec7 100644 --- a/IPAInstaller/VersionsViewController.m +++ b/IPAInstaller/VersionsViewController.m @@ -203,7 +203,7 @@ static NSUInteger ADMaxConcurrentEncProbes(void) { - (void)scheduleEncProbeForURL:(NSString *)url { if (url.length == 0 || [self.encInflight containsObject:url]) return; [self.encInflight addObject:url]; - __weak VersionsViewController *weakSelf = self; + AD_WEAK VersionsViewController *weakSelf = self; [MachOInspector inspectURL:url completion:^(MachOInspectionResult r) { VersionsViewController *s = weakSelf; if (!s) return; diff --git a/IPAInstaller/control b/IPAInstaller/control index 0dc6314..68a8e23 100644 --- a/IPAInstaller/control +++ b/IPAInstaller/control @@ -2,12 +2,13 @@ Package: ca.adrien.appdrop Name: AppDrop Version: 3.2.0.3 Architecture: iphoneos-arm -Description: Browse 43,000+ iOS apps by category and install them on iOS 5-10. Fully iPad-optimized. v3.2: new "Most Downloaded" home shortcut + per-app download counts + sort by downloads, live active-users count, and you can now contribute IPAs yourself at https://upload.appdrop.ca. Also fixes version/compatibility selection (always the newest installable, non-encrypted build) and a category-loading glitch. v3.2.0.1: "Uncategorized" category now lists apps that have no category, download counts & Most Downloaded refresh continuously, and the iOS 5.x launch crash is fixed. v3.2.0.3: community "compatibility builds" (an app re-targeted to a lower iOS) merge cleanly and are fully reversible. (17 themes + dark mode, Favorites & folders, 9 languages.) +Description: Browse 43,000+ iOS apps by category and install them on iOS 3-10. Fully iPad-optimized. v3.2: new "Most Downloaded" home shortcut + per-app download counts + sort by downloads, live active-users count, and you can now contribute IPAs yourself at https://upload.appdrop.ca. Also fixes version/compatibility selection (always the newest installable, non-encrypted build) and a category-loading glitch. v3.2.0.1: "Uncategorized" category now lists apps that have no category, download counts & Most Downloaded refresh continuously, and the iOS 5.x launch crash is fixed. v3.2.0.3: community "compatibility builds" (an app re-targeted to a lower iOS) merge cleanly and are fully reversible. (17 themes + dark mode, Favorites & folders, 9 languages.) Homepage: https://github.com/AdrienRL1/AppDrop Depiction: https://adrienrl1.github.io/cydia/depiction.html Icon: https://adrienrl1.github.io/cydia/PackageIcon.png Maintainer: Adrien Ruest-Lorquet Author: Adrien Ruest-Lorquet Section: Utilities -Depends: firmware (>= 5.0), ai.akemi.appsyncunified | net.angelxwind.appsyncunified | com.linusyang.appsync, com.autopear.installipa | ai.akemi.appinst -Tag: compatible_min::ios5.0 +Depends: firmware (>= 3.0), ai.akemi.appsyncunified | net.angelxwind.appsyncunified | com.linusyang.appsync | us.hackulo.appsync31 | us.hackulo.appsync32 | net.angelxwind.appsync40plus +Recommends: com.autopear.installipa | ai.akemi.appinst +Tag: compatible_min::ios3.0 diff --git a/build-toolchain/setup-system-deps.sh b/build-toolchain/setup-system-deps.sh new file mode 100644 index 0000000..1f2910f --- /dev/null +++ b/build-toolchain/setup-system-deps.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# setup-system-deps.sh — installs the apt packages required by the AppDrop +# Linux build environment (Theos toolchain + packaging). +# +# Safe to re-run: apt-get install is idempotent. Works on Debian/Ubuntu, +# including GitHub Actions ubuntu-24.04 runners (where libtinfo5 is gone +# from the archive — we fall back to a compat symlink onto libtinfo6). +set -e + +SUDO="" +if [ "$(id -u)" != "0" ]; then + SUDO="sudo" +fi + +export DEBIAN_FRONTEND=noninteractive + +$SUDO apt-get update + +$SUDO apt-get install --no-install-recommends -y \ + build-essential clang lld llvm llvm-dev \ + make git wget curl zip unzip rsync fakeroot dpkg perl \ + libplist-dev libplist-utils libssl-dev python3 || true + +# Theos prefers gmake; on Debian/Ubuntu plain `make` is GNU make already. +if ! command -v gmake >/dev/null 2>&1; then + if command -v make >/dev/null 2>&1; then + $SUDO ln -sf "$(command -v make)" /usr/local/bin/gmake + fi +fi + +# libtinfo5: the prebuilt Theos clang toolchain links against libtinfo.so.5. +# Ubuntu 24.04 / Debian 13 dropped the package, so try apt first and fall +# back to symlinking the ABI-compatible libtinfo.so.6. +has_tinfo5() { + [ -e /usr/lib/x86_64-linux-gnu/libtinfo.so.5 ] || [ -e /lib/x86_64-linux-gnu/libtinfo.so.5 ] +} +if ! has_tinfo5; then + $SUDO apt-get install --no-install-recommends -y libtinfo5 2>/dev/null || true +fi +if ! has_tinfo5; then + for d in /usr/lib/x86_64-linux-gnu /lib/x86_64-linux-gnu; do + if [ -e "$d/libtinfo.so.6" ]; then + $SUDO ln -sf "$d/libtinfo.so.6" "$d/libtinfo.so.5" + echo "==> libtinfo5 absent from apt — symlinked $d/libtinfo.so.6 -> libtinfo.so.5" + break + fi + done +fi +if ! has_tinfo5; then + echo "WARN: libtinfo.so.5 still missing — the Theos toolchain clang may fail to start." >&2 +fi + +echo "==> System dependencies OK." \ No newline at end of file diff --git a/build.sh b/build.sh index c29267f..2c150f2 100755 --- a/build.sh +++ b/build.sh @@ -38,6 +38,99 @@ if ! command -v fakeroot >/dev/null 2>&1 \ bash "$PROJECT_REAL/build-toolchain/setup-system-deps.sh" fi +# deps/mbedtls (headers) n'est PAS commite (.gitignore) — seules les libs statiques +# deps/build/*.a (mbedTLS 3.6.0, armv7) le sont. Sur un runner CI fraichement clone, +# on recupere donc les headers de la MEME version pour que HTTPSClient.m compile. +if [ ! -f "$PROJECT_REAL/deps/mbedtls/include/mbedtls/ssl.h" ]; then + echo "==> Headers mbedTLS absents — clonage de mbedtls v3.6.0 (headers only)..." + rm -rf "$PROJECT_REAL/deps/mbedtls" + git clone -q --depth 1 --branch v3.6.0 https://github.com/Mbed-TLS/mbedtls.git "$PROJECT_REAL/deps/mbedtls" +fi + +# --------------------------------------------------------------------------- +# Patch du SDK iPhoneOS10.3 (theos/sdks) pour qu'il soit IDENTIQUE au SDK local +# decrit dans BUILD-LINUX.md (points 4 et 5). Le SDK distribue par theos/sdks +# est allege : il lui manque le crt1 armv7 et certains symboles dans les .tbd. +# Sans ce patch, le link armv7 echoue (Undefined symbols : ___udivsi3, ___divdi3, +# __Unwind_SjLj_*, _objc_msgSend_stret, ...). Ces symboles existent reellement sur +# l'appareil (le binaire Mac de reference les lie de la meme facon) : on se contente +# de les declarer dans les .tbd pour que ld64 accepte de les marquer comme imports. +# +# Idempotent : si le SDK est deja patche (cas du build local Unraid ou la toolchain +# persiste), rien n'est refait. C'est donc surtout utile sur un runner CI fraichement +# clone, ou le SDK vient brut de theos/sdks. +patch_legacy_sdk() { + local sdk_dir="$THEOS/sdks/iPhoneOS10.3.sdk" + [ -d "$sdk_dir" ] || return 0 + + # 4. crt1.3.1.o (armv7/armv7s) — absent du SDK10.3 allege, present dans le SDK9.3. + # Sans lui, ld releve la cible a iOS 7.0 (LC_MAIN) et l'app ne demarre pas sur iPad 1. + # Avec lui : LC_UNIXTHREAD, min 5.0, comme le binaire Mac. + if [ ! -f "$sdk_dir/usr/lib/crt1.3.1.o" ]; then + echo "==> iPhoneOS10.3.sdk : crt1.3.1.o absent — recuperation depuis iPhoneOS9.3.sdk..." + local tmpdir + tmpdir="$(mktemp -d)" + git clone -q --no-checkout --depth 1 --filter=blob:none https://github.com/theos/sdks.git "$tmpdir/sdks" + ( + cd "$tmpdir/sdks" + git sparse-checkout set iPhoneOS9.3.sdk/usr/lib/crt1.3.1.o >/dev/null 2>&1 || true + git checkout -q + ) + if [ -f "$tmpdir/sdks/iPhoneOS9.3.sdk/usr/lib/crt1.3.1.o" ]; then + mkdir -p "$sdk_dir/usr/lib" + cp "$tmpdir/sdks/iPhoneOS9.3.sdk/usr/lib/crt1.3.1.o" "$sdk_dir/usr/lib/crt1.3.1.o" + echo " crt1.3.1.o installe." + else + echo " WARN: crt1.3.1.o introuvable dans iPhoneOS9.3.sdk (ld retombera sur iOS 7.0)." >&2 + fi + rm -rf "$tmpdir" + fi + + # 5. .tbd completes — ajoute les symboles manquants a la PREMIERE liste 'symbols: [...]' + # de chaque .tbd (celle de la tranche armv7). Idempotent : un symbole deja present + # n'est pas redouble. + patch_tbd_symbols() { + local tbd="$1" + shift + [ -f "$tbd" ] || { echo " WARN: $tbd absent — patch ignore." >&2; return 0; } + TBD_SYMS="$*" python3 - "$tbd" <<'PYEOF' +import os, re, sys +from pathlib import Path + +path = Path(sys.argv[1]) +required = os.environ["TBD_SYMS"].split() +text = path.read_text() + +# Premiere liste "symbols: [ ... ]" du fichier (tranche armv7). Les noms de symboles +# ne contiennent jamais ']', donc le premier ']' rencontre ferme bien la liste. +m = re.search(r'(symbols:\s*\[)(.*?)(\])', text, re.S) +if not m: + raise SystemExit(f'Could not find a symbols list in {path}') + +block = m.group(2) +existing = [s.strip() for s in re.split(r',\s*', block) if s.strip()] +missing = [s for s in required if s not in existing] +if not missing: + sys.exit(0) + +merged = existing + missing +# Reformatage simple, indentation alignee comme le reste du .tbd (26 espaces). +indent = ' ' * 28 +wrapped = (',\n' + indent).join(merged) +replacement = m.group(1) + ' ' + wrapped + ' ' + m.group(3) +path.write_text(text[:m.start()] + replacement + text[m.end():]) +print(f' {path.name}: +{len(missing)} symbole(s) ({", ".join(missing)})') +PYEOF + } + + patch_tbd_symbols "$sdk_dir/usr/lib/libSystem.tbd" \ + ___udivsi3 ___udivdi3 ___divsi3 ___divdi3 ___umodsi3 ___fixdfdi ___floatdidf \ + __Unwind_SjLj_Register __Unwind_SjLj_Unregister __Unwind_SjLj_Resume + patch_tbd_symbols "$sdk_dir/usr/lib/libobjc.A.tbd" _objc_msgSend_stret +} + +patch_legacy_sdk + # Cible : SDK iPhoneOS10.3 installe (le Makefile demande 7.0, absent ici), deploiement min iOS 5.0. # GO_EASY_ON_ME=1 : ne pas transformer les warnings en erreurs (-Werror), comme le build macOS. TARGET_TRIPLE="iphone:clang:10.3:5.0" diff --git a/ios3/.gitignore b/ios3/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/ios3/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/ios3/README.md b/ios3/README.md new file mode 100644 index 0000000..00c1e0a --- /dev/null +++ b/ios3/README.md @@ -0,0 +1,253 @@ +# AppDrop — iOS 3 / armv6 backport (`iOS3iThink`) + +This directory backports AppDrop so it builds and runs on **armv6 / iOS 3.1** +(original iPhone, iPhone 3G, iPod touch 1st/2nd gen). + +## TL;DR — the approach + +"Downgrading from ARC" has two very different halves: + +1. **Reference counting** — the `@property (strong/weak/copy)` keywords, retains, + releases, autoreleases. This part is **cheap to convert**: under + `-fno-objc-arc`, clang auto-synthesizes correct retaining setters and emits + plain `retain`/`release` **message sends**, which the iOS 3 runtime has had + since day one. The only manual work is `weak` → non-zeroing weak. +2. **Blocks and GCD** — these need C runtime functions (`_Block_copy`, + `dispatch_async`, `dispatch_once`, …) that **do not exist in iOS 3's + `libSystem`**. iOS 3 has no libdispatch and no blocks runtime at all. This is + the genuinely hard part, and it is independent of ARC vs MRC. + +So this backport **converts the app to MRC** (manual reference counting) and +statically links tiny, self-contained implementations of the **blocks** and +**GCD** runtimes into the binary. The result is a single Mach-O that manages its +own memory with native `retain`/`release`, carries its own blocks/GCD support, +and runs on a stock iOS 3 device — **with no `libarclite` and no ARC runtime +shim**. + +### Why MRC instead of an ARC-runtime shim? + +An earlier iteration kept the source in ARC and statically linked an ARC runtime +shim (`objc_retain`, `objc_storeStrong`, `objc_retainAutoreleasedReturnValue`, +…) plus an empty `libarclite` stub to satisfy clang's driver. That works on +paper but is fragile on real iOS 3 hardware: ARC's autoreleased-return-value +optimization (`objc_retainAutoreleasedReturnValue` / `objc_autoreleaseReturnValue`) +depends on a cooperating runtime and a specific calling-convention handshake, +and the empty-`libarclite` driver hack fights the toolchain. MRC sidesteps all of +it: the compiler emits ordinary `objc_msgSend(obj, @selector(release))` calls +that the 3.x runtime already implements. + +### How small the conversion actually was (measured, not guessed) + +The whole codebase is ~16k lines across 42 `.m` files. Switching the compile to +`-fno-objc-arc` produced **zero** ownership errors — every error was the same +single category: `weak`/`__weak` (zeroing weak references need the iOS 5+ +runtime). The complete manual change set: + +| Change | Sites | Fix | +|---|---|---| +| `__weak Foo *x = self;` local idiom | 24 | `AD_WEAK Foo *x = self;` (macro = `__unsafe_unretained`, i.e. non-zeroing weak) | +| `@property (nonatomic, weak)` | 2 | `@property (nonatomic, assign)` (delegate + transient bar button — both non-owning) | + +`AD_WEAK` is defined in `AppDropCompat.h`. Non-zeroing weak matches what the old +ARC `objc_storeWeak` shim did anyway (a plain assignment), so behavior is +unchanged — it's just explicit now and compiles natively. + +Everything else below is SDK/runtime backfill, unrelated to ARC vs MRC: + +| Problem | Files affected | Fix | +|---|---|---| +| `dict[key]` / `arr[i]` subscripting | ~12 | declared in `AppDropCompat.h`, IMPs in `IOS5Compat.m` | +| `NS_ENUM` / `NS_OPTIONS` macros (iOS 6 SDK) | many | macro fallback in `AppDropCompat.h` | +| `NSTextAlignment*` / `NSLineBreakBy*` renames (iOS 6) | ~10 | `#define` to the iOS 2-era `UI*` names | +| `UIInterfaceOrientationMask*` (iOS 6) | 3 | enum in `AppDropCompat.h` | +| `shouldAutorotate` / `supportedInterfaceOrientations` (iOS 6) | 1 | category decl in `AppDropCompat.h` (runtime-guarded in code) | +| `UISwitch`/`UIView -tintColor` (iOS 6/7) | 2 | category decls, runtime-guarded with `respondsToSelector:` | +| `UITableViewHeaderFooterView` (iOS 6 class) | 1 | `NSClassFromString` lookup so it never hard-links on armv6 | +| `@YES` / `@NO` boxed literals | a few | `__objc_yes/no` builtins (5.1 SDK breaks these) | +| `NSArray -firstObject` (iOS 4) | several | `+load` IMP in `AppDropRuntime.m` | +| `NSData` base64 (iOS 7) | 1 | `+load` IMP in `AppDropRuntime.m` | +| `NSString sizeWithAttributes:` / `drawAtPoint:withAttributes:` (iOS 7) | few | bridged to iOS-2 `UIStringDrawing` | +| `UIImage +imageWithData:scale:` (iOS 6) | 1 | `+load` IMP | +| `NSUUID` (iOS 6 class) | 1 | real `@implementation` (CFUUID-backed) | +| `NSJSONSerialization` (iOS 5 class) | 10 | cJSON-backed class in `AppDropJSON.m` | +| `UINavigationItem setRightBarButtonItems:`/`setLeftBarButtonItems:` + getters (iOS 5) | 9 (Search/Catalog/Collection/AppDetail/Root) | `+load` IMP in `AppDropRuntime.m`; >1 item hosted in a transparent `UIToolbar` via the singular setter (right side reversed to match iOS 5 ordering). **iOS 3.1.3:** the wrapper toolbar is given a content-tight explicit width (per-item measurement) instead of `-sizeToFit`, which on 3.1.3 snaps to the full 320 pt bar — that was hiding the title + left/back button and shoving the buttons to the left edge | +| `+[UIView animateWithDuration:…]` block animations (iOS 4.0; all 3 variants) | 10 (NumberPickerSheet/Category*/AppTile/AppDetail) | `+load` IMPs on the UIView metaclass in `AppDropRuntime.m`; bridged to the iOS-2 `beginAnimations:`/`commitAnimations` API, completion delivered via `animationDidStop:finished:context:`, curve unpacked from `UIViewAnimationOptions` bits 16-17 | +| `NSCache` (iOS 4.0 class; weak-imported → `nil` on iOS 3.1.3, so the icon RAM cache was dead) | 1 (`IconLoader`) | `ADCache` in `AppDropCache.m` — a `NSMutableDictionary` + `@synchronized` work-alike with LRU eviction honouring `countLimit` + `totalCostLimit`; `NSCache` token macro-rewritten to `ADCache` (same strategy as `ADBezierPath`) | +| ImageIO thumbnail decode (`CGImageSourceCreateWithData` / `…CreateThumbnailAtIndex`, iOS 4.0; weak-imported) | 1 (`IconLoader`) | every weak symbol guarded with `&sym != NULL`; on iOS 3.1.3 (keys/funcs `NULL`) it falls back to a full `+[UIImage imageWithData:]` decode + downscale, so icons actually appear instead of every decode returning `nil` | +| `+[UIView animateWithDuration:…]` block animations (iOS 4.0; all 3 variants) | 10 (NumberPickerSheet/Category*/AppTile/AppDetail) | `+load` IMPs on the UIView metaclass in `AppDropRuntime.m`; bridged to the iOS-2 `beginAnimations:`/`commitAnimations` API, completion delivered via `animationDidStop:finished:context:`, curve unpacked from `UIViewAnimationOptions` bits 16-17 | +| Blocks runtime (`_Block_copy`, `__NSConcreteStackBlock`, …) | 52 blocks | static `shim/blocks/` (Apple libclosure, public domain) | +| GCD (`dispatch_async`, `dispatch_once`, …) | 56 sites | static pthread-backed `shim/gcd_shim.c` | +| mbedTLS prebuilt was armv7-only | link | rebuilt for armv6 from source (v3.6.2) | +| mbedTLS `clock_gettime`/`CLOCK_MONOTONIC` absent on iOS 3 | link | `shim/mbed_platform_compat.c` (gettimeofday-based) | +| Memory management (`objc_retain`, `objc_storeStrong`, …) | all | **none — MRC emits native `retain`/`release` message sends** | + +This is verified end-to-end on Linux: all 42 sources compile under +`-fno-objc-arc`, the app links to a real `armv6` Mach-O with `NOUNDEFS`, and a +guard asserts that **zero** ARC C-functions and **zero** unresolved blocks/GCD +imports remain. + +## Layout + +``` +ios3/ +├── build-ios3.sh # one-command Linux cross-compile -> .ipa + .deb +├── compat/ +│ ├── AppDropCompat.h # prefix header: macros/decls + AD_WEAK so sources parse +│ ├── AppDropRuntime.m # +load backfills (firstObject, base64, NSUUID, …) +│ ├── AppDropJSON.m # NSJSONSerialization via cJSON +│ ├── cJSON.[ch] # public-domain JSON (v1.7.18) +│ ├── AppDropBezier.m # self-contained ADBezierPath (UIBezierPath is iOS 3.2+) +│ ├── AppDropGestures.m # self-contained AD*GestureRecognizer + dispatch engine (gestures are iOS 3.2+) +│ └── shim/ +│ ├── blocks/ # blocks runtime (libclosure) +│ ├── gcd_shim.c # GCD on pthreads +│ ├── gcd_mainq.c # _dispatch_main_q storage +│ └── mbed_platform_compat.c +└── README.md +``` + +> Note: there is no `arc_shim.m` — the ARC runtime shim was removed when the app +> was converted to MRC. Only the blocks and GCD shims remain, because iOS 3 +> lacks those runtimes regardless of ARC/MRC. + +## Building locally + +```sh +cd ios3 +./build-ios3.sh +``` + +First run downloads the iOS 5.1 SDK + builds the cctools/ld64/ldid toolchain +and mbedTLS (~10 min). Subsequent runs are cached. Output: +`ios3/build/AppDrop.ipa` and `ios3/build/AppDrop.deb`. + +> **Why the iOS 5.1 SDK?** It's the last SDK whose headers still target a +> deployment min low enough for `armv6`, while clang can still emit code for +> `-target armv6-apple-ios3.1`. We compile *against* 5.1 headers but the runtime +> shims supply everything iOS 3 itself lacks, and we set `-miphoneos-version-min` +> so the binary loads on 3.x. + +## Status / open items + +- ✅ Converted to **MRC** (42/42 compile under `-fno-objc-arc`), links + (`NOUNDEFS`), self-contained blocks/GCD, native `retain`/`release`, no + `libarclite`. +- ✅ Packages `.ipa` + Cydia `.deb`, localizations converted to JSON. +- ✅ **Info.plist lowered for iOS 3.** `MinimumOSVersion` is now `3.0` (was + `5.0`) and the `UIRequiredDeviceCapabilities` `armv7` entry was removed, so an + armv6 / iOS 3 device no longer rejects the bundle at install time. +- ✅ **GCD serial queues are real now.** `dispatch_queue_create` returns a true + FIFO serial queue (single worker thread draining an in-order list); only the + global queue and `DISPATCH_QUEUE_CONCURRENT` stay concurrent. This is what + `LocalCatalog`'s `_searchQueue` needs — every SQLite query runs in order on + one `sqlite3*` handle, so there is no race on the shared db handle. The main + queue already hops onto the main `CFRunLoop` (`CFRunLoopPerformBlock` + + `CFRunLoopWakeUp`), so UIKit work from `dispatch_get_main_queue` stays on the + main thread. +- ✅ **Launch crash fixed (`Symbol not found: _imp_implementationWithBlock`).** + `AppDropRuntime.m` used to install its backfilled IMPs via + `imp_implementationWithBlock()`, which only exists in iOS 4.3+ `libobjc`; on + iOS 3.1.3 dyld aborted the process at launch. Every IMP is now a plain static + C function `(id self, SEL _cmd, …)` passed straight to `class_addMethod()` + with the same type encodings — no block trampoline, no missing runtime + symbol. `build-ios3.sh`'s leaked-symbol guard now also fails the build if + `imp_implementationWithBlock` / `imp_removeBlock` ever reappear. +- ✅ **Binary + `.app` are named `AppDrop`.** `APP_NAME="AppDrop"` in + `build-ios3.sh`, matching `CFBundleExecutable=AppDrop` in `Info.plist`, so + SpringBoard finds the executable inside `AppDrop.app` (an earlier mismatch — + exec `IPAInstaller` vs plist `AppDrop` — caused a "does not have an executable + path" launch rejection). The source folder stays `IPAInstaller/`. +- ✅ **Launch crash fixed (`-[UILongPressGestureRecognizer setMinimumPressDuration:]: + unrecognized selector`).** The entire `UIGestureRecognizer` family — + `UITap`/`UILongPress`/`UIPanGestureRecognizer`, their property setters, and + `-[UIView addGestureRecognizer:]` — is **iOS 3.2+**. On 3.1.3 the class symbols + exist enough to `alloc`/`init` (so the object is live), but the methods are dead + stubs, so the first setter call (`setMinimumPressDuration:` in + `CategoryViewController`) threw `NSInvalidArgumentException` → `SIGABRT` right + after `makeKeyAndVisible`. Fixed the same way as `UIBezierPath`: a + self-contained backport in `compat/AppDropGestures.m` — real + `AD{,Tap,LongPress,Pan}GestureRecognizer` classes plus a tiny dispatch engine + (a `UIWindow -sendEvent:` swizzle that walks the hit-view superview chain and + feeds touches to the attached recognizers) — macro-rewritten over the UIKit + class names in `AppDropCompat.h`. This both removes the crash **and** keeps the + app usable: every tile tap, banner tap, edit-mode long-press and resize-pan + rides on these recognizers, so a bare guard would have stopped the crash but + frozen the UI. Uses only iOS-2.0-era primitives (`locationInView:`, + `CACurrentMediaTime`, associated objects — already proven at launch by the + rootVC backfill). +- ✅ **Launch crash fixed (`dyld: Symbol not found: _CFRunLoopPerformBlock`).** + The GCD shim's `run_on_main()` (in `compat/shim/gcd_shim.c`) hopped main-queue + work onto the main run loop via `CFRunLoopPerformBlock`, which **first ships in + iOS 4.0** (CoreFoundation 550). On 3.1.3 dyld's lazy bind couldn't resolve it + and aborted with `SIGTRAP` right after `makeKeyAndVisible` (the first + `dispatch_async(dispatch_get_main_queue(), …)` triggers it). Replaced with a + one-shot `CFRunLoopTimer` (available since iOS 2.0) fired immediately on the + main run loop in `kCFRunLoopCommonModes`: the copied block rides in the timer + context, the callback runs + `Block_release`es it, then invalidates the timer. + `build-ios3.sh` gained a second symbol guard that fails the build if any + iOS-4.0+ CoreFoundation symbol (`CFRunLoopPerformBlock`, `CFRunLoopWakeUpV2`, + …) appears as an undefined import, so this can't silently regress (the older + guard only catches *unresolved* imports, but CF 4.0 symbols resolve against + the 5.1 SDK at build time and only fail at runtime on 3.x). +- ✅ **Launch crash fixed (`EXC_BAD_ACCESS` on a GCD worker thread ~2s after + launch).** The app starts, then `LocalCatalog` kicks catalog-DB work onto its + serial `_searchQueue` and the global background queue (`dispatch_async`). + Those queues are backed by the `gcd_shim.c` pthread workers, which ran the + Objective-C blocks **with no `NSAutoreleasePool` on the thread**. On iOS 3 + under MRC there is no implicit per-thread pool (only the main thread's + `CFRunLoop` provides one), so every autoreleased Foundation object (NSURL, + NSData, NSString, file ops, JSON, SQLite row wrappers) leaked — the device + log fills with `*** _NSAutoreleaseNoPool(): … just leaking` — and once enough + piled up the runtime faulted in `objc_msgSend` (`EXC_BAD_ACCESS at 0xe`, + crashed Thread 3). Fixed by wrapping **every** block executed on a + shim-spawned thread in its own `NSAutoreleasePool` (`ad_invoke()` in + `gcd_shim.c`: the detached trampoline, the serial worker, `dispatch_after`, + and group async/notify threads, plus their inline fallbacks). The pool is + driven through the objc runtime C API since the shim is C; `NSAutoreleasePool` + is iOS 2.0 and always present. +- ✅ **Launch crash fixed (`EXC_BAD_ACCESS` in `objc_msgSend` on the main + thread / Thread 0, fault address `0xb`).** The static blocks runtime + (`compat/shim/blocks/runtime.c`) shipped its default object-retain/-release + callouts (`_Block_retain_object_default` / `_Block_release_object_default`) as + **no-ops**. On a stock iOS, libSystem's objc runtime calls `_Block_use_RR(objc_retain, + objc_release)` during startup to wire those callouts to real retain/release, + but this self-contained shim is **never handed those callbacks** (nothing calls + `_Block_use_RR`). The result: `Block_copy` ran `_Block_object_assign` → + `_Block_retain_object()` → *nothing*, so an Objective-C object captured by a + block was **not retained on copy**. The capturing autorelease scope then drained + and freed it, and by the time the copied block ran — e.g. a + `dispatch_async(dispatch_get_main_queue(), ^{ … })` body fired off the main + `CFRunLoopTimer` hop — it messaged a dangling pointer, faulting in `objc_msgSend` + on Thread 0 (matching the crash: main thread, `libobjc` top frame, stack through + `CoreFoundation` run loop → AppDrop → the GCD main-queue shim). Fixed by making + the two default callouts actually send `-retain` / `-release` via the objc C API + (`objc_msgSend` + `sel_registerName`, both iOS 2.0), exactly as the real + `_Block_use_RR` would. This is the block-capture analogue of the worker-thread + pool fix above and uses the same primitives already proven at launch. +- ✅ **Launch crash fixed for real (`EXC_BAD_ACCESS` in `objc_msgSend` on the + main thread / Thread 0, fault address `0x12`, ~0.6 s after `makeKeyAndVisible`).** + The retain fix above was necessary but not the trigger; the actual fault was a + **double-release of the deferred block** in `dispatch_after`'s main-queue path + (`compat/shim/gcd_shim.c`). The old code wrapped the user block `mb` in a + trampoline `^{ run_on_main(Block_copy(mb)); Block_release(mb); }`. The blocks + runtime already retains a block captured by another block on `Block_copy` and + releases it when the wrapper is destroyed (its dispose helper), so the extra + manual `Block_release(mb)` over-released `mb`: it was freed early, which in turn + disposed *its* captured objects (the `AppDelegate` `self`, the alert strings) + ahead of time. When the main `CFRunLoopTimer` finally fired ~0.6 s later (the + deferred "catalog quality" alert), it messaged those dangling objects → + `objc_msgSend` fault on Thread 0, stack through `CoreFoundation` run loop → + AppDrop → the GCD shim. This recurred verbatim across rebuilds (only the binary + addresses shifted) because the retain change didn't touch this path. Fixed by + dropping the wrapper entirely: `dispatch_after` now stores the single + `Block_copy`'d block plus an `is_main` flag, and `after_thread` hands that one + reference to `run_on_main` (which owns and releases it after its one-shot timer + fires) for the main queue, or runs+releases it inline for other queues — no + second copy, no manual over-release. +- ⚠️ The GCD shim is a small pthread implementation (serial **and** concurrent + queues, `dispatch_once`, `after`, groups, semaphores). The serial path is now + correct, but the whole thing may still need hardening under heavy load. +- ⚠️ The `.deb` `control` still declares `firmware (>= 5.0)` and AppSync + dependencies that are iOS 5+ names. For an iOS 3 install, lower the + `firmware` floor and adjust the AppSync/installipa dependency names to the + 3.x-era packages. diff --git a/ios3/build-ios3.sh b/ios3/build-ios3.sh new file mode 100644 index 0000000..68856a8 --- /dev/null +++ b/ios3/build-ios3.sh @@ -0,0 +1,297 @@ +#!/bin/sh +# build-ios3.sh — Cross-compile AppDrop for armv6 / iOS 3.1 on Linux. +# +# AppDrop is written in ARC against the iOS 7 SDK + Theos. iOS 3/4 devices have +# neither ARC, blocks, nor GCD in their system libraries, and the iOS 5.1 SDK +# (the last SDK with armv6 library slices) predates many APIs the app uses. +# +# This script makes the *unmodified ARC source* run on iOS 3 by: +# 1. Compiling against the iOS 5.1 SDK (armv6 slices) with a compat prefix +# header (AppDropCompat.h) that backfills missing iOS 6/7 SDK declarations. +# 2. Statically linking tiny runtime shims so the binary carries its own +# ARC runtime, blocks runtime and libdispatch (GCD) — none of which exist +# on an iOS 3 device. They are no-ops / native pass-throughs on iOS 5+. +# 3. Backfilling NSJSONSerialization (cJSON), NSUUID, NSData base64, and +# NSString drawing/sizing at runtime (AppDropRuntime.m / AppDropJSON.m). +# 4. Building mbedTLS for armv6. +# +# Produces: build/AppDrop.ipa (and a Cydia .deb if dpkg-deb is available) +# +# Env knobs (all optional): +# DEPLOY_TARGET - min iOS version (default 3.1) +# CLANG / AR / RANLIB / LLVM_CONFIG - toolchain binaries +# SDK_URL - override iOS 5.1 SDK source +set -e + +scriptroot="$(cd "$(dirname "$0")" && pwd)" +cd "$scriptroot" + +APP_NAME="AppDrop" # binary + .app name (user-facing brand) +ARCH="armv6" +DEPLOY_TARGET="${DEPLOY_TARGET:-3.1}" +TRIPLE="${ARCH}-apple-ios${DEPLOY_TARGET}" + +SRC="$scriptroot/../IPAInstaller" # AppDrop ObjC sources +COMPAT="$scriptroot/compat" # this backport's compat layer +work="$scriptroot/build/work" +sdk="$work/sdks/iPhoneOS5.1.sdk" +out="$scriptroot/build" +obj="$out/obj" +mkdir -p "$work/sdks" "$out" "$obj" + +CLANG="${CLANG:-clang}" +AR="${AR:-llvm-ar}" +RANLIB="${RANLIB:-llvm-ranlib}" +LLVM_CONFIG="${LLVM_CONFIG:-llvm-config}" + +# --------------------------------------------------------------------------- +# 1. iOS 5.1 SDK (last SDK with armv6 library slices) +# --------------------------------------------------------------------------- +if [ ! -d "$sdk" ]; then + printf '\n==> Fetching iOS 5.1 SDK...\n' + rm -rf "$work/sdks/_dl"; mkdir -p "$work/sdks/_dl"; cd "$work/sdks/_dl" + git init -q + git remote add origin "${SDK_URL:-https://github.com/EachAndOther/Legacy-iOS-SDKs.git}" + git config core.sparseCheckout true + echo "iPhoneOS5.1.sdk/*" > .git/info/sparse-checkout + git pull -q --depth 1 origin master + mv iPhoneOS5.1.sdk "$sdk" + cd "$scriptroot"; rm -rf "$work/sdks/_dl" +fi +file "$sdk/usr/lib/libobjc.A.dylib" | grep -q armv6 || { echo "ERROR: SDK has no armv6 slice" >&2; exit 1; } + +# --------------------------------------------------------------------------- +# 2. cctools-port (ld64, lipo, strip) + ldid (cached) +# --------------------------------------------------------------------------- +tcbin="$work/toolchain/bin"; export PATH="$tcbin:$PATH"; mkdir -p "$tcbin" +if [ ! -x "$tcbin/ld64.ld64" ] || [ ! -x "$tcbin/lipo" ] || [ ! -x "$tcbin/install_name_tool" ]; then + printf '\n==> Building cctools-port (ld64, lipo, strip, install_name_tool)...\n' + ncpus="$(nproc 2>/dev/null || echo 2)" + c=fee8115127bb849d7481ea0015f181d3ebbd33cf + cd "$work"; rm -rf "cctools-port-$c" + wget -qO- "https://github.com/Un1q32/cctools-port/archive/$c.tar.gz" | tar -xz + cd "cctools-port-$c/cctools" + ./configure --enable-silent-rules --with-llvm-config="$LLVM_CONFIG" CC="$CLANG" CXX="${CLANG}++" + make -C libstuff -j"$ncpus"; make -C libmacho -j"$ncpus" + make -C ld64 -j"$ncpus"; make -C misc strip lipo install_name_tool -j"$ncpus" + cp ld64/src/ld/ld "$tcbin/ld64.ld64"; cp misc/lipo "$tcbin/lipo"; cp misc/strip "$tcbin/cctools-strip" + cp misc/install_name_tool "$tcbin/install_name_tool" + cd "$scriptroot" +fi +if ! command -v ldid >/dev/null && [ ! -x "$tcbin/ldid" ]; then + printf '\n==> Building ldid...\n' + c=ef330422ef001ef2aa5792f4c6970d69f3c1f478 + cd "$work"; rm -rf "ldid-$c" + wget -qO- "https://github.com/ProcursusTeam/ldid/archive/$c.tar.gz" | tar -xz + cd "ldid-$c"; make CXX="${CLANG}++" LDFLAGS="-lplist-2.0"; cp ldid "$tcbin/ldid"; cd "$scriptroot" +fi +LDID="$(command -v ldid || echo "$tcbin/ldid")" + +# --------------------------------------------------------------------------- +# 3. mbedTLS for armv6 +# --------------------------------------------------------------------------- +mbeddir="$work/mbedtls-src" +mbedlib="$work/libmbedtls_all.a" +if [ ! -f "$mbedlib" ]; then + printf '\n==> Building mbedTLS (armv6)...\n' + [ -d "$mbeddir" ] || git clone --depth 1 --branch v3.6.2 https://github.com/Mbed-TLS/mbedtls.git "$mbeddir" + MF="-target $TRIPLE -arch $ARCH -isysroot $sdk -miphoneos-version-min=$DEPLOY_TARGET -Os \ + -fno-modules -Wno-everything -I$mbeddir/include -I$mbeddir/library \ + -DMBEDTLS_HAVE_TIME -DMBEDTLS_HAVE_TIME_DATE" + mo="$work/mbedobj"; rm -rf "$mo"; mkdir -p "$mo" + for s in "$mbeddir"/library/*.c; do + b="$(basename "$s" .c)" + # platform_util.c uses clock_gettime/CLOCK_MONOTONIC (absent on iOS 3) — + # replaced by compat/mbed_platform_compat.c below. + [ "$b" = "platform_util.c" ] && continue + [ "$b" = "platform_util" ] && continue + "$CLANG" $MF -c "$s" -o "$mo/$b.o" 2>/dev/null || true + done + "$CLANG" $MF -c "$COMPAT/mbed_platform_compat.c" -o "$mo/_platform_compat.o" + "$AR" rcs "$mbedlib" "$mo"/*.o; "$RANLIB" "$mbedlib" +fi + +# --------------------------------------------------------------------------- +# 4. Compile AppDrop (MRC) + compat layer (MRC) + blocks/GCD runtime shims +# --------------------------------------------------------------------------- +printf '\n==> Compiling AppDrop for %s...\n' "$TRIPLE" +rm -f "$obj"/*.o + +# AppDrop sources, compiled under Manual Reference Counting (-fno-objc-arc). +# iOS 3/4's libobjc has no ARC runtime (objc_retain/release/storeStrong, zeroing +# __weak), so the app was historically compiled ARC + a fake ARC runtime shim. +# Under MRC, clang emits ordinary -retain/-release/-autorelease *message sends* +# that the iOS 3 runtime implements natively — no ARC shim, no libarclite. Every +# .m EXCEPT the iOS-5 subscript shim (superseded by AppDropRuntime.m). +MRCAPP="-target $TRIPLE -isysroot $sdk -fno-objc-arc -fobjc-abi-version=2 \ + -include $COMPAT/AppDropCompat.h -I$mbeddir/include \ + -Wno-deprecated-declarations -Wno-unused-command-line-argument -Os" +for s in "$SRC"/*.m; do + b="$(basename "$s" .m)" + [ "$b" = "IOS5Compat" ] && continue # replaced by AppDropRuntime.m + "$CLANG" $MRCAPP -c "$s" -o "$obj/app_$b.o" +done + +# MRC compat (runtime plumbing must not be ARC-managed) +MRC="-target $TRIPLE -isysroot $sdk -fno-objc-arc -fobjc-abi-version=2 -Wno-everything -Os" +"$CLANG" $MRC -include "$COMPAT/AppDropCompat.h" -c "$COMPAT/AppDropRuntime.m" -o "$obj/compat_runtime.o" +"$CLANG" $MRC -include "$COMPAT/AppDropCompat.h" -I"$COMPAT" -c "$COMPAT/AppDropJSON.m" -o "$obj/compat_json.o" +"$CLANG" $MRC -include "$COMPAT/AppDropCompat.h" -c "$COMPAT/AppDropBezier.m" -o "$obj/compat_bezier.o" +"$CLANG" $MRC -include "$COMPAT/AppDropCompat.h" -c "$COMPAT/AppDropGestures.m" -o "$obj/compat_gestures.o" +"$CLANG" $MRC -include "$COMPAT/AppDropCompat.h" -I"$COMPAT" -c "$COMPAT/AppDropBlocks.m" -o "$obj/compat_blocks.o" +"$CLANG" $MRC -include "$COMPAT/AppDropCompat.h" -I"$COMPAT" -c "$COMPAT/AppDropBlockOp.m" -o "$obj/compat_blockop.o" +"$CLANG" $MRC -include "$COMPAT/AppDropCompat.h" -I"$COMPAT" -c "$COMPAT/AppDropCache.m" -o "$obj/compat_cache.o" +"$CLANG" $MRC -I"$COMPAT" -c "$COMPAT/cJSON.c" -o "$obj/compat_cjson.o" + +# Runtime shims: blocks + GCD only. iOS 3 has no blocks runtime and no +# libdispatch regardless of ARC/MRC, so the binary still carries its own. +# (The former arc_shim.m is gone — MRC needs no ARC runtime.) +"$CLANG" $MRC -I"$COMPAT/shim/blocks" -DHAVE_OBJC=1 \ + -DHAVE_SYNC_BOOL_COMPARE_AND_SWAP_INT=1 -DHAVE_SYNC_BOOL_COMPARE_AND_SWAP_LONG=1 \ + -c "$COMPAT/shim/blocks/runtime.c" -o "$obj/shim_blocks_rt.o" +"$CLANG" $MRC -I"$COMPAT/shim/blocks" -c "$COMPAT/shim/blocks/data.c" -o "$obj/shim_blocks_data.o" +"$CLANG" $MRC -c "$COMPAT/shim/gcd_shim.c" -o "$obj/shim_gcd.o" +"$CLANG" $MRC -c "$COMPAT/shim/gcd_mainq.c" -o "$obj/shim_gcd_mainq.o" + +# --------------------------------------------------------------------------- +# 5. Link +# --------------------------------------------------------------------------- +printf '\n==> Linking...\n' +# MRC: no -fobjc-arc, no libarclite. The objc runtime calls are plain message +# sends resolved by libobjc on the device. +"$CLANG" -target "$TRIPLE" -isysroot "$sdk" \ + -fuse-ld=ld64 -mlinker-version=762 \ + -Wl,-headerpad_max_install_names \ + -framework UIKit -framework Foundation -framework CoreGraphics \ + -framework QuartzCore -framework CFNetwork -framework SystemConfiguration \ + -lsqlite3 -lz \ + "$obj"/*.o "$mbedlib" \ + -o "$out/$APP_NAME" 2> "$work/link.err" || { cat "$work/link.err" >&2; exit 1; } +file "$out/$APP_NAME" + +# --------------------------------------------------------------------------- +# 5b. Verify ImageIO is NOT linked (it is dlopen()ed at runtime instead) +# --------------------------------------------------------------------------- +# ImageIO lives at /System/Library/Frameworks/ on iOS 4+ but at +# /System/Library/PrivateFrameworks/ on iOS 3.1.3. A hard LC_LOAD_DYLIB on +# either path makes dyld abort at launch on the OTHER OS. So the binary must +# carry NO ImageIO load command at all: IconLoader.m dlopen()s the public path +# first (iOS 4+), then the private path (iOS 3), and dlsym()s every symbol. +# NOTE: a plain `grep` on the binary can NOT be used here anymore — the two +# dlopen() path strings in IconLoader.m legitimately live in __cstring. Inspect +# the actual LC_LOAD_DYLIB load commands and the undefined-symbol table instead. +printf '\n==> Verifying ImageIO is not hard-linked (runtime dlopen instead)...\n' +OTOOL="$(command -v llvm-otool || command -v otool || true)" +if [ -n "$OTOOL" ]; then + if "$OTOOL" -L "$out/$APP_NAME" 2>/dev/null | grep -q 'ImageIO'; then + echo "ERROR: ImageIO LC_LOAD_DYLIB found in binary. It must be dlopen()ed at runtime," >&2 + echo " not linked — a hard path crashes on iOS 3 (public) or iOS 4 (private)." >&2 + exit 1 + fi +else + echo "WARN: otool not found — skipping LC_LOAD_DYLIB check for ImageIO." >&2 +fi +imgio_syms="$(llvm-nm -u "$out/$APP_NAME" 2>/dev/null | grep -E '_(CGImageSource|kCGImageSource)' || true)" +if [ -n "$imgio_syms" ]; then + echo "ERROR: undefined ImageIO symbols remain (must be resolved via dlsym, not the linker):" >&2 + echo "$imgio_syms" >&2 + exit 1 +fi +echo "OK: no ImageIO load command, no undefined ImageIO symbols — one binary loads on both iOS 3 and 4." + +# Fail loudly if any ARC/blocks/GCD symbol leaked in as an unresolved import +# (would crash on a real iOS 3 device). ARC C-functions must NOT appear at all +# now that the app is MRC — if they do, something is still compiled ARC. +leaked="$(llvm-nm -mu "$out/$APP_NAME" 2>/dev/null | grep -iE '(_objc_(retain|release|storeStrong|storeWeak|loadWeak|autorelease)|dispatch_async|dispatch_once|Block_copy|retainBlock|imp_implementationWithBlock|imp_removeBlock)' || true)" +if [ -n "$leaked" ]; then + echo "ERROR: unresolved ARC/blocks/GCD/objc-runtime imports remain (would crash on real iOS 3):" >&2; echo "$leaked" >&2; exit 1 +fi +echo "OK: blocks/GCD resolved internally, MRC retain/release native — binary is iOS 3 self-contained." + +# Second guard: symbols that DO resolve against the 5.1 SDK at build time but do +# NOT exist on iOS 3.1.3 CoreFoundation (149/4xx), so dyld aborts at launch with +# "Symbol not found". CFRunLoopPerformBlock is iOS 4.0. Keep this list growing as +# we discover more 4.0+ APIs that slip through. +toonew="$(llvm-nm -u "$out/$APP_NAME" 2>/dev/null | grep -oE '_(CFRunLoopPerformBlock)' | sort -u || true)" +if [ -n "$toonew" ]; then + echo "ERROR: iOS 4.0+ symbols present as imports (resolve vs 5.1 SDK but missing on iOS 3.1.3 — dyld will abort at launch):" >&2; echo "$toonew" >&2; exit 1 +fi +echo "OK: no known iOS 4.0+ CoreFoundation symbols imported." + +# --------------------------------------------------------------------------- +# 6. Bundle .app, fake-sign, package .ipa (+ Cydia .deb when possible) +# --------------------------------------------------------------------------- +printf '\n==> Packaging...\n' +app="$out/Payload/$APP_NAME.app" +rm -rf "$out/Payload"; mkdir -p "$app" +cp "$out/$APP_NAME" "$app/AppDrop.armv6" +cp "$SRC/AppDropLauncher.sh" "$app/AppDrop" +chmod 0755 "$app/AppDrop" "$app/AppDrop.armv6" +cp "$SRC/Info.plist" "$app/Info.plist" 2>/dev/null || true +# bundle resources (icons, launch images, localizations) +[ -d "$SRC/Resources" ] && cp -R "$SRC/Resources/"* "$app/" 2>/dev/null || true +# compile each .lproj/Localizable.strings -> Localizable.json (iOS-6 binary-plist +# bug workaround the project relies on). The project's Localization.m prefers +# Localizable.json (parsed via NSJSONSerialization, which our shim backfills on +# iOS 3/4). We convert .strings -> .json with python3 (plutil is macOS-only; +# plistutil cannot parse .strings). +for d in "$app"/*.lproj; do + [ -d "$d" ] || continue + lang="$(basename "$d" .lproj)" + src="$SRC/Resources/$lang.lproj/Localizable.strings" + [ -f "$src" ] || continue + python3 - "$src" "$d/Localizable.json" <<'PYEOF' || echo " WARN: $lang localization not converted" +import sys, re, json +src, dst = sys.argv[1], sys.argv[2] +text = open(src, encoding="utf-8").read() +# strip /* */ and // comments +text = re.sub(r"/\*.*?\*/", "", text, flags=re.S) +text = re.sub(r"//[^\n]*", "", text) +pat = re.compile(r'"((?:[^"\\]|\\.)*)"\s*=\s*"((?:[^"\\]|\\.)*)"\s*;', re.S) +def unesc(s): + return s.encode().decode("unicode_escape") if "\\" in s else s +d = {} +for k, v in pat.findall(text): + d[unesc(k)] = unesc(v) +json.dump(d, open(dst, "w", encoding="utf-8"), ensure_ascii=False) +print(" %s: %d strings" % (dst.split("/")[-2], len(d))) +PYEOF +done + +"$LDID" -S"$SRC/entitlements.plist" "$app/AppDrop.armv6" + +cd "$out"; rm -f "$APP_NAME.ipa"; zip -qr "AppDrop.ipa" Payload; cd "$scriptroot" + +# Cydia .deb (Architecture: iphoneos-arm) if dpkg-deb present +if command -v dpkg-deb >/dev/null; then + deb="$out/deb"; rm -rf "$deb" + mkdir -p "$deb/Applications" "$deb/DEBIAN" + cp -R "$app" "$deb/Applications/" + cp "$SRC/control" "$deb/DEBIAN/control" 2>/dev/null || true + # iOS 3 packaging fixups. The shared control already targets iOS 3 + # (firmware >= 3.0, compatible_min::ios3.0). These seds are idempotent + # safety nets in case the shared control is ever bumped back to iOS 5, + # plus they strip the optional external-installer Recommends line: + # AppDrop installs in-process via MobileInstallationInstall, and «IPA + # Installer Console» / appinst both require firmware >= 4.0 so Cydia + # can't install them on iOS 3 anyway. (AppSync Unified is still needed + # to allow unsigned/cracked .ipas, so it stays in Depends.) + if [ -f "$deb/DEBIAN/control" ]; then + sed -i \ + -e 's/firmware (>= 5\.0)/firmware (>= 3.0)/g' \ + -e 's/, *com\.autopear\.installipa *| *ai\.akemi\.appinst//g' \ + -e 's/com\.autopear\.installipa *| *ai\.akemi\.appinst, *//g' \ + -e '/^Recommends: *com\.autopear\.installipa *| *ai\.akemi\.appinst *$/d' \ + -e 's/compatible_min::ios5\.0/compatible_min::ios3.0/g' \ + "$deb/DEBIAN/control" + fi + if [ -d "$SRC/Layout/DEBIAN" ]; then + cp -f "$SRC/Layout/DEBIAN/postinst" "$deb/DEBIAN/" 2>/dev/null || true + cp -f "$SRC/Layout/DEBIAN/postrm" "$deb/DEBIAN/" 2>/dev/null || true + chmod 0755 "$deb/DEBIAN/postinst" "$deb/DEBIAN/postrm" 2>/dev/null || true + fi + dpkg-deb -Zgzip -b "$deb" "$out/AppDrop.deb" >/dev/null 2>&1 && \ + printf 'DEB: %s\n' "$out/AppDrop.deb" || true +fi + +printf '\nDone.\n IPA: %s\n' "$out/AppDrop.ipa" diff --git a/ios3/ci-ios3.yml.txt b/ios3/ci-ios3.yml.txt new file mode 100644 index 0000000..f532ca8 --- /dev/null +++ b/ios3/ci-ios3.yml.txt @@ -0,0 +1,45 @@ +name: Build AppDrop iOS 3 (armv6) + +on: + push: + branches: [ iOS3iThink ] + workflow_dispatch: + +jobs: + ios3-armv6: + name: Cross-compile armv6 / iOS 3 (iOS 5.1 SDK) + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache toolchain + SDK + mbedTLS + uses: actions/cache@v4 + with: + path: ios3/build/work + key: appdrop-ios3-toolchain-v1 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + clang lld llvm llvm-dev \ + cmake make git wget zip dpkg \ + libplist-dev libplist-utils libssl-dev python3 + + - name: Build IPA + DEB + run: | + chmod +x ios3/build-ios3.sh + ./ios3/build-ios3.sh + + - name: Upload IPA + uses: actions/upload-artifact@v4 + with: + name: AppDrop-iOS3-armv6-ipa + path: ios3/build/AppDrop.ipa + + - name: Upload DEB + uses: actions/upload-artifact@v4 + with: + name: AppDrop-iOS3-armv6-deb + path: ios3/build/AppDrop.deb diff --git a/ios3/compat/AppDropBezier.m b/ios3/compat/AppDropBezier.m new file mode 100644 index 0000000..40e592b --- /dev/null +++ b/ios3/compat/AppDropBezier.m @@ -0,0 +1,185 @@ +// AppDropBezier.m — self-contained UIBezierPath for iOS 3.1.x / armv6. +// +// UIBezierPath first shipped in iOS 3.2. On iOS 3.1.3 the class symbol is +// present in UIKit but its drawing methods are NOT implemented, so the very +// first glyph the app draws at launch (the home/star tab icons in +// AppDelegate.m, drawn from -setupAppearance) throws: +// +// *** -[UIBezierPath addLineToPoint:]: unrecognized selector sent to +// instance 0x2025a0 +// EXCEPTION in setup: NSInvalidArgumentException +// +// The launch @try in -application:didFinishLaunchingWithOptions: catches it and +// shows the "AppDrop failed to launch" screen. (Verified: appdrop-launch.log +// reaches "alloc CatalogVC", then the addLineToPoint: exception fires.) +// +// Rather than try to backfill methods onto Apple's partial 3.1 UIBezierPath +// (whose private ivars/CGPath we cannot reach), this ships a complete, +// self-contained class — ADBezierPath — backed by its own CGMutablePathRef, +// using only CoreGraphics primitives that have existed since iOS 2. The prefix +// header (AppDropCompat.h) does `#define UIBezierPath ADBezierPath`, so every +// existing call site uses this class with no source edits. Drawing output is +// identical (it's the same CoreGraphics fill/stroke/clip underneath), and it +// behaves the same on iOS 3.2–10 where the real class also works. +// +// This mirrors AppDropJSON.m, which likewise supplies a missing Foundation +// class (NSJSONSerialization) self-contained rather than depending on the OS. +// +// API surface implemented (exactly what the AppDrop sources use): +// + bezierPath +// + bezierPathWithRect: +// + bezierPathWithOvalInRect: +// + bezierPathWithRoundedRect:cornerRadius: +// - moveToPoint: +// - addLineToPoint: +// - closePath +// - fill +// - stroke +// - addClip +// - CGPath (used by IconLoader: CGContextAddPath(ctx, p.CGPath)) +// @property lineWidth (default 1.0, as UIBezierPath) +// @property lineCapStyle (CGLineCap) +// @property lineJoinStyle (CGLineJoin) +// @property usesEvenOddFillRule (default NO) + +#import +#import + +// The prefix header is force-included (-include AppDropCompat.h) and already +// declares @interface ADBezierPath + the `#define UIBezierPath ADBezierPath`. +// Undefine here so the @implementation keeps its real name (the macro must not +// rewrite the token inside this file). +#ifdef UIBezierPath +#undef UIBezierPath +#endif + +@implementation ADBezierPath { + CGMutablePathRef _path; +} + +@synthesize lineWidth = _lineWidth; +@synthesize lineCapStyle = _lineCapStyle; +@synthesize lineJoinStyle = _lineJoinStyle; +@synthesize usesEvenOddFillRule = _usesEvenOddFillRule; + +- (id)init { + self = [super init]; + if (self) { + _path = CGPathCreateMutable(); + _lineWidth = 1.0; // UIBezierPath default + _lineCapStyle = kCGLineCapButt; + _lineJoinStyle = kCGLineJoinMiter; + _usesEvenOddFillRule = NO; + } + return self; +} + +- (void)dealloc { + if (_path) CGPathRelease(_path); + [super dealloc]; +} + +#pragma mark - Constructors + ++ (ADBezierPath *)bezierPath { + return [[[self alloc] init] autorelease]; +} + ++ (ADBezierPath *)bezierPathWithRect:(CGRect)rect { + ADBezierPath *p = [self bezierPath]; + CGPathAddRect(p->_path, NULL, rect); + return p; +} + ++ (ADBezierPath *)bezierPathWithOvalInRect:(CGRect)rect { + ADBezierPath *p = [self bezierPath]; + CGPathAddEllipseInRect(p->_path, NULL, rect); + return p; +} + ++ (ADBezierPath *)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)radius { + ADBezierPath *p = [self bezierPath]; + CGFloat maxR = MIN(CGRectGetWidth(rect), CGRectGetHeight(rect)) / 2.0; + CGFloat r = radius; + if (r < 0) r = 0; + if (r > maxR) r = maxR; + + if (r <= 0) { // degenerate -> plain rectangle + CGPathAddRect(p->_path, NULL, rect); + return p; + } + + CGFloat minX = CGRectGetMinX(rect), minY = CGRectGetMinY(rect); + CGFloat maxX = CGRectGetMaxX(rect), maxY = CGRectGetMaxY(rect); + + // Clockwise rounded rectangle (matches UIBezierPath's winding). + CGPathMoveToPoint(p->_path, NULL, minX + r, minY); + CGPathAddLineToPoint(p->_path, NULL, maxX - r, minY); + CGPathAddArc(p->_path, NULL, maxX - r, minY + r, r, -M_PI_2, 0.0, NO); + CGPathAddLineToPoint(p->_path, NULL, maxX, maxY - r); + CGPathAddArc(p->_path, NULL, maxX - r, maxY - r, r, 0.0, M_PI_2, NO); + CGPathAddLineToPoint(p->_path, NULL, minX + r, maxY); + CGPathAddArc(p->_path, NULL, minX + r, maxY - r, r, M_PI_2, M_PI, NO); + CGPathAddLineToPoint(p->_path, NULL, minX, minY + r); + CGPathAddArc(p->_path, NULL, minX + r, minY + r, r, M_PI, M_PI + M_PI_2, NO); + CGPathCloseSubpath(p->_path); + return p; +} + +#pragma mark - Path construction + +- (void)moveToPoint:(CGPoint)point { + CGPathMoveToPoint(_path, NULL, point.x, point.y); +} + +- (void)addLineToPoint:(CGPoint)point { + // If no current point yet, CGPathAddLineToPoint is a no-op on an empty path; + // start the subpath so a stray addLine before move still behaves sanely. + if (CGPathIsEmpty(_path)) { + CGPathMoveToPoint(_path, NULL, point.x, point.y); + } else { + CGPathAddLineToPoint(_path, NULL, point.x, point.y); + } +} + +- (void)closePath { + if (!CGPathIsEmpty(_path)) CGPathCloseSubpath(_path); +} + +#pragma mark - Drawing + +- (void)fill { + CGContextRef c = UIGraphicsGetCurrentContext(); + if (!c) return; + CGContextSaveGState(c); + CGContextAddPath(c, _path); + if (_usesEvenOddFillRule) CGContextEOFillPath(c); else CGContextFillPath(c); + CGContextRestoreGState(c); +} + +- (void)stroke { + CGContextRef c = UIGraphicsGetCurrentContext(); + if (!c) return; + CGContextSaveGState(c); + CGContextAddPath(c, _path); + CGContextSetLineWidth(c, _lineWidth); + CGContextSetLineCap(c, _lineCapStyle); + CGContextSetLineJoin(c, _lineJoinStyle); + CGContextStrokePath(c); + CGContextRestoreGState(c); +} + +- (void)addClip { + CGContextRef c = UIGraphicsGetCurrentContext(); + if (!c) return; + CGContextAddPath(c, _path); + if (_usesEvenOddFillRule) CGContextEOClip(c); else CGContextClip(c); +} + +#pragma mark - CGPath bridge + +- (CGPathRef)CGPath { + return _path; // owned by the receiver; callers must not release (UIBezierPath semantics) +} + +@end diff --git a/ios3/compat/AppDropBlockOp.m b/ios3/compat/AppDropBlockOp.m new file mode 100644 index 0000000..e28d326 --- /dev/null +++ b/ios3/compat/AppDropBlockOp.m @@ -0,0 +1,85 @@ +// AppDropBlockOp.m — NSBlockOperation backport for iOS 3.1. +// +// NSBlockOperation is iOS 4.0+. iOS 3.x ships NSOperation and NSOperationQueue, +// just not the block convenience subclass. AppDrop's IconLoader enqueues every +// network icon fetch as +[NSBlockOperation blockOperationWithBlock:], so on a +// real 3.1 device the first uncached icon throws: +// +// *** +[NSBlockOperation blockOperationWithBlock:]: unrecognized selector +// +// AppDropCompat.h macro-rewrites `NSBlockOperation` → `ADBlockOperation` (this +// class) so every call site is unchanged. We subclass NSOperation (which DOES +// exist on 3.1) and run the stored blocks in -main. NSOperationQueue drives +// -start/-main and KVO for isExecuting/isFinished natively, so a plain +// non-concurrent NSOperation is all we need. +// +// Blocks are NOT ObjC objects on iOS 3 (see AppDropBlocks.h), so we store and +// release them through the C blocks runtime, never via -copy/-retain messages. +// +// MRC (-fno-objc-arc). + +#import +#import "AppDropBlocks.h" // _Block_copy / _Block_release + +// Keep the real ADBlockOperation name in this file (undo the compat.h rewrite). +#undef NSBlockOperation + +@implementation ADBlockOperation { + NSMutableArray *_blockBoxes; // ADBlockBox elements (each owns a heap block) + BOOL _adStarted; // dedupe guard for the iOS 3.x/4.x queue bug +} + ++ (instancetype)blockOperationWithBlock:(void (^)(void))block { + ADBlockOperation *op = [[[self alloc] init] autorelease]; + if (block) [op addExecutionBlock:block]; + return op; +} + +- (id)init { + if ((self = [super init])) { + _blockBoxes = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)addExecutionBlock:(void (^)(void))block { + if (!block) return; + // ADBlockBox heap-copies via _Block_copy and releases in its -dealloc; the + // array retains the BOX (a real NSObject), never the block itself. + [_blockBoxes addObject:[ADBlockBox boxWithBlock:block]]; +} + +// iOS 3.x/4.x NSOperationQueue has a scheduling race: when the queue runs with +// maxConcurrentOperationCount > 1 AND the queuePriority of already-enqueued +// operations is mutated (IconLoader does this for visible-first icon ordering +// while the catalog scrolls), the scheduler can send -start to the SAME +// operation twice. NSOperation's own -start then throws: +// *** -[ADBlockOperation start]: receiver has already started or finished +// Apple fixed this in iOS 5; on 3.1 we self-defend. The guard makes a second +// -start a no-op so [super start] (which drives isExecuting/isFinished KVO the +// queue depends on) is only ever invoked once. +- (void)start { + @synchronized (self) { + if (_adStarted || self.isExecuting || self.isFinished) return; + _adStarted = YES; + } + [super start]; +} + +- (void)main { + // NSOperationQueue has already transitioned us to executing; just run the + // blocks. (NSBlockOperation runs its execution blocks concurrently, but + // AppDrop only ever adds one, and serial execution is a safe superset.) + if (self.isCancelled) return; + for (ADBlockBox *box in [[_blockBoxes copy] autorelease]) { + if (self.isCancelled) break; + [box invoke]; + } +} + +- (void)dealloc { + [_blockBoxes release]; + [super dealloc]; +} + +@end diff --git a/ios3/compat/AppDropBlocks.h b/ios3/compat/AppDropBlocks.h new file mode 100644 index 0000000..98898bd --- /dev/null +++ b/ios3/compat/AppDropBlocks.h @@ -0,0 +1,81 @@ +// AppDropBlocks.h — safe block storage on iOS 3.x (blocks are NOT ObjC objects). +// +// THE PROBLEM +// ----------- +// The Objective-C `NSBlock` class hierarchy first ships in iOS 4. On iOS 3.x the +// compiler still stamps every block's `isa` with one of the `_NSConcrete*Block` +// symbols, but nothing back-fills those symbols with a real class — in this +// project's bundled blocks runtime they are literally `void *[32] = {0}` (32 zero +// words; see ios3/compat/shim/blocks/data.c). Therefore sending a block ANY +// Objective-C message dereferences a garbage Class and crashes: +// +// EXC_BAD_ACCESS (SIGBUS) at 0x0 in objc_msgSend +// +// The compiler/runtime sends a block an ObjC message in three common situations: +// 1. `@property(copy)` block setter → objc_setProperty → -copyWithZone: +// 2. an explicit `[someBlock copy]` → -copy +// 3. storing a block in an NSArray/NSDictionary → the collection -retains it +// +// All three are landmines on iOS 3. The BUNDLED C blocks runtime, in contrast, +// copies/releases a block WITHOUT messaging it, via the C functions _Block_copy +// and _Block_release. This header routes every block lifetime op through those. +// +// USAGE +// ----- +// * Replace a synthesized `@property(copy)` block setter/getter with a manual +// pair backed by an ivar, using AD_BLOCK_ACCESSORS (declare the ivar + +// `@dynamic name;` in the @implementation). +// * To stash a block in a collection, box it with ADBlockBox (it retains the +// block with _Block_copy and releases it with _Block_release in -dealloc), +// then call -invoke / -block as needed. +// +// MRC throughout (-fno-objc-arc), matching the rest of the iOS 3 compat layer. + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +// From the bundled C blocks runtime (ios3/compat/shim/blocks). These copy a +// stack block to the heap / bump-or-free a heap block's refcount WITHOUT ever +// sending the block an Objective-C message. +extern void *_Block_copy(const void *aBlock); +extern void _Block_release(const void *aBlock); + +#ifdef __cplusplus +} +#endif + +// Generate a manual getter/setter pair for a copy-semantics block @property, +// backed by `ivar`. Heap-copies the incoming block with _Block_copy and frees +// the previous one with _Block_release — never messages a block. Use inside an +// @implementation together with the matching ivar and `@dynamic ;`. +// +// @implementation Foo { +// void (^_onTap)(void); +// } +// @dynamic onTap; +// AD_BLOCK_ACCESSORS(onTap, setOnTap, _onTap, void(^)(void)) +// +#define AD_BLOCK_ACCESSORS(GETTER, SETTER, IVAR, BLOCKTYPE) \ + - (BLOCKTYPE)GETTER { return IVAR; } \ + - (void)SETTER:(BLOCKTYPE)blk { \ + if (blk == IVAR) return; \ + if (blk) blk = (BLOCKTYPE)_Block_copy((const void *)blk); \ + if (IVAR) _Block_release((const void *)IVAR); \ + IVAR = blk; \ + } + +// A retain-counted ObjC wrapper that owns a block via the C runtime, so blocks +// can live inside NSArray/NSDictionary (the collection retains the BOX, never +// the block). -invoke runs a zero-argument block; for other shapes, read -block +// and cast. +@interface ADBlockBox : NSObject { + void (^_block)(void); +} ++ (instancetype)boxWithBlock:(void (^)(void))block; // copies a void(^)(void) block ++ (instancetype)boxWithImageBlock:(void (^)(id image))block; // copies a void(^)(id) block (e.g. UIImage *) +- (void (^)(void))block; // the heap block (not copied); cast as needed +- (void)invoke; // calls the block if present (zero-arg shape) +@end diff --git a/ios3/compat/AppDropBlocks.m b/ios3/compat/AppDropBlocks.m new file mode 100644 index 0000000..b581956 --- /dev/null +++ b/ios3/compat/AppDropBlocks.m @@ -0,0 +1,34 @@ +// AppDropBlocks.m — ADBlockBox implementation. See AppDropBlocks.h for why. +// MRC (-fno-objc-arc). + +#import "AppDropBlocks.h" + +@implementation ADBlockBox + ++ (instancetype)boxWithBlock:(void (^)(void))block { + ADBlockBox *b = [[[self alloc] init] autorelease]; + if (block) { + // Heap-copy via the C runtime (never message the block). + b->_block = (void (^)(void))_Block_copy((const void *)block); + } + return b; +} + ++ (instancetype)boxWithImageBlock:(void (^)(id))block { + ADBlockBox *b = [[[self alloc] init] autorelease]; + if (block) { + b->_block = (void (^)(void))_Block_copy((const void *)block); + } + return b; +} + +- (void (^)(void))block { return _block; } + +- (void)invoke { if (_block) _block(); } + +- (void)dealloc { + if (_block) _Block_release((const void *)_block); + [super dealloc]; +} + +@end diff --git a/ios3/compat/AppDropCache.m b/ios3/compat/AppDropCache.m new file mode 100644 index 0000000..01b8912 --- /dev/null +++ b/ios3/compat/AppDropCache.m @@ -0,0 +1,149 @@ +// AppDropCache.m — NSCache replacement for iOS 3.x / armv6. +// +// NSCache debuts in iOS 4.0. The 5.1 SDK declares it (so call sites compile), +// but on a real iOS 3.1.3 device the class symbol is weak-imported and binds to +// NULL, so `[[NSCache alloc] init]` sends +alloc to a nil class and returns nil. +// AppDrop's IconLoader keeps every decoded icon in an NSCache; with the cache +// nil, -objectForKey: always misses and -setObject:forKey:cost: is a no-op, so +// every scroll re-reads + re-decodes icons from disk (visible stutter) and, in +// the worst case, the dead RAM tier leaves icons looking like they "never load" +// next to the iOS 6 build. +// +// ADCache is a complete, self-contained NSCache work-alike built only on +// iOS-2-era Foundation (NSMutableDictionary + @synchronized). It implements the +// subset of the API AppDrop uses — -objectForKey:, -setObject:forKey:, +// -setObject:forKey:cost:, -removeObjectForKey:, -removeAllObjects, plus the +// countLimit / totalCostLimit properties — with simple LRU eviction so both +// limits are actually honoured (real NSCache eviction is unspecified; LRU is a +// strict, predictable superset that behaves well for an icon cache). +// +// AppDropCompat.h macro-rewrites every `NSCache` token in the AppDrop sources to +// ADCache, so there are zero call-site edits and identical behaviour on iOS 3.1 +// through 10 — the same strategy as ADBezierPath / ADBlockOperation / the +// gesture recognizers. This file #undefs the macro so its @implementation keeps +// the real ADCache name. +// +// Compiled MRC (-fno-objc-arc), like the rest of the compat layer. + +#import "AppDropCompat.h" +#undef NSCache + +@interface ADCache () +{ + NSMutableDictionary *_store; // key -> value (retained) + NSMutableDictionary *_costs; // key -> NSNumber(cost) + NSMutableArray *_order; // keys, oldest (LRU) first … newest last + NSUInteger _totalCost; +} +@end + +@implementation ADCache + +@synthesize name = _name; +@synthesize countLimit = _countLimit; +@synthesize totalCostLimit = _totalCostLimit; +@synthesize delegate = _delegate; +@synthesize evictsObjectsWithDiscardedContent = _evictsObjectsWithDiscardedContent; + +- (id)init { + if ((self = [super init])) { + _store = [[NSMutableDictionary alloc] init]; + _costs = [[NSMutableDictionary alloc] init]; + _order = [[NSMutableArray alloc] init]; + _totalCost = 0; + _countLimit = 0; // 0 == no limit, matching NSCache + _totalCostLimit = 0; // 0 == no limit, matching NSCache + } + return self; +} + +- (void)dealloc { + [_store release]; + [_costs release]; + [_order release]; + [_name release]; + [super dealloc]; +} + +// Mark `key` as most-recently-used. Caller must hold the lock. +- (void)ad_touchKey:(id)key { + NSUInteger idx = [_order indexOfObject:key]; + if (idx != NSNotFound) [_order removeObjectAtIndex:idx]; + [_order addObject:key]; +} + +// Drop `key` entirely, keeping the running cost in sync. Caller holds the lock. +- (void)ad_removeKeyLocked:(id)key { + NSNumber *c = [_costs objectForKey:key]; + if (c) _totalCost -= [c unsignedIntegerValue]; + [_store removeObjectForKey:key]; + [_costs removeObjectForKey:key]; + NSUInteger idx = [_order indexOfObject:key]; + if (idx != NSNotFound) [_order removeObjectAtIndex:idx]; +} + +// Evict oldest entries until both limits are satisfied. Caller holds the lock. +- (void)ad_evictLocked { + // Honour countLimit. + if (_countLimit > 0) { + while ([_order count] > _countLimit) { + id victim = [[_order objectAtIndex:0] retain]; + [self ad_removeKeyLocked:victim]; + [victim release]; + } + } + // Honour totalCostLimit. + if (_totalCostLimit > 0) { + while (_totalCost > _totalCostLimit && [_order count] > 0) { + id victim = [[_order objectAtIndex:0] retain]; + [self ad_removeKeyLocked:victim]; + [victim release]; + } + } +} + +- (id)objectForKey:(id)key { + if (!key) return nil; + @synchronized (self) { + id obj = [_store objectForKey:key]; + if (obj) [self ad_touchKey:key]; + return [[obj retain] autorelease]; + } +} + +- (void)setObject:(id)obj forKey:(id)key { + [self setObject:obj forKey:key cost:0]; +} + +- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)cost { + if (!key) return; + if (!obj) { [self removeObjectForKey:key]; return; } + @synchronized (self) { + // Replace any existing entry's cost contribution first. + NSNumber *old = [_costs objectForKey:key]; + if (old) _totalCost -= [old unsignedIntegerValue]; + [_store setObject:obj forKey:key]; + [_costs setObject:[NSNumber numberWithUnsignedInteger:cost] forKey:key]; + _totalCost += cost; + [self ad_touchKey:key]; + [self ad_evictLocked]; + } +} + +- (void)removeObjectForKey:(id)key { + if (!key) return; + @synchronized (self) { + [self ad_removeKeyLocked:key]; + } +} + +- (void)removeAllObjects { + @synchronized (self) { + [_store removeAllObjects]; + [_costs removeAllObjects]; + [_order removeAllObjects]; + _totalCost = 0; + } +} + +@end diff --git a/ios3/compat/AppDropCompat.h b/ios3/compat/AppDropCompat.h new file mode 100644 index 0000000..5be3349 --- /dev/null +++ b/ios3/compat/AppDropCompat.h @@ -0,0 +1,338 @@ +// AppDropCompat.h — iOS 3 / armv6 backport prefix header. +// +// AppDrop targets the iOS 5.1 SDK (the last SDK that ships armv6 library +// slices). That SDK predates a pile of syntax/API the codebase relies on +// (NS_ENUM, modern subscripting, @YES/@NO boxing, renamed UIKit constants, +// NSUUID, base64, attributed string drawing, …). This header is force- +// included (clang -include) into every translation unit and declares the +// missing pieces so the MRC source compiles. Runtime behaviour for the +// methods declared here is supplied by AppDropRuntime.m. +// +// The app is compiled with -fno-objc-arc: memory management uses native +// retain/release message sends (present since iOS 2), so no ARC runtime shim +// or libarclite is needed. Pair this header only with the static blocks/ and +// gcd_shim.c runtimes, which iOS 3's libSystem never shipped (true 3.x +// hardware has no blocks runtime and no libdispatch). + +#import +#import + +// ---- Safe block storage on iOS 3 (blocks are NOT ObjC objects) ---- +// AD_BLOCK_ACCESSORS + ADBlockBox: never send a block an ObjC message; copy/ +// release via the bundled C blocks runtime instead. See AppDropBlocks.h. +#import "AppDropBlocks.h" + +// ---- AD_WEAK: weak-reference qualifier for the iOS 3 MRC target ---- +// True zeroing __weak needs the iOS 5+ ObjC runtime (objc_loadWeak / +// objc_storeWeak with side tables), which iOS 3/4 devices do not have. Under +// MRC we use __unsafe_unretained — a non-zeroing weak ref that breaks retain +// cycles without retaining. This matches the behaviour the old ARC shim +// already provided (its objc_storeWeak was a plain assign), but compiles +// cleanly and carries no phantom runtime dependency. Every former __weak / +// weak-property site routes through this one macro so a real zeroing-weak +// implementation can be swapped in centrally if one is ever back-ported. +#ifndef AD_WEAK +#define AD_WEAK __unsafe_unretained +#endif + +// ---- NS_ENUM / NS_OPTIONS: iOS 6 SDK macros, absent from the 5.1 SDK ---- +#ifndef NS_ENUM +#define NS_ENUM(_type, _name) _type _name; enum +#endif +#ifndef NS_OPTIONS +#define NS_OPTIONS(_type, _name) _type _name; enum +#endif + +// ---- Modern subscripting (declared in iOS 6 SDK headers) ---- +@interface NSObject (AppDropSubscript) +- (id)objectForKeyedSubscript:(id)key; +- (void)setObject:(id)obj forKeyedSubscript:(id)key; +- (id)objectAtIndexedSubscript:(NSUInteger)idx; +- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx; +@end + +// ---- Text alignment / line break: renamed in iOS 6 (UI* -> NS*) ---- +#ifndef NSTextAlignmentLeft +#define NSTextAlignmentLeft UITextAlignmentLeft +#define NSTextAlignmentCenter UITextAlignmentCenter +#define NSTextAlignmentRight UITextAlignmentRight +#endif +#ifndef NSLineBreakByWordWrapping +#define NSLineBreakByWordWrapping UILineBreakModeWordWrap +#define NSLineBreakByCharWrapping UILineBreakModeCharacterWrap +#define NSLineBreakByClipping UILineBreakModeClip +#define NSLineBreakByTruncatingHead UILineBreakModeHeadTruncation +#define NSLineBreakByTruncatingTail UILineBreakModeTailTruncation +#define NSLineBreakByTruncatingMiddle UILineBreakModeMiddleTruncation +#endif + +#import +#import + +// ---- UIBezierPath: present but NON-FUNCTIONAL on iOS 3.1 (real class debuts +// in iOS 3.2). On 3.1.3 the class symbol exists yet its drawing methods +// (moveToPoint:/addLineToPoint:/fill/stroke/...) are unimplemented, so the +// first glyph drawn at launch throws: +// *** -[UIBezierPath addLineToPoint:]: unrecognized selector +// and the launch @try shows "AppDrop failed to launch". +// +// ADBezierPath (AppDropBezier.m) is a complete, self-contained CGPath- +// backed replacement using only iOS-2-era CoreGraphics. The macro below +// transparently rewrites every `UIBezierPath` token in the AppDrop sources +// to ADBezierPath, so there are zero call-site edits and identical drawing +// output on iOS 3.1 through 10. (Same strategy as AppDropJSON.m, which +// ships its own NSJSONSerialization.) AppDropBezier.m #undefs this so its +// @implementation keeps the real ADBezierPath name. ---- +@interface ADBezierPath : NSObject +@property (nonatomic, assign) CGFloat lineWidth; +@property (nonatomic, assign) CGLineCap lineCapStyle; +@property (nonatomic, assign) CGLineJoin lineJoinStyle; +@property (nonatomic, assign) BOOL usesEvenOddFillRule; ++ (ADBezierPath *)bezierPath; ++ (ADBezierPath *)bezierPathWithRect:(CGRect)rect; ++ (ADBezierPath *)bezierPathWithOvalInRect:(CGRect)rect; ++ (ADBezierPath *)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)radius; +- (void)moveToPoint:(CGPoint)point; +- (void)addLineToPoint:(CGPoint)point; +- (void)closePath; +- (void)fill; +- (void)stroke; +- (void)addClip; +- (CGPathRef)CGPath; +@end +#ifndef UIBezierPath +#define UIBezierPath ADBezierPath +#endif + +// ---- UIGestureRecognizer family: present but NON-FUNCTIONAL on iOS 3.1 ---- +// The whole gesture-recognizer subsystem (UIGestureRecognizer + the +// UITap/UILongPress/UIPan subclasses, -[UIView addGestureRecognizer:], +// and the touch-routing engine that drives them) debuts in iOS 3.2. On +// 3.1.3 the class symbols exist enough to alloc/init a hollow object, but +// the property setters and the dispatch engine are dead stubs, so the +// first launch-path call throws: +// *** -[UILongPressGestureRecognizer setMinimumPressDuration:]: +// unrecognized selector → uncaught NSException → SIGABRT +// (CategoryViewController -viewDidLoad sets minimumPressDuration while +// building the Home tab — crash before any UI is shown). Verified against +// Apple's docs: minimumPressDuration is marked "iOS 3.2+". +// +// AppDropGestures.m provides complete, self-contained AD* replacements +// built only on iOS-2-era touch APIs (-[UIView touchesBegan:…], +// -[UIWindow sendEvent:], UITouch/UIEvent) plus a UIWindow dispatch +// swizzle. The macros below rewrite every UIKit gesture token in the +// AppDrop sources to the AD* class, so there are zero call-site edits and +// identical behaviour on iOS 3.1 through 10 — exactly the ADBezierPath / +// AppDropJSON strategy. AppDropGestures.m #undefs these so its +// @implementation keeps the real AD* names. +@class ADGestureRecognizer, ADTapGestureRecognizer, + ADLongPressGestureRecognizer, ADPanGestureRecognizer; +@protocol UIGestureRecognizerDelegate; // (declared in the 5.1 SDK headers) + +@interface ADGestureRecognizer : NSObject { +@protected + UIGestureRecognizerState _state; + UIView *_view; // non-retained (UIKit: view owns recognizer) + id _delegate; // non-retained + BOOL _enabled; + NSMutableArray *_targets; // [target(unsafe), NSValue(SEL)] pairs + UITouch *_trackedTouch; // single touch this recognizer follows (non-retained) +} +- (id)initWithTarget:(id)target action:(SEL)action; +- (void)addTarget:(id)target action:(SEL)action; +@property (nonatomic, readonly) UIGestureRecognizerState state; +@property (nonatomic, assign) id delegate; +@property (nonatomic, assign) BOOL enabled; +@property (nonatomic, readonly) UIView *view; +- (CGPoint)locationInView:(UIView *)view; +- (NSUInteger)numberOfTouches; +@end + +@interface ADTapGestureRecognizer : ADGestureRecognizer +@property (nonatomic) NSUInteger numberOfTapsRequired; +@property (nonatomic) NSUInteger numberOfTouchesRequired; +@end + +@interface ADLongPressGestureRecognizer : ADGestureRecognizer +@property (nonatomic) CFTimeInterval minimumPressDuration; +@property (nonatomic) CGFloat allowableMovement; +@property (nonatomic) NSUInteger numberOfTapsRequired; +@property (nonatomic) NSUInteger numberOfTouchesRequired; +@end + +@interface ADPanGestureRecognizer : ADGestureRecognizer +@property (nonatomic) NSUInteger minimumNumberOfTouches; +@property (nonatomic) NSUInteger maximumNumberOfTouches; +- (CGPoint)translationInView:(UIView *)view; +- (void)setTranslation:(CGPoint)t inView:(UIView *)view; +- (CGPoint)velocityInView:(UIView *)view; +@end + +#ifndef UIGestureRecognizer +#define UIGestureRecognizer ADGestureRecognizer +#define UITapGestureRecognizer ADTapGestureRecognizer +#define UILongPressGestureRecognizer ADLongPressGestureRecognizer +#define UIPanGestureRecognizer ADPanGestureRecognizer +#endif + +// ---- NSBlockOperation: iOS 4.0+ (true 3.x has NSOperation/NSOperationQueue +// but not the block convenience subclass). AppDrop's IconLoader builds its +// icon-fetch work as +[NSBlockOperation blockOperationWithBlock:], which runs +// the instant the first uncached tile appears — right after the catalog grid +// builds. On 3.1 that selector is unrecognized → NSInvalidArgumentException. +// Provide a real NSOperation subclass that runs one block in -main, and +// macro-rewrite the token so call sites are unchanged (same strategy as the +// gesture recognizers above). Implementation in AppDropBlockOp.m. +@interface ADBlockOperation : NSOperation ++ (instancetype)blockOperationWithBlock:(void (^)(void))block; +- (void)addExecutionBlock:(void (^)(void))block; +@end + +#ifndef NSBlockOperation +#define NSBlockOperation ADBlockOperation +#endif + +// ---- NSCache: iOS 4.0+ (true 3.x has no NSCache). The class symbol is weak- +// imported from the 5.1 SDK, so on a real iOS 3.1.3 device it binds to NULL +// and `[[NSCache alloc] init]` returns nil — AppDrop's IconLoader then has a +// dead RAM tier: every -objectForKey: misses and every +// -setObject:forKey:cost: is a no-op, so icons re-decode from disk on every +// scroll (visible stutter, and icons that look like they never settle next +// to the iOS 6 build). ADCache (AppDropCache.m) is a complete NSCache work- +// alike built only on iOS-2-era Foundation (NSMutableDictionary + +// @synchronized) with LRU eviction honouring countLimit + totalCostLimit. +// The macro rewrites every NSCache token in the AppDrop sources to ADCache, +// so there are zero call-site edits — same strategy as ADBezierPath / +// ADBlockOperation / the gesture recognizers. AppDropCache.m #undefs this +// so its @implementation keeps the real ADCache name. ---- +@interface ADCache : NSObject +@property (copy) NSString *name; +@property NSUInteger countLimit; +@property NSUInteger totalCostLimit; +@property (assign) id delegate; +@property BOOL evictsObjectsWithDiscardedContent; +- (id)objectForKey:(id)key; +- (void)setObject:(id)obj forKey:(id)key; +- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)cost; +- (void)removeObjectForKey:(id)key; +- (void)removeAllObjects; +@end + +#ifndef NSCache +#define NSCache ADCache +#endif + +// ---- UIInterfaceOrientationMask: iOS 6 NS_OPTIONS ---- +#ifndef UIInterfaceOrientationMaskPortrait +typedef NSUInteger UIInterfaceOrientationMask; +enum { + UIInterfaceOrientationMaskPortrait = (1 << UIInterfaceOrientationPortrait), + UIInterfaceOrientationMaskLandscapeLeft = (1 << UIInterfaceOrientationLandscapeLeft), + UIInterfaceOrientationMaskLandscapeRight = (1 << UIInterfaceOrientationLandscapeRight), + UIInterfaceOrientationMaskPortraitUpsideDown = (1 << UIInterfaceOrientationPortraitUpsideDown), + UIInterfaceOrientationMaskLandscape = (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight), + UIInterfaceOrientationMaskAll = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown), + UIInterfaceOrientationMaskAllButUpsideDown = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight), +}; +#endif + +// ---- UIViewController iOS 6+ autorotation selectors (absent from 5.1 SDK headers) ---- +@interface UIViewController (AppDropAutorotate) +- (NSUInteger)supportedInterfaceOrientations; +- (BOOL)shouldAutorotate; +@end + +// ---- presentViewController:animated:completion: / dismissViewControllerAnimated:completion: +// are iOS 5.0. The 5.1 SDK DECLARES them (so call sites compile), but on a +// real iOS 3.x device the selectors don't exist on UIViewController — only +// -presentModalViewController:animated: and +// -dismissModalViewControllerAnimated:. AppDrop's modals (settings, file +// picker, feedback, revival list) call the iOS 5 form, so the moment a +// modal opens 3.1 throws "unrecognized selector" → NSInvalidArgumentException. +// AppDropRuntime.m installs these two selectors at +load (only when absent, +// i.e. on 3.x), bridging to the iOS 3 modal API and invoking the completion +// block after the (un)present. No header declaration is needed here. ---- + +// ---- UIView -tintColor (iOS 7+; runtime-guarded with respondsToSelector) ---- +@interface UIView (AppDropTint) +- (void)setTintColor:(UIColor *)color; +- (UIColor *)tintColor; +@end + +// ---- UISwitch -tintColor (iOS 6+; runtime-guarded with respondsToSelector) ---- +@interface UISwitch (AppDropTint) +- (void)setTintColor:(UIColor *)color; +- (UIColor *)tintColor; +@end + +// ---- UITableViewHeaderFooterView (iOS 6+; runtime-guarded with isKindOfClass) ---- +@interface UITableViewHeaderFooterView : UIView +@property (nonatomic, retain) UIView *backgroundView; +@property (nonatomic, readonly, retain) UIView *contentView; +@property (nonatomic, readonly, retain) UILabel *textLabel; +@property (nonatomic, readonly, retain) UILabel *detailTextLabel; +@end + +// ---- @YES / @NO boxed BOOL literals (5.1 SDK defines YES/NO as (BOOL)1/(BOOL)0, +// which breaks @YES/@NO boxing; iOS 6 SDK uses __objc_yes/__objc_no builtins) ---- +#undef YES +#undef NO +#define YES __objc_yes +#define NO __objc_no + +// ---- String drawing attribute keys (UIStringDrawing, iOS 6+) ---- +extern NSString *const NSFontAttributeName; +extern NSString *const NSForegroundColorAttributeName; +extern NSString *const NSParagraphStyleAttributeName; + +// ---- NSArray -firstObject (public since iOS 4; absent from 5.1 SDK headers) ---- +@interface NSArray (AppDropFirstObject) +- (id)firstObject; +@end + +// ---- NSString sizeWithAttributes: / draw*, NSData base64, UIImage imageWithData:scale: ---- +@interface NSString (AppDropTextSize) +- (CGSize)sizeWithAttributes:(NSDictionary *)attrs; +- (void)drawAtPoint:(CGPoint)point withAttributes:(NSDictionary *)attrs; +- (void)drawInRect:(CGRect)rect withAttributes:(NSDictionary *)attrs; +@end +@interface NSData (AppDropBase64) +- (NSString *)base64EncodedStringWithOptions:(NSUInteger)opt; +- (id)initWithBase64EncodedString:(NSString *)str options:(NSUInteger)opt; +@end +@interface UIImage (AppDropScale) ++ (UIImage *)imageWithData:(NSData *)data scale:(CGFloat)scale; +@end + +// ---- NSUUID (iOS 6+); declared so calls compile, provided at runtime via shim ---- +@interface NSUUID : NSObject ++ (instancetype)UUID; +- (NSString *)UUIDString; +@end + +// ---- arc4random_uniform: iOS 4.3+ — libSystem on iOS 3.x exports arc4random() +// but NOT arc4random_uniform(). The 5.1 SDK still DECLARES it in , +// so call sites compile fine, but at launch dyld cannot bind the symbol on a +// real 3.x device and kills the process before any UI appears: +// Dyld Error Message: Symbol not found: _arc4random_uniform +// Expected in: /usr/lib/libSystem.B.dylib → EXC_BREAKPOINT (SIGTRAP) +// (This is what crashed AppDrop "while loading the catalog" — the tile/mosaic +// code reaches for arc4random_uniform on the launch path.) +// +// ad_arc4random_uniform() reimplements the unbiased, modulo-free algorithm +// using only arc4random() (present since iOS 2). The macro rewrites every +// arc4random_uniform token in the AppDrop sources to it, so there are zero +// call-site edits and no phantom libSystem dependency — same strategy as the +// ADBezierPath / gesture / NSBlockOperation shims above. +#include +#include +static inline uint32_t ad_arc4random_uniform(uint32_t upper_bound) { + if (upper_bound < 2) return 0; + // Smallest value r may take so that (r % upper_bound) is uniform: + // 2^32 % upper_bound, computed as (-upper_bound) % upper_bound. + uint32_t min = (uint32_t)(0u - upper_bound) % upper_bound; + uint32_t r; + do { r = arc4random(); } while (r < min); + return r % upper_bound; +} +#define arc4random_uniform ad_arc4random_uniform diff --git a/ios3/compat/AppDropGestures.m b/ios3/compat/AppDropGestures.m new file mode 100644 index 0000000..d999513 --- /dev/null +++ b/ios3/compat/AppDropGestures.m @@ -0,0 +1,768 @@ +// AppDropGestures.m — self-contained UIGestureRecognizer backport for iOS 3.1. +// +// WHY THIS EXISTS +// --------------- +// The whole UIKit gesture-recognizer subsystem — UIGestureRecognizer and its +// concrete subclasses (UITap / UILongPress / UIPanGestureRecognizer), the +// -[UIView addGestureRecognizer:] plumbing, and the touch-routing engine that +// drives them — first shipped in **iOS 3.2**. (Verified against Apple's own +// docs: e.g. -[UILongPressGestureRecognizer minimumPressDuration] is marked +// "iOS 3.2+".) On iOS 3.1.3 the *class symbols* exist enough to alloc/init a +// hollow object, but the property setters and the dispatch engine are dead +// stubs, so the first launch-path call throws: +// +// *** -[UILongPressGestureRecognizer setMinimumPressDuration:]: +// unrecognized selector sent to instance 0x253800 +// *** Terminating app due to uncaught exception 'NSInvalidArgumentException' +// +// (CategoryViewController -viewDidLoad sets minimumPressDuration while building +// the Home tab, so the app aborts at launch before anything is on screen.) +// +// THE FIX (same strategy as ADBezierPath / AppDropJSON) +// ----------------------------------------------------- +// AppDropCompat.h declares complete AD* gesture classes and macro-rewrites +// every `UI*GestureRecognizer` token in the AppDrop sources to the AD* class, +// so there are ZERO call-site edits. This file implements them using only +// iOS-2-era touch APIs that every iOS version has had since 2.0: +// +// * -[UIView touchesBegan:withEvent:] / Moved / Ended / Cancelled +// * -[UIWindow sendEvent:] (the single funnel every touch passes through) +// * UITouch / UIEvent, -locationInView:, -[UIView hitTest:withEvent:] +// +// A UIWindow -sendEvent: swizzle is the dispatch engine: for every touch phase +// it walks the hit-tested view's superview chain, feeds the touch to any AD +// recognizer attached to those views, and lets each recognizer update its +// state / fire its target-action. This reproduces the parts of UIKit's gesture +// engine that AppDrop actually uses (single-finger tap, long-press, pan) and +// nothing it doesn't. +// +// addGestureRecognizer: is ALSO 3.2+, so we install our own on every OS +// (class_addMethod when absent on 3.1; method-swizzle to our store when present +// on 3.2+). Because the app exclusively instantiates AD* recognizers (via the +// macro), routing all addGestureRecognizer: calls through our store is correct +// and identical on iOS 3.1 → 10 — UIKit's native engine is simply unused. +// +// MRC throughout (the whole project is -fno-objc-arc): retain/release by hand. + +#import +#import +#import +#import +#import + +// Keep the real AD* class names in this file (undo the compat.h rewrite). +#undef UIGestureRecognizer +#undef UITapGestureRecognizer +#undef UILongPressGestureRecognizer +#undef UIPanGestureRecognizer + +// AppDropCompat.h (force-included) already declares the public AD* @interfaces +// and the base class's @protected ivars, and the iOS 5.1 SDK already defines +// UIGestureRecognizerState. Here we only add the file-private engine API via a +// class extension; subclass-specific ivars live in each @implementation's +// braces (the same pattern AppDropBezier.m uses for ADBezierPath). + +#pragma mark - Base recognizer (private engine API) + +@interface ADGestureRecognizer () +- (void)setView:(UIView *)view; // header exposes -view readonly +- (BOOL)adShouldReceiveTouch:(UITouch *)touch; +- (void)adReset; +- (void)adFireIfNeeded; +- (void)adSetState:(UIGestureRecognizerState)s; +- (void)adTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)adTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)adTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)adTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; +@end + +@implementation ADGestureRecognizer + +- (id)initWithTarget:(id)target action:(SEL)action { + if ((self = [super init])) { + _state = UIGestureRecognizerStatePossible; + _enabled = YES; + _targets = [[NSMutableArray alloc] init]; + if (target && action) [self addTarget:target action:action]; + } + return self; +} + +- (void)dealloc { + [_targets release]; + [super dealloc]; +} + +- (void)addTarget:(id)target action:(SEL)action { + if (!target || !action) return; + // Store target as a non-retained box (UIKit does not retain gesture targets; + // retaining here would create owner→recognizer→owner cycles). + NSValue *t = [NSValue valueWithNonretainedObject:target]; + NSValue *a = [NSValue valueWithPointer:(void *)action]; + [_targets addObject:[NSArray arrayWithObjects:t, a, nil]]; +} + +- (UIGestureRecognizerState)state { return _state; } +- (id)delegate { return _delegate; } +- (void)setDelegate:(id)d { _delegate = d; } +- (BOOL)enabled { return _enabled; } +- (void)setEnabled:(BOOL)e { _enabled = e; if (!e) [self adReset]; } +- (UIView *)view { return _view; } +- (void)setView:(UIView *)v { _view = v; } // non-retained + +- (NSUInteger)numberOfTouches { return _trackedTouch ? 1 : 0; } + +- (CGPoint)locationInView:(UIView *)view { + UITouch *t = _trackedTouch; + if (!t) return CGPointZero; + return [t locationInView:(view ?: _view)]; +} + +- (void)adSetState:(UIGestureRecognizerState)s { + _state = s; + [self adFireIfNeeded]; +} + +// Fire every registered target-action. UIKit passes the recognizer as the sole +// argument for `action:` selectors that take one parameter (`handleX:`); for a +// zero-arg selector (`tapped`) it sends with no argument. We mirror that by +// inspecting the selector's argument count. +- (void)adFireIfNeeded { + if (_state != UIGestureRecognizerStateBegan && + _state != UIGestureRecognizerStateChanged && + _state != UIGestureRecognizerStateEnded && + _state != UIGestureRecognizerStateRecognized) { + return; + } + // Copy targets first: an action may mutate the view tree / recognizers. + NSArray *snapshot = [[_targets copy] autorelease]; + for (NSArray *pair in snapshot) { + id target = [[pair objectAtIndex:0] nonretainedObjectValue]; + SEL action = (SEL)[[pair objectAtIndex:1] pointerValue]; + if (!target || ![target respondsToSelector:action]) continue; + NSMethodSignature *sig = [target methodSignatureForSelector:action]; + // numberOfArguments includes self + _cmd; >2 means it takes the sender. + if (sig && [sig numberOfArguments] > 2) { + void (*imp)(id, SEL, id) = (void (*)(id, SEL, id))objc_msgSend; + imp(target, action, self); + } else { + void (*imp)(id, SEL) = (void (*)(id, SEL))objc_msgSend; + imp(target, action); + } + } +} + +- (void)adReset { + _state = UIGestureRecognizerStatePossible; + _trackedTouch = nil; +} + +// Default: ask the delegate whether this touch should be received. +- (BOOL)adShouldReceiveTouch:(UITouch *)touch { + if (_delegate && [_delegate respondsToSelector:@selector(gestureRecognizer:shouldReceiveTouch:)]) { + return [_delegate gestureRecognizer:(id)self shouldReceiveTouch:touch]; + } + return YES; +} + +- (void)adTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {} +- (void)adTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {} +- (void)adTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {} +- (void)adTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + [self adReset]; +} +@end + +#pragma mark - Tap + +@implementation ADTapGestureRecognizer { + NSUInteger _numberOfTapsRequired; + NSUInteger _numberOfTouchesRequired; + CGPoint _startPoint; +} +- (id)initWithTarget:(id)target action:(SEL)action { + if ((self = [super initWithTarget:target action:action])) { + _numberOfTapsRequired = 1; + _numberOfTouchesRequired = 1; + } + return self; +} +- (NSUInteger)numberOfTapsRequired { return _numberOfTapsRequired; } +- (void)setNumberOfTapsRequired:(NSUInteger)n { _numberOfTapsRequired = n; } +- (NSUInteger)numberOfTouchesRequired { return _numberOfTouchesRequired; } +- (void)setNumberOfTouchesRequired:(NSUInteger)n { _numberOfTouchesRequired = n; } + +- (void)adTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + if (_trackedTouch) return; + UITouch *t = [touches anyObject]; + if (![self adShouldReceiveTouch:t]) return; + _trackedTouch = t; + _startPoint = [t locationInView:_view]; +} +- (void)adTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + if (!_trackedTouch || ![touches containsObject:_trackedTouch]) return; + CGPoint p = [_trackedTouch locationInView:_view]; + // A tap tolerates a little slop; beyond ~15pt it's a scroll/drag, not a tap. + if (fabsf(p.x - _startPoint.x) > 15 || fabsf(p.y - _startPoint.y) > 15) { + [self adReset]; + } +} +- (void)adTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + if (!_trackedTouch || ![touches containsObject:_trackedTouch]) return; + UITouch *t = _trackedTouch; + NSUInteger taps = 1; + @try { taps = [t tapCount]; } @catch (__unused id e) {} + if (taps >= _numberOfTapsRequired) { + [self adSetState:UIGestureRecognizerStateRecognized]; + } + [self adReset]; +} +@end + +#pragma mark - Long press + + +@implementation ADLongPressGestureRecognizer { + CFTimeInterval _minimumPressDuration; + CGFloat _allowableMovement; + NSUInteger _numberOfTapsRequired; + NSUInteger _numberOfTouchesRequired; + CGPoint _startPoint; + BOOL _began; +} +- (id)initWithTarget:(id)target action:(SEL)action { + if ((self = [super initWithTarget:target action:action])) { + _minimumPressDuration = 0.5; // UIKit default (Apple docs) + _allowableMovement = 10.0; // UIKit default + _numberOfTapsRequired = 0; + _numberOfTouchesRequired = 1; + } + return self; +} +- (CFTimeInterval)minimumPressDuration { return _minimumPressDuration; } +- (void)setMinimumPressDuration:(CFTimeInterval)d { _minimumPressDuration = d; } +- (CGFloat)allowableMovement { return _allowableMovement; } +- (void)setAllowableMovement:(CGFloat)m { _allowableMovement = m; } +- (NSUInteger)numberOfTapsRequired { return _numberOfTapsRequired; } +- (void)setNumberOfTapsRequired:(NSUInteger)n { _numberOfTapsRequired = n; } +- (NSUInteger)numberOfTouchesRequired { return _numberOfTouchesRequired; } +- (void)setNumberOfTouchesRequired:(NSUInteger)n { _numberOfTouchesRequired = n; } + +- (void)adReset { + [super adReset]; + _began = NO; +} + +- (void)adTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + if (_trackedTouch) return; + UITouch *t = [touches anyObject]; + if (![self adShouldReceiveTouch:t]) return; + _trackedTouch = t; + _began = NO; + _startPoint = [t locationInView:_view]; + // Fire the "Began" transition after minimumPressDuration unless the finger + // lifted or moved too far. performSelector:afterDelay: schedules on the + // current run loop (main), matching UIKit's main-thread delivery. + [self performSelector:@selector(adLongPressElapsed) + withObject:nil + afterDelay:_minimumPressDuration]; +} + +- (void)adLongPressElapsed { + if (!_trackedTouch || _began) return; + _began = YES; + [self adSetState:UIGestureRecognizerStateBegan]; +} + +- (void)adTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + if (!_trackedTouch || ![touches containsObject:_trackedTouch]) return; + CGPoint p = [_trackedTouch locationInView:_view]; + if (!_began) { + // Too much movement before the press registers → not a long press. + if (fabsf(p.x - _startPoint.x) > _allowableMovement || + fabsf(p.y - _startPoint.y) > _allowableMovement) { + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(adLongPressElapsed) + object:nil]; + [self adReset]; + } + } else { + [self adSetState:UIGestureRecognizerStateChanged]; + } +} + +- (void)adTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + if (!_trackedTouch || ![touches containsObject:_trackedTouch]) return; + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(adLongPressElapsed) + object:nil]; + if (_began) { + [self adSetState:UIGestureRecognizerStateEnded]; + } + [self adReset]; +} + +- (void)adTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(adLongPressElapsed) + object:nil]; + if (_began) { [self adSetState:UIGestureRecognizerStateCancelled]; } + [self adReset]; +} +@end + +#pragma mark - Pan + + +@implementation ADPanGestureRecognizer { + NSUInteger _minimumNumberOfTouches; + NSUInteger _maximumNumberOfTouches; + CGPoint _startPoint; // in _view coords + CGPoint _lastPoint; + CFTimeInterval _lastTime; + CGPoint _velocity; + CGPoint _translationOffset; // adjustment from setTranslation:inView: + BOOL _began; +} +- (id)initWithTarget:(id)target action:(SEL)action { + if ((self = [super initWithTarget:target action:action])) { + _minimumNumberOfTouches = 1; + _maximumNumberOfTouches = NSUIntegerMax; + } + return self; +} +- (NSUInteger)minimumNumberOfTouches { return _minimumNumberOfTouches; } +- (void)setMinimumNumberOfTouches:(NSUInteger)n { _minimumNumberOfTouches = n; } +- (NSUInteger)maximumNumberOfTouches { return _maximumNumberOfTouches; } +- (void)setMaximumNumberOfTouches:(NSUInteger)n { _maximumNumberOfTouches = n; } + +- (void)adReset { + [super adReset]; + _began = NO; + _translationOffset = CGPointZero; + _velocity = CGPointZero; +} + +- (CGPoint)translationInView:(UIView *)view { + if (!_trackedTouch) return _translationOffset; + CGPoint p = [_trackedTouch locationInView:(view ?: _view)]; + return CGPointMake(p.x - _startPoint.x + _translationOffset.x, + p.y - _startPoint.y + _translationOffset.y); +} +- (void)setTranslation:(CGPoint)t inView:(UIView *)view { + if (_trackedTouch) { + CGPoint p = [_trackedTouch locationInView:(view ?: _view)]; + _startPoint = p; + } + _translationOffset = t; +} +- (CGPoint)velocityInView:(UIView *)view { return _velocity; } + +- (void)adTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + if (_trackedTouch) return; + UITouch *t = [touches anyObject]; + if (![self adShouldReceiveTouch:t]) return; + _trackedTouch = t; + _began = NO; + _startPoint = [t locationInView:_view]; + _lastPoint = _startPoint; + _lastTime = CACurrentMediaTime(); + _translationOffset = CGPointZero; + _velocity = CGPointZero; +} +- (void)adTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + if (!_trackedTouch || ![touches containsObject:_trackedTouch]) return; + CGPoint p = [_trackedTouch locationInView:_view]; + CFTimeInterval now = CACurrentMediaTime(); + CFTimeInterval dt = now - _lastTime; + if (dt > 0) { + _velocity = CGPointMake((p.x - _lastPoint.x) / dt, (p.y - _lastPoint.y) / dt); + } + _lastPoint = p; _lastTime = now; + if (!_began) { + if (fabsf(p.x - _startPoint.x) > 4 || fabsf(p.y - _startPoint.y) > 4) { + _began = YES; + [self adSetState:UIGestureRecognizerStateBegan]; + } + } else { + [self adSetState:UIGestureRecognizerStateChanged]; + } +} +- (void)adTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + if (!_trackedTouch || ![touches containsObject:_trackedTouch]) return; + if (_began) { [self adSetState:UIGestureRecognizerStateEnded]; } + [self adReset]; +} +- (void)adTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + if (_began) { [self adSetState:UIGestureRecognizerStateCancelled]; } + [self adReset]; +} +@end + +#pragma mark - Attachment store on UIView (addGestureRecognizer:) + +static char kADRecognizersKey; + +static NSMutableArray *ADViewRecognizers(UIView *view, BOOL create) { + NSMutableArray *arr = objc_getAssociatedObject(view, &kADRecognizersKey); + if (!arr && create) { + arr = [NSMutableArray array]; + objc_setAssociatedObject(view, &kADRecognizersKey, arr, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return arr; +} + +static BOOL ADViewHasControlAncestor(UIView *view) { + while (view) { + if ([view isKindOfClass:[UIControl class]]) return YES; + view = view.superview; + } + return NO; +} + +static UIScrollView *ADNearestScrollView(UIView *view) { + while (view) { + if ([view isKindOfClass:[UIScrollView class]]) return (UIScrollView *)view; + view = view.superview; + } + return nil; +} + +static UITouch *gADScrollTouch = nil; +static UIScrollView *gADScrollView = nil; +static CGPoint gADScrollStartPoint = { 0, 0 }; +static CGPoint gADScrollStartOffset = { 0, 0 }; +static CGPoint gADScrollLastPoint = { 0, 0 }; +static CFTimeInterval gADScrollLastTime = 0; +static CGFloat gADScrollVelocityY = 0; +static BOOL gADScrollEngaged = NO; +static BOOL gADScrollDidWriteOffset = NO; +static BOOL gADScrollMomentumActive = NO; +static NSTimer *gADScrollMomentumTimer = nil; +static id gADScrollMomentumDriver = nil; + +static BOOL ADTouchLooksUsable(id touch) { + return touch && [touch respondsToSelector:@selector(phase)] && [touch respondsToSelector:@selector(view)]; +} + +static BOOL ADScrollTouchIsActive(UITouch *touch) { + if (!ADTouchLooksUsable(touch)) return NO; + switch ([touch phase]) { + case UITouchPhaseBegan: + case UITouchPhaseMoved: + case UITouchPhaseStationary: + return YES; + default: + return NO; + } +} + +static CGFloat ADScrollClampedY(UIScrollView *sv, CGFloat proposedY) { + UIEdgeInsets inset = sv.contentInset; + CGFloat minY = -inset.top; + CGFloat maxY = sv.contentSize.height + inset.bottom - sv.bounds.size.height; + if (maxY < minY) maxY = minY; + if (proposedY < minY) return minY; + if (proposedY > maxY) return maxY; + return proposedY; +} + +static void ADScrollStopMomentum(void) { + [gADScrollMomentumTimer invalidate]; + gADScrollMomentumTimer = nil; +} + +static void ADScrollReset(void) { + ADScrollStopMomentum(); + if (gADScrollView) { + [gADScrollView release]; + gADScrollView = nil; + } + gADScrollTouch = nil; + gADScrollStartPoint = CGPointZero; + gADScrollStartOffset = CGPointZero; + gADScrollLastPoint = CGPointZero; + gADScrollLastTime = 0; + gADScrollVelocityY = 0; + gADScrollEngaged = NO; + gADScrollDidWriteOffset = NO; + gADScrollMomentumActive = NO; +} + +@interface ADScrollMomentumDriver : NSObject +- (void)adScrollMomentumTick:(NSTimer *)timer; +@end + +@implementation ADScrollMomentumDriver +- (void)adScrollMomentumTick:(NSTimer *)timer { + (void)timer; + UIScrollView *sv = gADScrollView; + if (!sv || !gADScrollMomentumActive || !gADScrollDidWriteOffset) { + ADScrollReset(); + return; + } + if (!sv.window) { + ADScrollReset(); + return; + } + CGFloat speed = fabsf(gADScrollVelocityY); + if (speed < 18.0f) { + ADScrollReset(); + return; + } + CGFloat currentY = sv.contentOffset.y; + CGFloat nextY = ADScrollClampedY(sv, currentY - (gADScrollVelocityY / 60.0f)); + if (fabsf(nextY - currentY) < 0.05f) { + gADScrollVelocityY *= 0.5f; + } else { + [sv setContentOffset:CGPointMake(sv.contentOffset.x, nextY) animated:NO]; + gADScrollVelocityY *= 0.94f; + } + if (fabsf(gADScrollVelocityY) < 18.0f) { + ADScrollReset(); + } +} +@end + +static void ADScrollBeginIfNeeded(UITouch *touch, UIView *hitView) { + if (!ADTouchLooksUsable(touch) || !hitView) return; + if (ADViewHasControlAncestor(hitView)) return; + if (gADScrollTouch && !ADScrollTouchIsActive(gADScrollTouch)) { + ADScrollReset(); + } + if (gADScrollTouch && touch != gADScrollTouch) return; + UIScrollView *sv = ADNearestScrollView(hitView); + if (!sv || !sv.scrollEnabled) return; + if (!gADScrollTouch) { + // A fresh touch always stops any in-flight momentum scroll (and releases + // the previously retained scroll view) — classic touch-to-stop. + ADScrollReset(); + gADScrollTouch = touch; + gADScrollView = [sv retain]; + // Window coordinates: deltas must NOT be measured in the scroll view's own + // coordinate space, because our setContentOffset: changes that space and + // would feed back into the delta (scroll would run at half speed). + gADScrollStartPoint = [touch locationInView:sv.window]; + gADScrollStartOffset = sv.contentOffset; + gADScrollLastPoint = gADScrollStartPoint; + gADScrollLastTime = CACurrentMediaTime(); + gADScrollVelocityY = 0; + gADScrollEngaged = NO; + gADScrollDidWriteOffset = NO; + } +} + +static void ADScrollUpdateIfNeeded(UITouch *touch) { + if (!ADTouchLooksUsable(touch) || !gADScrollTouch || touch != gADScrollTouch || !gADScrollView) return; + if (!ADScrollTouchIsActive(touch)) return; + CGPoint p = [touch locationInView:gADScrollView.window]; + CFTimeInterval now = CACurrentMediaTime(); + CFTimeInterval dt = now - gADScrollLastTime; + CGFloat stepY = p.y - gADScrollLastPoint.y; + if (dt > 0) { + CGFloat frameVelocityY = stepY / dt; + gADScrollVelocityY = (gADScrollVelocityY * 0.35f) + (frameVelocityY * 0.65f); + } + gADScrollLastPoint = p; + gADScrollLastTime = now; + + CGFloat dx = p.x - gADScrollStartPoint.x; + CGFloat dy = p.y - gADScrollStartPoint.y; + if (!gADScrollEngaged) { + if (fabsf(dx) < 4.0f && fabsf(dy) < 4.0f) return; + if (fabsf(dy) < fabsf(dx)) return; + gADScrollEngaged = YES; + } + if ([gADScrollView respondsToSelector:@selector(isDragging)] && [gADScrollView isDragging]) return; + // INCREMENTAL scrolling: move relative to the CURRENT contentOffset by this + // frame's finger delta, instead of "startOffset - totalDelta". The absolute + // formula snaps the content back whenever the offset changed under us mid- + // drag — UIKit's own tracking taking a few frames, a table reloadData from + // infinite-scroll loadMore, contentSize growth, etc. — which is exactly the + // visible "scroll suddenly jumps" bug. Per-frame deltas are immune: each + // frame only ever moves by what the finger moved since the previous frame. + CGFloat ny = ADScrollClampedY(gADScrollView, gADScrollView.contentOffset.y - stepY); + if (fabsf(ny - gADScrollView.contentOffset.y) > 0.05f) { + [gADScrollView setContentOffset:CGPointMake(gADScrollView.contentOffset.x, ny) animated:NO]; + gADScrollDidWriteOffset = YES; + } +} + +static void ADScrollFinishIfNeeded(UITouch *touch) { + if (!ADTouchLooksUsable(touch) || !gADScrollTouch || touch != gADScrollTouch) return; + gADScrollTouch = nil; + gADScrollMomentumActive = YES; + gADScrollEngaged = NO; + gADScrollStartPoint = CGPointZero; + gADScrollLastPoint = CGPointZero; + gADScrollLastTime = 0; + if (!gADScrollView) return; + if (![gADScrollView respondsToSelector:@selector(isDragging)] || [gADScrollView isDragging]) { + ADScrollReset(); + return; + } + if (!gADScrollDidWriteOffset || fabsf(gADScrollVelocityY) < 18.0f) { + ADScrollReset(); + return; + } + if (!gADScrollMomentumDriver) { + gADScrollMomentumDriver = [[ADScrollMomentumDriver alloc] init]; + } + ADScrollStopMomentum(); + gADScrollMomentumTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0) + target:gADScrollMomentumDriver + selector:@selector(adScrollMomentumTick:) + userInfo:nil + repeats:YES]; +} + +// Our addGestureRecognizer: — stores the AD recognizer on the view (retained by +// the associated array, matching UIKit's "view owns its recognizers"), sets the +// recognizer's non-retained back-pointer, and ensures touch handling is on. +static void ADView_addGestureRecognizer(id self, SEL _cmd, id gr) { + if (![gr isKindOfClass:[ADGestureRecognizer class]]) return; + UIView *view = (UIView *)self; + NSMutableArray *arr = ADViewRecognizers(view, YES); + if (![arr containsObject:gr]) [arr addObject:gr]; + [(ADGestureRecognizer *)gr setView:view]; + view.userInteractionEnabled = YES; +} + +static void ADView_removeGestureRecognizer(id self, SEL _cmd, id gr) { + NSMutableArray *arr = ADViewRecognizers((UIView *)self, NO); + if (arr) [arr removeObject:gr]; + if ([gr isKindOfClass:[ADGestureRecognizer class]]) [(ADGestureRecognizer *)gr setView:nil]; +} + +#pragma mark - Dispatch engine: UIWindow -sendEvent: swizzle + +// Walk the hit view's superview chain and deliver `touches` (phase `phase`) to +// every AD recognizer attached to those views. This is the heart of the engine. +static void ADDeliver(UIView *startView, NSSet *touches, UIEvent *event, NSInteger phase) { + UIView *v = startView; + while (v) { + NSMutableArray *recs = ADViewRecognizers(v, NO); + if (recs.count) { + // Copy: a recognizer's action can mutate the tree mid-iteration. + NSArray *snap = [[recs copy] autorelease]; + for (ADGestureRecognizer *r in snap) { + if (!r.enabled) continue; + switch (phase) { + case 0: [r adTouchesBegan:touches withEvent:event]; break; + case 1: [r adTouchesMoved:touches withEvent:event]; break; + case 2: [r adTouchesEnded:touches withEvent:event]; break; + default: [r adTouchesCancelled:touches withEvent:event]; break; + } + } + } + v = v.superview; + } +} + +static IMP gOrigSendEvent = NULL; + +static void ADWindow_sendEvent(id self, SEL _cmd, UIEvent *event) { + if (!event || !gOrigSendEvent) return; + NSSet *touches = nil; + UIView *deliverFrom = nil; + NSMutableSet *began = nil, *moved = nil, *ended = nil, *cancelled = nil; + + @try { + touches = [event allTouches]; + if (touches) { + // Group touches by phase, but only after we prove the object is a + // real touch. Old UIKit code can hand us unexpected private cluster + // objects, and we must never message `phase` to a dictionary. + for (id rawTouch in touches) { + if (!ADTouchLooksUsable(rawTouch)) continue; + UITouch *t = (UITouch *)rawTouch; + if (!deliverFrom) { + deliverFrom = [t view] ?: self; + } + UITouchPhase ph = [t phase]; + NSMutableSet **bucket = + (ph == UITouchPhaseBegan) ? &began : + (ph == UITouchPhaseMoved) ? &moved : + (ph == UITouchPhaseStationary)? &moved : + (ph == UITouchPhaseEnded) ? &ended : &cancelled; + if (!*bucket) *bucket = [NSMutableSet set]; + [*bucket addObject:t]; + } + } + } @catch (id e) { + // Never let gesture bookkeeping take down event delivery. + } + + // First let UIKit process the touch normally so scroll views, tables and + // controls get the earliest possible chance to begin tracking a drag. + ((void (*)(id, SEL, UIEvent *))gOrigSendEvent)(self, _cmd, event); + + // Then feed the same touch stream to AppDrop's backported recognizers. + // Keeping this after UIKit is what makes the custom gesture layer coexist + // with scrolling instead of fighting it. + @try { + if (deliverFrom) { + if (began) { + for (UITouch *t in began) ADScrollBeginIfNeeded(t, deliverFrom); + ADDeliver(deliverFrom, began, event, 0); + } + if (moved) { + for (UITouch *t in moved) ADScrollUpdateIfNeeded(t); + ADDeliver(deliverFrom, moved, event, 1); + } + if (ended) { + for (UITouch *t in ended) ADScrollFinishIfNeeded(t); + ADDeliver(deliverFrom, ended, event, 2); + } + if (cancelled) { + for (UITouch *t in cancelled) ADScrollFinishIfNeeded(t); + ADDeliver(deliverFrom, cancelled, event, 3); + } + } + } @catch (id e) { + // Never let gesture dispatch take down event delivery. + } +} + +#pragma mark - Install + +@interface ADGestureRecognizer (Install) +@end + +@implementation ADGestureRecognizer (Install) ++ (void)load { + @autoreleasepool { + // 1) addGestureRecognizer: / removeGestureRecognizer: on UIView. + // iOS 3.1: absent → class_addMethod installs ours. + // iOS 3.2+: present → method_setImplementation routes to our store + // (the app uses AD* recognizers exclusively, so UIKit's + // native engine is intentionally bypassed). + Class viewCls = [UIView class]; + SEL addSel = @selector(addGestureRecognizer:); + SEL remSel = @selector(removeGestureRecognizer:); + Method addM = class_getInstanceMethod(viewCls, addSel); + if (addM) { + method_setImplementation(addM, (IMP)ADView_addGestureRecognizer); + } else { + class_addMethod(viewCls, addSel, (IMP)ADView_addGestureRecognizer, "v@:@"); + } + Method remM = class_getInstanceMethod(viewCls, remSel); + if (remM) { + method_setImplementation(remM, (IMP)ADView_removeGestureRecognizer); + } else { + class_addMethod(viewCls, remSel, (IMP)ADView_removeGestureRecognizer, "v@:@"); + } + + // 2) Swizzle UIWindow -sendEvent: to run our dispatch engine on every + // touch, then forward to the original. + Class winCls = [UIWindow class]; + SEL seSel = @selector(sendEvent:); + Method seM = class_getInstanceMethod(winCls, seSel); + if (seM) { + gOrigSendEvent = method_getImplementation(seM); + method_setImplementation(seM, (IMP)ADWindow_sendEvent); + } + } +} +@end diff --git a/ios3/compat/AppDropJSON.m b/ios3/compat/AppDropJSON.m new file mode 100644 index 0000000..b26489b --- /dev/null +++ b/ios3/compat/AppDropJSON.m @@ -0,0 +1,113 @@ +// AppDropJSON.m — NSJSONSerialization backfill for iOS 3.x / 4.x (the class +// first appeared in iOS 5). cJSON-backed. Installed as a class pair at load so +// existing call sites ([NSJSONSerialization JSONObjectWithData:options:error:] +// and dataWithJSONObject:options:error:) work unchanged. No-op on iOS 5+. +// +// Build this file WITHOUT ARC (runtime plumbing). + +#import +#import +#import "cJSON.h" + +#pragma mark - cJSON -> Foundation + +static id AppDropJSONToFoundation(cJSON *node) { + if (!node) return [NSNull null]; + if (cJSON_IsNull(node)) return [NSNull null]; + if (cJSON_IsTrue(node)) return [NSNumber numberWithBool:YES]; + if (cJSON_IsFalse(node)) return [NSNumber numberWithBool:NO]; + if (cJSON_IsNumber(node)) { + double d = node->valuedouble; + if (d == (double)node->valueint && d == (double)(long long)d) + return [NSNumber numberWithLongLong:(long long)d]; + return [NSNumber numberWithDouble:d]; + } + if (cJSON_IsString(node)) { + const char *s = node->valuestring ? node->valuestring : ""; + return [NSString stringWithUTF8String:s] ?: @""; + } + if (cJSON_IsArray(node)) { + NSMutableArray *arr = [NSMutableArray array]; + for (cJSON *c = node->child; c; c = c->next) + [arr addObject:AppDropJSONToFoundation(c)]; + return arr; + } + if (cJSON_IsObject(node)) { + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + for (cJSON *c = node->child; c; c = c->next) { + NSString *key = c->string ? [NSString stringWithUTF8String:c->string] : nil; + if (key) [dict setObject:AppDropJSONToFoundation(c) forKey:key]; + } + return dict; + } + return [NSNull null]; +} + +#pragma mark - Foundation -> cJSON + +static cJSON *AppDropFoundationToJSON(id obj) { + if (!obj || obj == [NSNull null]) return cJSON_CreateNull(); + if ([obj isKindOfClass:[NSString class]]) return cJSON_CreateString([obj UTF8String]); + if ([obj isKindOfClass:[NSNumber class]]) { + NSNumber *n = obj; + const char *t = [n objCType]; + if (t && (t[0] == 'c' || t[0] == 'B')) { + // bool vs char heuristic: CFBoolean reports 'c' + if (n == (id)kCFBooleanTrue || n == (id)kCFBooleanFalse) + return cJSON_CreateBool([n boolValue]); + } + return cJSON_CreateNumber([n doubleValue]); + } + if ([obj isKindOfClass:[NSArray class]]) { + cJSON *arr = cJSON_CreateArray(); + for (id e in obj) cJSON_AddItemToArray(arr, AppDropFoundationToJSON(e)); + return arr; + } + if ([obj isKindOfClass:[NSDictionary class]]) { + cJSON *o = cJSON_CreateObject(); + for (id k in obj) cJSON_AddItemToObjectCS(o, [[k description] UTF8String], AppDropFoundationToJSON([obj objectForKey:k])); + return o; + } + return cJSON_CreateNull(); +} + +#pragma mark - Class-method IMPs + +static id AppDropJSONObjectWithData(id cls, SEL _cmd, NSData *data, NSUInteger opt, NSError **err) { + if (err) *err = nil; + if (![data length]) return nil; + cJSON *root = cJSON_ParseWithLength((const char *)[data bytes], [data length]); + if (!root) { + if (err) *err = [NSError errorWithDomain:@"AppDropJSON" code:3840 userInfo:nil]; + return nil; + } + id result = AppDropJSONToFoundation(root); + cJSON_Delete(root); + return result; +} + +static NSData *AppDropDataWithJSONObject(id cls, SEL _cmd, id object, NSUInteger opt, NSError **err) { + if (err) *err = nil; + cJSON *root = AppDropFoundationToJSON(object); + char *txt = (opt & 1 /*NSJSONWritingPrettyPrinted*/) ? cJSON_Print(root) : cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (!txt) return nil; + NSData *d = [NSData dataWithBytes:txt length:strlen(txt)]; + free(txt); + return d; +} + +static BOOL AppDropIsValidJSONObject(id cls, SEL _cmd, id obj) { + return [obj isKindOfClass:[NSArray class]] || [obj isKindOfClass:[NSDictionary class]]; +} + +__attribute__((constructor)) +static void AppDropInstallNSJSONSerialization(void) { + if (objc_getClass("NSJSONSerialization")) return; + Class c = objc_allocateClassPair([NSObject class], "NSJSONSerialization", 0); + Class meta = object_getClass(c); + class_addMethod(meta, @selector(JSONObjectWithData:options:error:), (IMP)AppDropJSONObjectWithData, "@@:@L^@"); + class_addMethod(meta, @selector(dataWithJSONObject:options:error:), (IMP)AppDropDataWithJSONObject, "@@:@L^@"); + class_addMethod(meta, @selector(isValidJSONObject:), (IMP)AppDropIsValidJSONObject, "c@:@"); + objc_registerClassPair(c); +} diff --git a/ios3/compat/AppDropRuntime.m b/ios3/compat/AppDropRuntime.m new file mode 100644 index 0000000..b6f1c92 --- /dev/null +++ b/ios3/compat/AppDropRuntime.m @@ -0,0 +1,1465 @@ +// AppDropRuntime.m — runtime backfill for APIs missing on iOS 3.x / 4.x. +// +// Mirrors the existing IOS5Compat.m approach (install IMPs via +load), extended +// down to the iOS 3/4 baseline. Everything here is a no-op when the running OS +// already provides the method, so the same binary still behaves natively on +// iOS 5–10. +// +// IMPORTANT (iOS 3 dyld): the IMPs are installed as plain static C functions, +// NOT via imp_implementationWithBlock(). imp_implementationWithBlock first +// shipped in iOS 4.3's libobjc; on iOS 3.1.3 it is absent and dyld aborts the +// process at launch with: +// Symbol not found: _imp_implementationWithBlock +// Expected in: /usr/lib/libobjc.A.dylib +// class_addMethod() takes a raw IMP (function pointer) directly — its signature +// is (id self, SEL _cmd, ...), which the type-encoding strings below already +// describe — so no block trampoline (and no missing runtime symbol) is needed. +// +// Covers: +// * NSArray -firstObject (iOS 4 API, missing pre-4) +// * NSData base64 (encode/decode) (iOS 7 API) +// * NSString sizeWithAttributes: + drawAtPoint:/drawInRect:withAttributes: +// (iOS 7 NSStringDrawing; bridged to the +// iOS 2-era UIStringDrawing size/draw API) +// * UIImage +imageWithData:scale: (iOS 6 API) +// * +[UIView animateWithDuration:...] (iOS 4.0 block animations; all three +// variants — bridged to the iOS-2 +// begin/commit API, completion fired via +// animationDidStop:finished:context:) +// * NSUUID (iOS 6 class; CFUUID-backed) +// * NSJSONSerialization (iOS 5 class; cJSON-backed) — see +// AppDropJSON.m, installed the same way. +// * UIScreen -scale (iOS 4.0; armv6 devices are all 1x) +// * CALayer -contentsScale/-set… (iOS 4.0; 1x no-op on iOS 3) +// * UIGraphicsBeginImageContextWithOptions +// (iOS 4.0 C function; RTLD_NEXT to the +// real UIKit impl on 4+, 1x fallback on 3) +// * Modern subscripting (dict[key] / arr[i] read + write; the +// iOS 6 SDK syntax sends objectFor- +// KeyedSubscript: etc., absent pre-iOS 6. +// Ported here from the build-excluded +// IOS5Compat.m so it covers every site.) +// * +[NSPropertyListSerialization propertyListWithData:options:format:error:] +// (iOS 4.0 class method; absent on iOS +// 3.1.3 — sending it aborts the app the +// instant a downloaded IPA's Info.plist +// is parsed, IPAPackage.m. Bridged to the +// iOS-2 +propertyListFromData:mutability- +// Option:format:errorDescription:, which +// reads both binary and XML plists.) + +#import +#import +#import +#import +#include +#import "AppDropBlocks.h" // _Block_copy / _Block_release + ADBlockBox (present/dismiss completion) + +#pragma mark - Modern subscripting (dict[key], arr[i] — iOS 6 SDK syntax) +// clang lowers `dict[key]` to -[NSDictionary objectForKeyedSubscript:], +// `arr[i]` to -[NSArray objectAtIndexedSubscript:], and the assignment forms +// to the matching setters. Those selectors first shipped in iOS 6; on iOS 3 +// they are unrecognized and throw NSInvalidArgumentException (the launch crash +// we hit in setupAppearance: -[NSCFDictionary objectForKeyedSubscript:]). +// +// The IMPs delegate through the iOS-2-era selector (-objectForKey:, +// -objectAtIndex:, …) so the runtime dispatches to the CONCRETE class's real +// method (__NSCFDictionary), not the abstract-class stub. No-ops on iOS 6+ +// where the OS already provides the subscript methods. + +static id AppDropDictObjectForKeyedSubscript(id self, SEL _cmd, id key) { + return [self objectForKey:key]; +} +static void AppDropMDictSetObjectForKeyedSubscript(id self, SEL _cmd, id obj, id key) { + [(NSMutableDictionary *)self setObject:obj forKey:key]; +} +static id AppDropArrObjectAtIndexedSubscript(id self, SEL _cmd, NSUInteger idx) { + return [self objectAtIndex:idx]; +} +static void AppDropMArrSetObjectAtIndexedSubscript(id self, SEL _cmd, id obj, NSUInteger idx) { + NSMutableArray *arr = (NSMutableArray *)self; + if (idx == [arr count]) { // assigning past the end appends (Apple semantics) + [arr addObject:obj]; + } else { + [arr replaceObjectAtIndex:idx withObject:obj]; + } +} + +@implementation NSDictionary (AppDropSubscriptImpl) ++ (void)load { + if ([NSDictionary instancesRespondToSelector:@selector(objectForKeyedSubscript:)]) return; + class_addMethod([NSDictionary class], @selector(objectForKeyedSubscript:), + (IMP)AppDropDictObjectForKeyedSubscript, "@@:@"); +} +@end + +@implementation NSMutableDictionary (AppDropSubscriptImpl) ++ (void)load { + if ([NSMutableDictionary instancesRespondToSelector:@selector(setObject:forKeyedSubscript:)]) return; + class_addMethod([NSMutableDictionary class], @selector(setObject:forKeyedSubscript:), + (IMP)AppDropMDictSetObjectForKeyedSubscript, "v@:@@"); +} +@end + +@implementation NSArray (AppDropSubscriptImpl) ++ (void)load { + if ([NSArray instancesRespondToSelector:@selector(objectAtIndexedSubscript:)]) return; + class_addMethod([NSArray class], @selector(objectAtIndexedSubscript:), + (IMP)AppDropArrObjectAtIndexedSubscript, "@@:L"); +} +@end + +@implementation NSMutableArray (AppDropSubscriptImpl) ++ (void)load { + if ([NSMutableArray instancesRespondToSelector:@selector(setObject:atIndexedSubscript:)]) return; + class_addMethod([NSMutableArray class], @selector(setObject:atIndexedSubscript:), + (IMP)AppDropMArrSetObjectAtIndexedSubscript, "v@:@L"); +} +@end + +#pragma mark - Attribute key constants (weak: real ones win on iOS 6+) + +NSString *const NSFontAttributeName = @"NSFont"; +NSString *const NSForegroundColorAttributeName = @"NSColor"; +NSString *const NSParagraphStyleAttributeName = @"NSParagraphStyle"; + +#pragma mark - NSArray -firstObject + +static id AppDropFirstObject(id self, SEL _cmd) { + return [self count] ? [self objectAtIndex:0] : nil; +} + +@implementation NSArray (AppDropFirstObjectImpl) ++ (void)load { + if ([NSArray instancesRespondToSelector:@selector(firstObject)]) return; + class_addMethod([NSArray class], @selector(firstObject), (IMP)AppDropFirstObject, "@@:"); +} +@end + +#pragma mark - NSString sizeWithAttributes: / draw* (bridge to UIStringDrawing) + +static UIFont *AppDropFontFromAttrs(NSDictionary *attrs) { + UIFont *f = [attrs objectForKey:NSFontAttributeName]; + return f ?: [UIFont systemFontOfSize:[UIFont systemFontSize]]; +} + +static CGSize AppDropSizeWithAttributes(id self, SEL _cmd, NSDictionary *attrs) { + return [self sizeWithFont:AppDropFontFromAttrs(attrs)]; +} + +static void AppDropDrawAtPoint(id self, SEL _cmd, CGPoint p, NSDictionary *attrs) { + UIColor *c = [attrs objectForKey:NSForegroundColorAttributeName]; + if (c) [c set]; + [self drawAtPoint:p withFont:AppDropFontFromAttrs(attrs)]; +} + +static void AppDropDrawInRect(id self, SEL _cmd, CGRect r, NSDictionary *attrs) { + UIColor *c = [attrs objectForKey:NSForegroundColorAttributeName]; + if (c) [c set]; + [self drawInRect:r withFont:AppDropFontFromAttrs(attrs)]; +} + +@implementation NSString (AppDropTextSizeImpl) ++ (void)load { + if (![NSString instancesRespondToSelector:@selector(sizeWithAttributes:)]) { + class_addMethod([NSString class], @selector(sizeWithAttributes:), + (IMP)AppDropSizeWithAttributes, "{CGSize=ff}@:@"); + } + if (![NSString instancesRespondToSelector:@selector(drawAtPoint:withAttributes:)]) { + class_addMethod([NSString class], @selector(drawAtPoint:withAttributes:), + (IMP)AppDropDrawAtPoint, "v@:{CGPoint=ff}@"); + } + if (![NSString instancesRespondToSelector:@selector(drawInRect:withAttributes:)]) { + class_addMethod([NSString class], @selector(drawInRect:withAttributes:), + (IMP)AppDropDrawInRect, "v@:{CGRect={CGPoint=ff}{CGSize=ff}}@"); + } +} +@end + +#pragma mark - NSData base64 (encode + decode) + +static const char b64tab[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static NSString *AppDropBase64Encode(id self, SEL _cmd, NSUInteger opt) { + const unsigned char *d = [self bytes]; + NSUInteger len = [self length]; + NSMutableData *out = [NSMutableData dataWithLength:((len + 2) / 3) * 4]; + char *o = [out mutableBytes]; + NSUInteger i = 0, j = 0; + while (i + 2 < len) { + unsigned int n = (d[i] << 16) | (d[i+1] << 8) | d[i+2]; + o[j++] = b64tab[(n >> 18) & 63]; o[j++] = b64tab[(n >> 12) & 63]; + o[j++] = b64tab[(n >> 6) & 63]; o[j++] = b64tab[n & 63]; + i += 3; + } + if (i < len) { + unsigned int n = d[i] << 16; + if (i + 1 < len) n |= d[i+1] << 8; + o[j++] = b64tab[(n >> 18) & 63]; + o[j++] = b64tab[(n >> 12) & 63]; + o[j++] = (i + 1 < len) ? b64tab[(n >> 6) & 63] : '='; + o[j++] = '='; + } + return [[NSString alloc] initWithBytes:o length:j encoding:NSASCIIStringEncoding]; +} + +static id AppDropBase64Decode(id self, SEL _cmd, NSString *str, NSUInteger opt) { + static signed char rev[256]; static BOOL init = NO; + if (!init) { memset(rev, -1, sizeof(rev)); for (int k = 0; k < 64; k++) rev[(unsigned char)b64tab[k]] = k; init = YES; } + const char *s = [str cStringUsingEncoding:NSASCIIStringEncoding]; + if (!s) return nil; + NSMutableData *out = [NSMutableData data]; + int buf = 0, bits = 0; + for (NSUInteger k = 0; s[k]; k++) { + signed char v = rev[(unsigned char)s[k]]; + if (v < 0) continue; + buf = (buf << 6) | v; bits += 6; + if (bits >= 8) { bits -= 8; unsigned char b = (buf >> bits) & 0xFF; [out appendBytes:&b length:1]; } + } + return out; +} + +@implementation NSData (AppDropBase64Impl) ++ (void)load { + if (![NSData instancesRespondToSelector:@selector(base64EncodedStringWithOptions:)]) { + class_addMethod([NSData class], @selector(base64EncodedStringWithOptions:), + (IMP)AppDropBase64Encode, "@@:L"); + } + if (![NSData instancesRespondToSelector:@selector(initWithBase64EncodedString:options:)]) { + class_addMethod([NSData class], @selector(initWithBase64EncodedString:options:), + (IMP)AppDropBase64Decode, "@@:@L"); + } +} +@end + +#pragma mark - UIImage +imageWithData:scale: + +// iOS 4.0 added +[UIImage imageWithCGImage:scale:orientation:]; iOS 3.1.3 only +// has the iOS 2.0 +imageWithCGImage:. Every armv6 device is a 1x display, so +// scale is always 1.0 and AppDrop always passes UIImageOrientationUp — both +// call sites in IconLoader.m — so falling through is exact, not lossy. +static UIImage *AppDropImageWithCGImageScaleOrientation(id cls, SEL _cmd, + CGImageRef cg, + CGFloat scale, + UIImageOrientation orientation) { + (void)scale; + (void)orientation; + if (!cg) return nil; + return [UIImage imageWithCGImage:cg]; +} + +static UIImage *AppDropImageWithDataScale(id cls, SEL _cmd, NSData *data, CGFloat scale) { + UIImage *img = [UIImage imageWithData:data]; + if (!img) return nil; + if ([UIImage instancesRespondToSelector:@selector(initWithCGImage:scale:orientation:)]) { + return [[UIImage alloc] initWithCGImage:img.CGImage scale:scale orientation:UIImageOrientationUp]; + } + return img; // pre-scale-aware OS: 1.0 scale is the only option +} + +// iOS 4.0 also added the INSTANCE property -[UIImage scale]. iOS 3.1.3 has no +// such selector, so reading `image.scale` (IconLoader.m's pre-decode path) +// throws NSInvalidArgumentException → uncaught → terminate → SIGABRT, a few +// seconds after launch once the first icons decode. Every armv6 device is a 1x +// display, so a constant 1.0 is exact. No-op on iOS 4+ where UIImage already +// answers -scale. +static CGFloat AppDropImageScale(id self, SEL _cmd) { + return 1.0f; +} + +@implementation UIImage (AppDropScaleImpl) ++ (void)load { + Class meta = object_getClass([UIImage class]); + if (![UIImage respondsToSelector:@selector(imageWithData:scale:)]) { + class_addMethod(meta, @selector(imageWithData:scale:), (IMP)AppDropImageWithDataScale, "@@:@f"); + } + if (![UIImage respondsToSelector:@selector(imageWithCGImage:scale:orientation:)]) { + class_addMethod(meta, @selector(imageWithCGImage:scale:orientation:), + (IMP)AppDropImageWithCGImageScaleOrientation, "@@:^vfi"); + } + if (![UIImage instancesRespondToSelector:@selector(scale)]) { + class_addMethod([UIImage class], @selector(scale), (IMP)AppDropImageScale, "f@:"); + } +} +@end + +#pragma mark - +[UIImage imageNamed:] extension-optional (iOS 3.1 quirk) +// On iOS 4.0+ `[UIImage imageNamed:@"tab-search"]` resolves "tab-search.png" +// (and the @2x variant) automatically. On iOS 3.1.3 imageNamed: requires the +// EXPLICIT extension — an extensionless name returns nil. AppDrop loads its +// tab-bar icons (tab-search / tab-install / tab-settings) and the default-theme +// button/cell/card chrome (btn-blue, cell-bg, card-bg, linen, …) by bare name, +// so on iOS 3 those images come back nil: the Search/Install/Settings tab icons +// and the default-theme glossy buttons silently vanish (issues #3 / #5). +// +// Fix: swizzle imageNamed: to fall back to an explicit ".png" (and "@2x.png") +// lookup ONLY when the original returned nil for an extensionless name. This is +// a no-op on iOS 4+ (the original already succeeds) so the binary stays native +// on 4–10. + +static IMP gOrigImageNamed = NULL; + +static UIImage *AppDropImageNamed(id self, SEL _cmd, NSString *name) { + UIImage *img = ((UIImage *(*)(id, SEL, NSString *))gOrigImageNamed)(self, _cmd, name); + if (img || ![name isKindOfClass:[NSString class]] || name.length == 0) return img; + if ([name pathExtension].length > 0) return img; // already had an extension; nothing to retry + + NSBundle *bundle = [NSBundle mainBundle]; + CGFloat scale = 1.0f; + if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) + scale = [[UIScreen mainScreen] scale]; + + // Prefer the @2x asset on a 2x screen, then fall back to the 1x file. + if (scale >= 2.0f) { + NSString *p2 = [bundle pathForResource:[name stringByAppendingString:@"@2x"] ofType:@"png"]; + if (p2) { + UIImage *i2 = [UIImage imageWithContentsOfFile:p2]; + // imageWithContentsOfFile reads @2x files at scale 1.0 on iOS 3; if the + // runtime supports -scale we can rebuild at the right scale via CGImage. + if (i2 && [UIImage respondsToSelector:@selector(imageWithCGImage:scale:orientation:)]) { + i2 = [UIImage imageWithCGImage:i2.CGImage scale:2.0f orientation:UIImageOrientationUp]; + } + if (i2) return i2; + } + } + NSString *p1 = [bundle pathForResource:name ofType:@"png"]; + if (p1) { + UIImage *i1 = [UIImage imageWithContentsOfFile:p1]; + if (i1) return i1; + } + return img; // still nil — give the caller what the OS returned +} + +@implementation UIImage (AppDropImageNamedExtImpl) ++ (void)load { + Class meta = object_getClass([UIImage class]); + Method m = class_getClassMethod([UIImage class], @selector(imageNamed:)); + if (!m) return; + gOrigImageNamed = method_getImplementation(m); + // Swizzle in-place: replace the class method's IMP with our wrapper. Safe on + // every OS — the wrapper only adds a fallback when the original returns nil. + if (class_addMethod(meta, @selector(imageNamed:), (IMP)AppDropImageNamed, "@@:@")) { + // method didn't exist on this class object (shouldn't happen) — added it. + } else { + method_setImplementation(m, (IMP)AppDropImageNamed); + } +} +@end + +#pragma mark - NSUUID (CFUUID-backed) +// Defined as a real class so the linker resolves _OBJC_CLASS_$_NSUUID at call +// sites. On iOS 3/4 (no system NSUUID) this is the only implementation. On +// iOS 6+ the OS already has NSUUID; the duplicate is harmless — both are +// CFUUID-backed and only -UUIDString is used by AppDrop. + +@implementation NSUUID ++ (instancetype)UUID { + return [[self alloc] init]; +} +- (NSString *)UUIDString { + CFUUIDRef u = CFUUIDCreate(NULL); + CFStringRef s = CFUUIDCreateString(NULL, u); + NSString *r = [(NSString *)s copy]; + CFRelease(s); + CFRelease(u); + return r; +} +@end + +#pragma mark - UIScreen -scale (iOS 4.0) +// The launch path (AppDelegate's tab-icon builders) reads +// [UIScreen mainScreen].scale before anything is on screen. -[UIScreen scale] +// first shipped in iOS 4.0; on iOS 3.1.3 it is an unrecognized selector, which +// throws an uncaught NSException at launch (objc_exception_throw → terminate → +// SIGABRT). Every armv6 device (original iPhone, 3G, iPod touch 1/2g) is a +// non-Retina 1x display, so backfilling a constant 1.0 is exact. No-op on +// iOS 4+ where UIScreen already answers -scale. + +static CGFloat AppDropScreenScale(id self, SEL _cmd) { + return 1.0f; +} + +@implementation UIScreen (AppDropScaleImpl) ++ (void)load { + if ([UIScreen instancesRespondToSelector:@selector(scale)]) return; + class_addMethod([UIScreen class], @selector(scale), (IMP)AppDropScreenScale, "f@:"); +} +@end + +#pragma mark - UIColor -getRed:green:blue:alpha: / -getWhite:alpha: (iOS 5.0) +// These component-extraction methods first shipped in iOS 5.0. On iOS 3.1.3 the +// concrete colour classes (UIDeviceRGBColor / UIDeviceWhiteColor) do NOT respond +// to them, so any call throws: +// *** -[UIDeviceRGBColor getRed:green:blue:alpha:]: unrecognized selector +// sent to instance ... NSInvalidArgumentException +// This is exactly the crash hit on a theme switch: IOS6Theme's colour math +// (ad_lum / ad_mix / ad_rgb) and UpdateNotesViewController decompose UIColors via +// these selectors. We backfill them by going through the colour's CGColor — which +// every UIColor has exposed since iOS 2.0 — and reading its components directly. +// +// Installed on [UIColor class]: class_addMethod on the (abstract) base class makes +// the IMP reachable from the concrete subclasses via normal message lookup, while +// CGColor dispatches to the real backing colour. No-op on iOS 5+ where the OS +// already provides the methods, so the same binary stays native on 5–10. + +static BOOL AppDropColorGetRGBA(id self, SEL _cmd, CGFloat *r, CGFloat *g, CGFloat *b, CGFloat *a) { + CGColorRef cg = [(UIColor *)self CGColor]; + if (!cg) return NO; + const CGFloat *comps = CGColorGetComponents(cg); + size_t n = CGColorGetNumberOfComponents(cg); + if (!comps) return NO; + CGFloat rr, gg, bb, aa; + if (n >= 4) { // RGBA + rr = comps[0]; gg = comps[1]; bb = comps[2]; aa = comps[3]; + } else if (n == 2) { // White + alpha — promote to grey RGB + rr = gg = bb = comps[0]; aa = comps[1]; + } else if (n == 1) { // White only + rr = gg = bb = comps[0]; aa = 1.0f; + } else { + return NO; + } + if (r) *r = rr; + if (g) *g = gg; + if (b) *b = bb; + if (a) *a = aa; + return YES; +} + +static BOOL AppDropColorGetWhiteAlpha(id self, SEL _cmd, CGFloat *w, CGFloat *a) { + CGColorRef cg = [(UIColor *)self CGColor]; + if (!cg) return NO; + const CGFloat *comps = CGColorGetComponents(cg); + size_t n = CGColorGetNumberOfComponents(cg); + if (!comps) return NO; + CGFloat ww, aa; + if (n >= 4) { // RGBA → luma-ish grey (matches Apple's behaviour closely enough) + ww = 0.299f*comps[0] + 0.587f*comps[1] + 0.114f*comps[2]; aa = comps[3]; + } else if (n == 2) { // White + alpha + ww = comps[0]; aa = comps[1]; + } else if (n == 1) { // White only + ww = comps[0]; aa = 1.0f; + } else { + return NO; + } + if (w) *w = ww; + if (a) *a = aa; + return YES; +} + +@implementation UIColor (AppDropComponentGettersImpl) ++ (void)load { + // CGFloat is `float` on armv6/armv7 (32-bit) → encoding "^f" for each pointer. + if (![UIColor instancesRespondToSelector:@selector(getRed:green:blue:alpha:)]) { + class_addMethod([UIColor class], @selector(getRed:green:blue:alpha:), + (IMP)AppDropColorGetRGBA, "c@:^f^f^f^f"); + } + if (![UIColor instancesRespondToSelector:@selector(getWhite:alpha:)]) { + class_addMethod([UIColor class], @selector(getWhite:alpha:), + (IMP)AppDropColorGetWhiteAlpha, "c@:^f^f"); + } +} +@end + +#pragma mark - CALayer -contentsScale / -setContentsScale: (iOS 4.0) +// AppTileView sets self.layer.contentsScale during -initWithFrame: (the catalog +// grid builds these as soon as the first tab loads). CALayer gained +// -contentsScale in iOS 4.0; on iOS 3 -setContentsScale: is an unrecognized +// selector. On a 1x display the setter is a no-op and the getter is 1.0. + +static CGFloat AppDropLayerContentsScale(id self, SEL _cmd) { + return 1.0f; +} +static void AppDropLayerSetContentsScale(id self, SEL _cmd, CGFloat scale) { + (void)scale; // 1x display: nothing to store +} + +@implementation CALayer (AppDropContentsScaleImpl) ++ (void)load { + if (![CALayer instancesRespondToSelector:@selector(contentsScale)]) { + class_addMethod([CALayer class], @selector(contentsScale), + (IMP)AppDropLayerContentsScale, "f@:"); + } + if (![CALayer instancesRespondToSelector:@selector(setContentsScale:)]) { + class_addMethod([CALayer class], @selector(setContentsScale:), + (IMP)AppDropLayerSetContentsScale, "v@:f"); + } +} +@end + +#pragma mark - UIGraphicsBeginImageContextWithOptions (iOS 4.0 C function) +// This C function (not a selector) is called in ~26 places, all in drawing +// paths reachable seconds after launch. It first shipped in iOS 4.0's UIKit; +// on iOS 3 the symbol is absent. We provide our OWN definition so the static +// linker binds every call site to this — no undefined UIKit import that would +// abort dyld on iOS 3. At runtime we look up the REAL UIKit implementation with +// RTLD_NEXT (skipping ourselves): on iOS 4+ it's found and forwarded to, so +// Retina rendering is unchanged; on iOS 3 it's NULL and we fall back to the +// iOS-2-era UIGraphicsBeginImageContext (1x is the only option on armv6). +// +// NOTE: must use RTLD_NEXT, not RTLD_DEFAULT — RTLD_DEFAULT would resolve to +// THIS function (it's a global symbol in the main executable) and recurse. + +typedef void (*AppDropUBICWO)(CGSize, BOOL, CGFloat); + +void UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale) { + static AppDropUBICWO real = NULL; + static int resolved = 0; + if (!resolved) { + real = (AppDropUBICWO)dlsym(RTLD_NEXT, "UIGraphicsBeginImageContextWithOptions"); + resolved = 1; + } + if (real) { + real(size, opaque, scale); + return; + } + // iOS 3 fallback: 1x bitmap context (all armv6 devices are non-Retina). + UIGraphicsBeginImageContext(size); +} + +#pragma mark - NSArray/NSMutableArray block-based sorting (iOS 4.0) +// -[NSMutableArray sortUsingComparator:] and -[NSArray sortedArrayUsingComparator:] +// take an NSComparator block and first shipped in iOS 4.0. On iOS 3.1.3 they are +// unrecognized selectors (the launch path hits sortUsingComparator: while building +// the Catalogue category list). We bridge them to the iOS-2-era +// sortUsingFunction:context: / sortedArrayUsingFunction:context: APIs, passing the +// block through as the context and invoking it from a C trampoline. No-op on iOS 4+ +// where the OS already provides the block-based variants. + +typedef NSComparisonResult (^AppDropComparatorBlock)(id, id); + +static NSInteger AppDropComparatorTrampoline(id a, id b, void *ctx) { + AppDropComparatorBlock cmp = (__bridge AppDropComparatorBlock)ctx; + return (NSInteger)cmp(a, b); +} + +static void AppDropSortUsingComparator(id self, SEL _cmd, id cmp) { + [self sortUsingFunction:AppDropComparatorTrampoline context:(void *)cmp]; +} + +static id AppDropSortedArrayUsingComparator(id self, SEL _cmd, id cmp) { + return [self sortedArrayUsingFunction:AppDropComparatorTrampoline context:(void *)cmp]; +} + +@implementation NSArray (AppDropComparatorSortImpl) ++ (void)load { + if (![NSArray instancesRespondToSelector:@selector(sortedArrayUsingComparator:)]) { + class_addMethod([NSArray class], @selector(sortedArrayUsingComparator:), + (IMP)AppDropSortedArrayUsingComparator, "@@:@?"); + } + if (![NSMutableArray instancesRespondToSelector:@selector(sortUsingComparator:)]) { + class_addMethod([NSMutableArray class], @selector(sortUsingComparator:), + (IMP)AppDropSortUsingComparator, "v@:@?"); + } +} +@end + +#pragma mark - UIWindow -rootViewController / -setRootViewController: (iOS 4.0) +// UIWindow gained rootViewController in iOS 4.0; on iOS 3.1.3 AppDelegate's +// @catch fallback (and the normal path) send setRootViewController: which is an +// unrecognized selector. We back it with an associated object (available since +// iOS 3.1.0) and reproduce the iOS 4 behaviour: install the controller's view +// as the window's content subview, removing the previous one. No-op on iOS 4+. + +static char kAppDropRootVCKey; + +static id AppDropWindowGetRootVC(id self, SEL _cmd) { + return objc_getAssociatedObject(self, &kAppDropRootVCKey); +} + +static void AppDropWindowSetRootVC(id self, SEL _cmd, id vc) { + UIViewController *old = objc_getAssociatedObject(self, &kAppDropRootVCKey); + if (old && old.isViewLoaded && old.view.superview == self) { + [old.view removeFromSuperview]; + } + objc_setAssociatedObject(self, &kAppDropRootVCKey, vc, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (vc) { + UIViewController *newVC = (UIViewController *)vc; + newVC.view.frame = [(UIWindow *)self bounds]; + [(UIWindow *)self addSubview:newVC.view]; + } +} + +@implementation UIWindow (AppDropRootVCImpl) ++ (void)load { + if ([UIWindow instancesRespondToSelector:@selector(setRootViewController:)]) return; + class_addMethod([UIWindow class], @selector(rootViewController), + (IMP)AppDropWindowGetRootVC, "@@:"); + class_addMethod([UIWindow class], @selector(setRootViewController:), + (IMP)AppDropWindowSetRootVC, "v@:@"); +} +@end + +#pragma mark - UIDevice userInterfaceIdiom (iOS 3.2+) on iOS 3.1.x + +// -[UIDevice userInterfaceIdiom] was introduced in iOS 3.2. On iOS 3.1.x it is +// an unrecognized selector, which aborts the app the moment any device-class +// check runs (e.g. CatalogFilter +defaultDeviceClass while loading/expanding +// the catalog). iOS 3.1 hardware is always iPhone/iPod touch, so return +// UIUserInterfaceIdiomPhone (0). +static NSInteger AppDropUserInterfaceIdiom(id self, SEL _cmd) { + return 0; // UIUserInterfaceIdiomPhone +} + +@interface UIDevice (AppDropIOS3Idiom) @end +@implementation UIDevice (AppDropIOS3Idiom) ++ (void)load { + if ([UIDevice instancesRespondToSelector:@selector(userInterfaceIdiom)]) return; + // 'i' = NSInteger (int on 32-bit arm); signature "i@:". + class_addMethod([UIDevice class], @selector(userInterfaceIdiom), + (IMP)AppDropUserInterfaceIdiom, "i@:"); +} +@end + +#pragma mark - +[NSOperationQueue mainQueue] (iOS 4.0) on iOS 3.x + +// +[NSOperationQueue mainQueue] is ALSO iOS 4.0 (NSOperationQueue itself is +// iOS 2.0, but the main-queue singleton accessor is not). The catalog feed, +// install metadata and version-list fetches all pass +// [NSOperationQueue mainQueue] to sendAsynchronousRequest:queue:completionHandler:, +// so the FIRST catalog load on a real 3.1.3 device throws: +// +// *** +[NSOperationQueue mainQueue]: unrecognized selector +// +// Backport: return a process-lifetime singleton queue. It only serves as an +// identity token — the ADAsyncURLCollector delivery path compares the queue +// pointer against +mainQueue and routes main-queue work through GCD's +// dispatch_get_main_queue() (gcd_shim), so operations are never actually +// executed ON this sentinel queue for the main-thread case. No-op on iOS 4+. + +static NSOperationQueue *gAppDropMainQueueSentinel = nil; + +static id AppDropMainQueue(id cls, SEL _cmd) { + if (!gAppDropMainQueueSentinel) { + gAppDropMainQueueSentinel = [[NSOperationQueue alloc] init]; // intentionally leaked: process-lifetime singleton + [gAppDropMainQueueSentinel setMaxConcurrentOperationCount:1]; + } + return gAppDropMainQueueSentinel; +} + +@interface NSOperationQueue (AppDropMainQueue) @end +@implementation NSOperationQueue (AppDropMainQueue) ++ (void)load { + if ([NSOperationQueue respondsToSelector:@selector(mainQueue)]) return; + Class meta = object_getClass([NSOperationQueue class]); + class_addMethod(meta, @selector(mainQueue), (IMP)AppDropMainQueue, "@@:"); +} +@end + +#pragma mark - -[NSOperationQueue addOperationWithBlock:] (iOS 4.0) on iOS 3.x + +// The INSTANCE method -[NSOperationQueue addOperationWithBlock:] is iOS 4.0+ +// (distinct from the NSBlockOperation class, which AppDropBlockOp.m already +// backports). v3.1.3's IconLoader calls it on diskDecodeQueue for the disk- +// cache decode step, which runs the moment the catalog grid shows its first +// uncached icon -> on a real 3.1.3 device: +// +// *** -[NSOperationQueue addOperationWithBlock:]: unrecognized selector +// *** Terminating app due to uncaught exception 'NSInvalidArgumentException' +// +// Backport: wrap the block in an ADBlockOperation (which heap-copies it via the +// C blocks runtime - blocks are NOT ObjC objects on iOS 3, see AppDropBlocks.h) +// and feed that to -addOperation:, which iOS 3.x has had since 2.0. No-op on +// iOS 4+ where the real selector exists. + +static void AppDropAddOperationWithBlock(id self, SEL _cmd, void (^block)(void)) { + if (!block) return; + // Main-queue semantics: if this is the backported +mainQueue sentinel, the + // caller expects the block on the MAIN THREAD. The sentinel is a plain + // background queue (it exists only as an identity token), so route the work + // through GCD's main queue instead of actually executing on the sentinel. + if (self == gAppDropMainQueueSentinel && gAppDropMainQueueSentinel) { + void (^heap)(void) = (void (^)(void))_Block_copy((const void *)block); + dispatch_async(dispatch_get_main_queue(), ^{ heap(); _Block_release((const void *)heap); }); + return; + } + [self addOperation:[ADBlockOperation blockOperationWithBlock:block]]; +} + +@interface NSOperationQueue (AppDropAddBlock) @end +@implementation NSOperationQueue (AppDropAddBlock) ++ (void)load { + if ([NSOperationQueue instancesRespondToSelector:@selector(addOperationWithBlock:)]) return; + // "v@:@?" -> void, self, _cmd, block + class_addMethod([NSOperationQueue class], @selector(addOperationWithBlock:), + (IMP)AppDropAddOperationWithBlock, "v@:@?"); +} +@end + +#pragma mark - Adaptive concurrency helpers (IOS5Compat.m is excluded from this build) + +// The theos (iOS 5-10) build compiles IPAInstaller/IOS5Compat.m, which defines +// these. The iOS 3 build EXCLUDES that file (AppDropRuntime.m supersedes its +// subscript shims), so the definitions must live here too — same semantics. +// -[NSProcessInfo activeProcessorCount] exists since iOS 2.0; no guard needed. +NSUInteger ADRecommendedConcurrency(void) { + NSUInteger n = [[NSProcessInfo processInfo] activeProcessorCount]; + return n < 1 ? 1 : n; +} + +NSUInteger ADRecommendedConcurrencyCapped(NSUInteger maxCap) { + NSUInteger n = ADRecommendedConcurrency(); + return (maxCap && n > maxCap) ? maxCap : n; +} + +#pragma mark - NSException callStackSymbols (iOS 4.0+) on iOS 3.x + +// -[NSException callStackSymbols] was introduced in iOS 4.0. On iOS 3.x it is +// an unrecognized selector. main.m's uncaught-exception handler calls it, so +// provide a harmless empty-array fallback to keep crash logging clean. +static id AppDropCallStackSymbols(id self, SEL _cmd) { + return [NSArray array]; +} + +@interface NSException (AppDropIOS3CallStack) @end +@implementation NSException (AppDropIOS3CallStack) ++ (void)load { + if ([NSException instancesRespondToSelector:@selector(callStackSymbols)]) return; + class_addMethod([NSException class], @selector(callStackSymbols), + (IMP)AppDropCallStackSymbols, "@@:"); +} +@end + +#pragma mark - -[UINavigationItem setRightBarButtonItems:] / setLeftBarButtonItems: (+ getters) (iOS 5.0) + +// The PLURAL (array) bar-button accessors — +// -setRightBarButtonItems:[/animated:], -setLeftBarButtonItems:[/animated:], +// -rightBarButtonItems, -leftBarButtonItems — +// are iOS 5.0. The 5.1 SDK declares them (so `navigationItem.rightBarButtonItems = @[...]` +// compiles), but on a real iOS 3.1.3 device UINavigationItem only answers the +// SINGULAR iOS-2 API: -setRightBarButtonItem:[/animated:] / -setLeftBarButtonItem:. +// The Search tab (refreshSearchNav) assigns TWO right items (Filters + Select), +// and Catalog / Collection / AppDetail / Root do the same → the moment that nav +// bar is configured, 3.1 throws "unrecognized selector" → NSInvalidArgumentException +// → SIGABRT. This is the search-tab launch crash. +// +// iOS 3's navigation item shows only ONE button per side, so to honour an array +// of >1 we host the buttons in a transparent UIToolbar and hand that to the +// SINGULAR setter as the customView of one UIBarButtonItem — the canonical +// iOS 3/4 multi-button technique. A single-element (or empty) array maps +// directly to the singular setter with no toolbar wrapper. We also store the +// original array via an associated object so the matching getter round-trips +// (some call sites read .rightBarButtonItems back). No-op on iOS 5+ where UIKit +// already provides all four selectors. + +static char kAppDropRightItemsKey; +static char kAppDropLeftItemsKey; + +// Build the single UIBarButtonItem that represents `items` for the singular API. +// nil/empty -> no items (clear the side) +// 1 -> that item, unchanged +// >1 -> a transparent UIToolbar (sized COMPACTLY to its contents) wrapped +// in one custom-view item +// +// `reverseOrder` matches iOS 5+ semantics: the navigationItem's RIGHT-side array +// is ordered right-to-left (element 0 is the RIGHTMOST button), whereas a +// UIToolbar lays its items out left-to-right. So for the right side we reverse +// the array before handing it to the toolbar; the left side is left-to-right on +// both and needs no reversal. +// +// CRITICAL (iOS 3.1.3): we must NOT call -[UIToolbar sizeToFit] here. On iOS 3 +// a toolbar's -sizeToFit snaps its width to the FULL navigation-bar width +// (320 pt), so the transparent wrapper toolbar ends up covering the entire bar +// — hiding the title and the left/back button, and jamming the buttons against +// the left edge (exactly the "no title, no Feedback/Home/back button, buttons +// shoved left" symptom). Instead we measure each item and give the toolbar an +// explicit, content-tight width so it occupies only the right (or left) end of +// the bar, leaving the title centered and the opposite side free. + +// Estimate the on-screen width of one bar-button item for compact layout. +static CGFloat AppDropEstimateBarItemWidth(UIBarButtonItem *item) { + if (!item) return 0.0f; + // Custom view: use its own width. + UIView *cv = [item respondsToSelector:@selector(customView)] ? [item customView] : nil; + if (cv) { + CGFloat cw = cv.frame.size.width; + return cw > 0 ? cw + 12.0f : 44.0f; + } + // System items (Refresh, Add, Done-style icons, etc.): fixed-width glyph slot. + if ([item respondsToSelector:@selector(width)] && item.width > 0) { + return item.width + 12.0f; // explicit fixed-space width + } + // Titled text button: measure the title with the bar-button font (~15pt bold) + // plus the rounded-rect bezel padding on each side. + NSString *title = [item respondsToSelector:@selector(title)] ? [item title] : nil; + if (title.length) { + UIFont *f = [UIFont boldSystemFontOfSize:15]; + CGSize sz = [title sizeWithFont:f]; // iOS-2 API; present on every OS here + return sz.width + 24.0f; // ~12pt bezel padding each side + } + // Image-only or system glyph item with no measurable title. + return 44.0f; +} + +static UIBarButtonItem *AppDropFlexibleSpaceItem(void) { + return [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:nil] autorelease]; +} + +@interface ADTransparentToolbar : UIToolbar { +} +- (void)drawRect:(CGRect)rect; +@end + +@implementation ADTransparentToolbar +- (void)drawRect:(CGRect)rect { + // Draw nothing — transparent background on every iOS, including iOS 3 where + // setBackgroundImage:forToolbarPosition:barMetrics: doesn't exist. +} +@end + +static UIBarButtonItem *AppDropWrapBarItems(NSArray *items, BOOL reverseOrder) { + NSUInteger count = [items count]; + if (count == 0) return nil; + if (count == 1) return [items objectAtIndex:0]; + + NSArray *ordered = items; + if (reverseOrder) { + NSMutableArray *rev = [NSMutableArray arrayWithCapacity:count]; + for (NSUInteger i = count; i > 0; i--) { + [rev addObject:[items objectAtIndex:(i - 1)]]; + } + ordered = rev; + } + + // Right side (reverseOrder): lead with a flexible space so the buttons are + // pushed against the toolbar's RIGHT edge — the nav bar right-aligns the + // custom view, so this is what makes the buttons hug the screen edge + // instead of floating slightly left of it. + NSMutableArray *toolbarItems = [NSMutableArray arrayWithCapacity:count + 2]; + if (reverseOrder) { + [toolbarItems addObject:AppDropFlexibleSpaceItem()]; + } + [toolbarItems addObjectsFromArray:ordered]; + if (reverseOrder) { + // UIToolbar pads ~6pt inside its right edge; a small NEGATIVE fixed + // space after the last button cancels that pad so the rightmost button + // visually lines up with the screen edge like a native nav-bar button. + // (Negative fixed-space widths are honoured on iOS 3–10; if a future + // OS clamps them to 0 the only effect is the old 6pt gap returns.) + UIBarButtonItem *pull = [[[UIBarButtonItem alloc] + initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace + target:nil action:nil] autorelease]; + pull.width = -6.0f; + [toolbarItems addObject:pull]; + } + + // Compact content width: sum of per-item widths + inter-item gaps + end caps. + CGFloat width = 0.0f; + for (UIBarButtonItem *it in ordered) width += AppDropEstimateBarItemWidth(it); + width += (CGFloat)(count - 1) * 8.0f; // gaps between buttons + width += reverseOrder ? 20.0f : 12.0f; // a little extra room lets right-side buttons hug the edge + if (width < 44.0f) width = (CGFloat)count * 44.0f; // floor: keep buttons tappable + + ADTransparentToolbar *bar = [[[ADTransparentToolbar alloc] initWithFrame:CGRectMake(0, 0, width, 44)] autorelease]; + // Transparent so it blends into the navigation bar instead of drawing a + // second opaque toolbar background on top of it. On iOS 5+ the empty + // background image clears the chrome; on iOS 3/4 (no setBackgroundImage:…) + // the ADTransparentToolbar -drawRect: override below draws NOTHING, so the + // old black-translucent box (issue #4: black space + black-on-black icons in + // the Search nav bar) never appears. + bar.barStyle = UIBarStyleBlackTranslucent; + bar.translucent = YES; + bar.backgroundColor = [UIColor clearColor]; + if ([bar respondsToSelector:@selector(setBackgroundImage:forToolbarPosition:barMetrics:)]) { + [bar setBackgroundImage:[[[UIImage alloc] init] autorelease] + forToolbarPosition:0 /* UIToolbarPositionAny */ + barMetrics:0 /* UIBarMetricsDefault */]; + } + [bar setItems:toolbarItems animated:NO]; + // Explicit compact frame — deliberately NO sizeToFit (see note above). + bar.frame = CGRectMake(0, 0, width, 44); + return [[[UIBarButtonItem alloc] initWithCustomView:bar] autorelease]; +} + +static void AppDropSetRightBarButtonItems(id self, SEL _cmd, NSArray *items, BOOL animated) { + objc_setAssociatedObject(self, &kAppDropRightItemsKey, items, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self setRightBarButtonItem:AppDropWrapBarItems(items, YES) animated:animated]; +} +static void AppDropSetRightBarButtonItemsNoAnim(id self, SEL _cmd, NSArray *items) { + AppDropSetRightBarButtonItems(self, _cmd, items, NO); +} +static id AppDropGetRightBarButtonItems(id self, SEL _cmd) { + NSArray *stored = objc_getAssociatedObject(self, &kAppDropRightItemsKey); + if (stored) return stored; + UIBarButtonItem *single = [self rightBarButtonItem]; + return single ? [NSArray arrayWithObject:single] : [NSArray array]; +} + +static void AppDropSetLeftBarButtonItems(id self, SEL _cmd, NSArray *items, BOOL animated) { + objc_setAssociatedObject(self, &kAppDropLeftItemsKey, items, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self setLeftBarButtonItem:AppDropWrapBarItems(items, NO) animated:animated]; +} +static void AppDropSetLeftBarButtonItemsNoAnim(id self, SEL _cmd, NSArray *items) { + AppDropSetLeftBarButtonItems(self, _cmd, items, NO); +} +static id AppDropGetLeftBarButtonItems(id self, SEL _cmd) { + NSArray *stored = objc_getAssociatedObject(self, &kAppDropLeftItemsKey); + if (stored) return stored; + UIBarButtonItem *single = [self leftBarButtonItem]; + return single ? [NSArray arrayWithObject:single] : [NSArray array]; +} + +@interface UINavigationItem (AppDropPluralBarItems) @end +@implementation UINavigationItem (AppDropPluralBarItems) ++ (void)load { + if ([UINavigationItem instancesRespondToSelector:@selector(setRightBarButtonItems:)]) return; + // "v@:@" -> void, self, _cmd, id (no-animated setter) + // "v@:@c" -> void, self, _cmd, id, BOOL (animated setter) + // "@@:" -> id, self, _cmd (getter) + class_addMethod([UINavigationItem class], @selector(setRightBarButtonItems:), + (IMP)AppDropSetRightBarButtonItemsNoAnim, "v@:@"); + class_addMethod([UINavigationItem class], @selector(setRightBarButtonItems:animated:), + (IMP)AppDropSetRightBarButtonItems, "v@:@c"); + class_addMethod([UINavigationItem class], @selector(rightBarButtonItems), + (IMP)AppDropGetRightBarButtonItems, "@@:"); + class_addMethod([UINavigationItem class], @selector(setLeftBarButtonItems:), + (IMP)AppDropSetLeftBarButtonItemsNoAnim, "v@:@"); + class_addMethod([UINavigationItem class], @selector(setLeftBarButtonItems:animated:), + (IMP)AppDropSetLeftBarButtonItems, "v@:@c"); + class_addMethod([UINavigationItem class], @selector(leftBarButtonItems), + (IMP)AppDropGetLeftBarButtonItems, "@@:"); +} +@end + +#pragma mark - presentViewController:animated:completion: / dismissViewControllerAnimated:completion: (iOS 5.0) + +// These two selectors are iOS 5.0. The 5.1 SDK declares them, so call sites +// compile, but on a real iOS 3.x device UIViewController only responds to the +// iOS-2 modal API (-presentModalViewController:animated: / +// -dismissModalViewControllerAnimated:). AppDrop opens several modals +// (CatalogViewController, FeedbackViewController, FilePickerViewController, +// RevivalListViewController), so without this the first modal throws an +// unrecognized selector → uncaught NSException → SIGABRT. +// +// We bridge to the old modal API and fire the completion block AFTER the +// animation would finish (no completion callback exists pre-iOS-5, so we +// approximate with a delay matching UIKit's ~0.35s modal transition). The +// completion block is heap-copied with _Block_copy (blocks are NOT ObjC +// objects on iOS 3 — see AppDropBlocks.h) and released after it runs. +// No-op on iOS 5+ where UIKit already answers these selectors. + +static void AppDropRunAndReleaseCompletion(void (^completion)(void)) { + if (!completion) return; + completion(); + _Block_release((const void *)completion); +} + +static void AppDropPresentVC(id self, SEL _cmd, id vcToPresent, BOOL animated, void (^completion)(void)) { + [self presentModalViewController:vcToPresent animated:animated]; + if (completion) { + void (^heap)(void) = (void (^)(void))_Block_copy((const void *)completion); + double delay = animated ? 0.35 : 0.0; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ AppDropRunAndReleaseCompletion(heap); }); + } +} + +static void AppDropDismissVC(id self, SEL _cmd, BOOL animated, void (^completion)(void)) { + [self dismissModalViewControllerAnimated:animated]; + if (completion) { + void (^heap)(void) = (void (^)(void))_Block_copy((const void *)completion); + double delay = animated ? 0.35 : 0.0; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ AppDropRunAndReleaseCompletion(heap); }); + } +} + +@interface UIViewController (AppDropModalBridge) @end +@implementation UIViewController (AppDropModalBridge) ++ (void)load { + if (![UIViewController instancesRespondToSelector:@selector(presentViewController:animated:completion:)]) { + // "v@:@c@?" → void, self, _cmd, id, BOOL(char), block(@?) + class_addMethod([UIViewController class], + @selector(presentViewController:animated:completion:), + (IMP)AppDropPresentVC, "v@:@c@?"); + } + if (![UIViewController instancesRespondToSelector:@selector(dismissViewControllerAnimated:completion:)]) { + class_addMethod([UIViewController class], + @selector(dismissViewControllerAnimated:completion:), + (IMP)AppDropDismissVC, "v@:c@?"); + } +} +@end + +#pragma mark - viewWillLayoutSubviews / viewDidLayoutSubviews bridge (iOS 5.0) +// -[UIViewController viewWillLayoutSubviews] / -viewDidLayoutSubviews first +// shipped in iOS 5.0. AppDrop puts ALL of its frame math in these callbacks +// (CategoryViewController/SearchViewController/CatalogViewController/… set their +// scroll-view contentSize and re-flow tiles there). On iOS 3.1.3 UIKit NEVER +// calls them, so the home grid's UIScrollView keeps a zero contentSize and the +// long pages simply don't scroll — and there's no scroll indicator (issue #1). +// +// Fix: swizzle -[UIView layoutSubviews] (called by UIKit on every iOS since 2.0 +// whenever a view lays out). When the laid-out view is a UIViewController's +// ROOT view (its nextResponder is that controller), drive the controller's +// viewWillLayoutSubviews / viewDidLayoutSubviews around the normal layout. This +// is gated to iOS < 5.0 (CoreFoundation < 675.00) so on 5+ the OS keeps firing +// the callbacks itself and we don't double-invoke them. + +static IMP gOrigViewLayoutSubviews = NULL; +static IMP gVCLayoutNoopIMP = NULL; + +static void AppDropVCLayoutNoop(id self, SEL _cmd) { + // No-op base implementation of viewWillLayoutSubviews / viewDidLayoutSubviews + // so subclasses' [super viewWillLayoutSubviews] calls don't throw on iOS 3/4 + // (UIViewController gained these in iOS 5.0). + (void)self; (void)_cmd; +} + +// YES only when -vc-'s class provides a REAL override of -sel- (not the no-op +// base IMP we installed above, and not a missing method). Prevents us from +// "driving" layout on plain controllers that never opted in. +static BOOL AppDropVCOverridesLayout(UIViewController *vc, SEL sel) { + Class c = [vc class]; + Method m = class_getInstanceMethod(c, sel); + if (!m) return NO; + IMP imp = method_getImplementation(m); + return imp != gVCLayoutNoopIMP; +} + +static void AppDropViewLayoutSubviews(id self, SEL _cmd) { + UIView *v = (UIView *)self; + UIViewController *vc = nil; + @try { + id next = [v nextResponder]; + if ([next isKindOfClass:[UIViewController class]]) { + UIViewController *cand = (UIViewController *)next; + // Only the controller's ROOT view drives its layout callbacks. + if (cand.isViewLoaded && cand.view == v) vc = cand; + } + } @catch (__unused id e) {} + + BOOL doWill = vc && AppDropVCOverridesLayout(vc, @selector(viewWillLayoutSubviews)); + BOOL doDid = vc && AppDropVCOverridesLayout(vc, @selector(viewDidLayoutSubviews)); + + if (doWill) { + @try { [vc viewWillLayoutSubviews]; } @catch (__unused id e) {} + } + if (gOrigViewLayoutSubviews) { + ((void (*)(id, SEL))gOrigViewLayoutSubviews)(self, _cmd); + } + if (doDid) { + @try { [vc viewDidLayoutSubviews]; } @catch (__unused id e) {} + } +} + +@interface UIView (AppDropLayoutBridge) @end +@implementation UIView (AppDropLayoutBridge) ++ (void)load { + // Only needed on iOS < 5.0; on 5+ UIKit already fires the VC layout callbacks. + // CoreFoundation 675.00 == iOS 5.0; below that we install the bridge. + if (kCFCoreFoundationVersionNumber >= 675.00) return; + // No-op base impls so the subclasses' [super viewWill/DidLayoutSubviews] resolve. + if (![UIViewController instancesRespondToSelector:@selector(viewWillLayoutSubviews)]) { + class_addMethod([UIViewController class], @selector(viewWillLayoutSubviews), + (IMP)AppDropVCLayoutNoop, "v@:"); + } + if (![UIViewController instancesRespondToSelector:@selector(viewDidLayoutSubviews)]) { + class_addMethod([UIViewController class], @selector(viewDidLayoutSubviews), + (IMP)AppDropVCLayoutNoop, "v@:"); + } + gVCLayoutNoopIMP = (IMP)AppDropVCLayoutNoop; + Method m = class_getInstanceMethod([UIView class], @selector(layoutSubviews)); + if (!m) return; + gOrigViewLayoutSubviews = method_getImplementation(m); + method_setImplementation(m, (IMP)AppDropViewLayoutSubviews); +} +@end + +#pragma mark - -[UITableView setBackgroundView:] / backgroundView (iOS 3.2) + +// UITableView -backgroundView / -setBackgroundView: debut in iOS 3.2. The 5.1 +// SDK declares them so call sites compile, but on a real iOS 3.1.3 device the +// selectors don't exist on UITableView, so the first assignment throws: +// *** -[UITableView setBackgroundView:]: unrecognized selector +// → uncaught NSInvalidArgumentException → SIGABRT +// AppDrop sets `table.backgroundView = nil` in several controllers (and +// -[RootViewController buildTable] does it unconditionally) to strip the light +// iOS-6 grouped backdrop, so the app aborts the moment a table is built. +// +// We install both selectors (only when absent, i.e. on 3.x). The stored view is +// kept as an associated object with retain semantics; a non-nil view is inserted +// as the table's lowest subview, nil just removes the previously stored one. On +// 3.1.3 the grouped backdrop the app removes doesn't exist, so `= nil` is a +// harmless no-op; the real insert/remove keeps a future non-nil assignment sane. + +static const char kAppDropTableBackgroundViewKey; + +static id AppDropTableBackgroundView(id self, SEL _cmd) { + return objc_getAssociatedObject(self, &kAppDropTableBackgroundViewKey); +} + +static void AppDropTableSetBackgroundView(id self, SEL _cmd, id bgView) { + UIView *old = objc_getAssociatedObject(self, &kAppDropTableBackgroundViewKey); + if (old == bgView) return; + if (old) [old removeFromSuperview]; + // OBJC_ASSOCIATION_RETAIN (=01401) so the view lives as long as the table. + objc_setAssociatedObject(self, &kAppDropTableBackgroundViewKey, bgView, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (bgView) { + UIView *table = (UIView *)self; + [bgView setFrame:[table bounds]]; + [table insertSubview:bgView atIndex:0]; + } +} + +@interface UITableView (AppDropBackgroundViewShim) @end +@implementation UITableView (AppDropBackgroundViewShim) ++ (void)load { + if (![UITableView instancesRespondToSelector:@selector(backgroundView)]) { + class_addMethod([UITableView class], @selector(backgroundView), + (IMP)AppDropTableBackgroundView, "@@:"); + } + if (![UITableView instancesRespondToSelector:@selector(setBackgroundView:)]) { + class_addMethod([UITableView class], @selector(setBackgroundView:), + (IMP)AppDropTableSetBackgroundView, "v@:@"); + } +} +@end + +#pragma mark - +[UIView animateWithDuration:...] block animations (iOS 4.0) + +// The block-based UIView animation CLASS methods are iOS 4.0+: +// +animateWithDuration:animations: +// +animateWithDuration:animations:completion: +// +animateWithDuration:delay:options:animations:completion: +// The 5.1 SDK declares them so call sites compile, but on iOS 3.1.3 they are +// unrecognized selectors on the UIView METACLASS — this is EXACTLY the crash +// this backport hit: +// *** +[UIView animateWithDuration:animations:completion:]: +// unrecognized selector sent to class 0x3839c028 (= UIView metaclass) +// → objc_msgSend dereferences a bogus class → EXC_BAD_ACCESS (SIGBUS) +// It fires from every animated menu/transition: ADNumberPickerSheet (sheet +// slide in/out), CategoryViewController (Home tab edit-mode + layout), +// CategoryTileView (tile fade-in), AppTileView (tap bounce) and +// AppDetailViewController (banner resize). +// +// We bridge to the iOS-2-era begin/commit animation API +// (+beginAnimations:context: … +commitAnimations), run the animations block +// synchronously between begin and commit (matching UIKit), and deliver the +// completion via the classic animationDidStop:finished:context: delegate +// callback. The completion block is heap-copied with _Block_copy (blocks are +// NOT ObjC objects on iOS 3 — see AppDropBlocks.h) and released after it fires. +// The collector self-retains for the duration of the transition (the +1 from +// +alloc is balanced by -autorelease in the didStop callback), so it survives +// regardless of whether this build's UIView retains the animation delegate. +// No-op on iOS 4+ where UIKit already answers these selectors. + +@interface ADAnimationCompletion : NSObject { + void (^_completion)(BOOL); // heap block, owned via _Block_copy +} +- (id)initWithCompletion:(void (^)(BOOL))completion; +@end + +@implementation ADAnimationCompletion +- (id)initWithCompletion:(void (^)(BOOL))completion { + if ((self = [super init])) { + if (completion) _completion = (void (^)(BOOL))_Block_copy((const void *)completion); + } + return self; +} +- (void)animationDidStop:(NSString *)animationID + finished:(NSNumber *)finished + context:(void *)context { + if (_completion) _completion([finished boolValue]); + [self autorelease]; // balance the self-retain taken before +commitAnimations +} +- (void)dealloc { + if (_completion) _Block_release((const void *)_completion); + [super dealloc]; +} +@end + +static void AppDropRunBlockAnimation(NSTimeInterval duration, + NSTimeInterval delay, + UIViewAnimationCurve curve, + BOOL hasCurve, + void (^animations)(void), + void (^completion)(BOOL)) { + // No animations block: UIKit still fires the completion (with finished=YES). + // Approximate by invoking it on the next main-runloop turn. Mirror the + // _Block_copy / _Block_release pattern used by the modal bridge above. + if (!animations) { + if (completion) { + void (^heap)(BOOL) = (void (^)(BOOL))_Block_copy((const void *)completion); + dispatch_async(dispatch_get_main_queue(), ^{ + heap(YES); + _Block_release((const void *)heap); + }); + } + return; + } + + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationDuration:duration]; + if (delay > 0.0) [UIView setAnimationDelay:delay]; + if (hasCurve) [UIView setAnimationCurve:curve]; + if (completion) { + ADAnimationCompletion *col = + [[ADAnimationCompletion alloc] initWithCompletion:completion]; + [UIView setAnimationDelegate:col]; // see note above re: lifetime / +1 + [UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)]; + } + animations(); + [UIView commitAnimations]; +} + +// IMP shapes match the three selectors. NSTimeInterval is double ('d'); +// NSUInteger is encoded 'L' on 32-bit armv6 (matching the rest of this file); +// blocks are '@?'. +static void AppDropAnimateDurationAnimations(id cls, SEL _cmd, + NSTimeInterval duration, + void (^animations)(void)) { + AppDropRunBlockAnimation(duration, 0.0, 0, NO, animations, NULL); +} + +static void AppDropAnimateDurationAnimationsCompletion(id cls, SEL _cmd, + NSTimeInterval duration, + void (^animations)(void), + void (^completion)(BOOL)) { + AppDropRunBlockAnimation(duration, 0.0, 0, NO, animations, completion); +} + +static void AppDropAnimateDurationDelayOptions(id cls, SEL _cmd, + NSTimeInterval duration, + NSTimeInterval delay, + NSUInteger options, + void (^animations)(void), + void (^completion)(BOOL)) { + // UIViewAnimationOptions packs the curve in bits 16-17, and the values map + // 1:1 onto UIViewAnimationCurve (EaseInOut=0, EaseIn=1, EaseOut=2, + // Linear=3). All other options (BeginFromCurrentState, AllowUserInteraction, + // …) have no iOS-3 begin/commit equivalent and are harmless to drop. + UIViewAnimationCurve curve = (UIViewAnimationCurve)((options >> 16) & 0x3); + AppDropRunBlockAnimation(duration, delay, curve, YES, animations, completion); +} + +@interface UIView (AppDropBlockAnimations) @end +@implementation UIView (AppDropBlockAnimations) ++ (void)load { + // Class methods live on the metaclass; +respondsToSelector: tests them. + Class meta = object_getClass((id)[UIView class]); + if (![UIView respondsToSelector:@selector(animateWithDuration:animations:)]) { + class_addMethod(meta, @selector(animateWithDuration:animations:), + (IMP)AppDropAnimateDurationAnimations, "v@:d@?"); + } + if (![UIView respondsToSelector:@selector(animateWithDuration:animations:completion:)]) { + class_addMethod(meta, @selector(animateWithDuration:animations:completion:), + (IMP)AppDropAnimateDurationAnimationsCompletion, "v@:d@?@?"); + } + if (![UIView respondsToSelector:@selector(animateWithDuration:delay:options:animations:completion:)]) { + class_addMethod(meta, @selector(animateWithDuration:delay:options:animations:completion:), + (IMP)AppDropAnimateDurationDelayOptions, "v@:ddL@?@?"); + } +} +@end + +#pragma mark - -[NSString componentsSeparatedByCharactersInSet:] (iOS 3.2) + +// Introduced in iOS 3.2; absent on iOS 3.1.x → unrecognized selector. AppDrop's +// InstallManager parses ipainstaller output with it (-installedVersionForBundle:), +// which runs whenever an install/update is attempted. We implement it by scanning +// the receiver and splitting on any character that is a member of the set — +// identical semantics to Apple's, including empty substrings between adjacent +// separators. No-op on iOS 3.2+/4+ where Foundation already provides it. +static id AppDropComponentsSeparatedByCharactersInSet(id self, SEL _cmd, NSCharacterSet *sep) { + if (!sep) return [NSArray arrayWithObject:self]; + NSMutableArray *parts = [NSMutableArray array]; + NSUInteger len = [(NSString *)self length]; + NSUInteger start = 0; + for (NSUInteger i = 0; i < len; i++) { + unichar ch = [(NSString *)self characterAtIndex:i]; + if ([sep characterIsMember:ch]) { + [parts addObject:[(NSString *)self substringWithRange:NSMakeRange(start, i - start)]]; + start = i + 1; + } + } + [parts addObject:[(NSString *)self substringWithRange:NSMakeRange(start, len - start)]]; + return parts; +} + +@interface NSString (AppDropComponentsSepImpl) @end +@implementation NSString (AppDropComponentsSepImpl) ++ (void)load { + if ([NSString instancesRespondToSelector:@selector(componentsSeparatedByCharactersInSet:)]) return; + class_addMethod([NSString class], @selector(componentsSeparatedByCharactersInSet:), + (IMP)AppDropComponentsSeparatedByCharactersInSet, "@@:@"); +} +@end + +#pragma mark - +[NSURLConnection sendAsynchronousRequest:queue:completionHandler:] (iOS 5.0) + +// This class method is iOS 5.0. The 5.1 SDK declares it (call sites compile), +// but on iOS 3.x it is an unrecognized selector on NSURLConnection. AppDrop +// uses it for the catalog feed, search, install metadata and version lists — +// i.e. the moment any real network list loads, 3.1 would throw. We back it with +// the iOS-2 delegate API (-initWithRequest:delegate:) via a tiny self-retaining +// collector that accumulates data and posts the completion onto the requested +// NSOperationQueue (all call sites pass +mainQueue). The completion block is a +// real ObjC-collection-safe value here only because it is captured by ADBlockBox +// (blocks are not ObjC objects on iOS 3 — see AppDropBlocks.h). +// No-op on iOS 5+ where Foundation already provides the method. + +@interface ADAsyncURLCollector : NSObject { + NSMutableData *_data; + NSURLResponse *_response; + NSOperationQueue *_queue; + ADBlockBox *_completionBox; // boxes void(^)(NSURLResponse*,NSData*,NSError*) +} +@end + +@implementation ADAsyncURLCollector + +- (id)initWithQueue:(NSOperationQueue *)queue + completion:(void (^)(NSURLResponse *, NSData *, NSError *))completion { + if ((self = [super init])) { + _data = [[NSMutableData alloc] init]; + _queue = [queue retain]; + // Reuse ADBlockBox's safe heap-copy storage; cast the 3-arg shape through + // the void(^)(id) box slot (only the heap pointer matters; we cast back). + _completionBox = [[ADBlockBox boxWithImageBlock:(void(^)(id))completion] retain]; + } + return self; +} + +- (void)deliverResponse:(NSURLResponse *)resp data:(NSData *)data error:(NSError *)error { + void (^completion)(NSURLResponse *, NSData *, NSError *) = + (void (^)(NSURLResponse *, NSData *, NSError *))[_completionBox block]; + if (!completion) return; + // -[NSOperationQueue addOperationWithBlock:] is itself iOS 4.0, so we can't + // use it on 3.x. Every AppDrop call site passes +mainQueue, so deliver via + // GCD's main queue (the bundled gcd_shim provides this on iOS 3). If a real + // addOperationWithBlock: exists (iOS 4+) and a non-main queue was given, + // honor it. + // +[NSOperationQueue mainQueue] is ALSO iOS 4.0 — guard it, or this very + // shim would throw on 3.1.3 the moment the catalog feed answers. + NSOperationQueue *mainQ = [NSOperationQueue respondsToSelector:@selector(mainQueue)] + ? [NSOperationQueue mainQueue] : nil; + if (_queue && mainQ && _queue != mainQ) { + [_queue addOperationWithBlock:^{ completion(resp, data, error); }]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ completion(resp, data, error); }); + } +} + +- (void)connection:(NSURLConnection *)c didReceiveResponse:(NSURLResponse *)response { + [_response release]; + _response = [response retain]; + [_data setLength:0]; +} + +- (void)connection:(NSURLConnection *)c didReceiveData:(NSData *)d { + [_data appendData:d]; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)c { + [self deliverResponse:_response data:_data error:nil]; + [c release]; // balance the +alloc the shim did to keep the connection alive + [self autorelease]; // balance the self-retain +} + +- (void)connection:(NSURLConnection *)c didFailWithError:(NSError *)error { + [self deliverResponse:_response data:nil error:error]; + [c release]; + [self autorelease]; +} + +- (void)dealloc { + [_data release]; + [_response release]; + [_queue release]; + [_completionBox release]; + [super dealloc]; +} + +@end + +static void AppDropSendAsyncRequest(id cls, SEL _cmd, NSURLRequest *request, + NSOperationQueue *queue, + void (^handler)(NSURLResponse *, NSData *, NSError *)) { + // init gives the collector a +1 that we deliberately keep: NSURLConnection + // holds only a weak ref to its delegate, so this +1 is what keeps the + // collector alive for the life of the load. connectionDidFinish/Fail (or the + // start-failure path below) autoreleases it to balance. + ADAsyncURLCollector *collector = + [[ADAsyncURLCollector alloc] initWithQueue:queue completion:handler]; + NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request + delegate:collector + startImmediately:YES]; + if (!conn) { + NSError *err = [NSError errorWithDomain:@"AppDropAsyncURL" code:1 + userInfo:[NSDictionary dictionaryWithObject:@"Cannot start connection" + forKey:NSLocalizedDescriptionKey]]; + [collector deliverResponse:nil data:nil error:err]; + [collector autorelease]; + } +} + +@interface NSURLConnection (AppDropAsyncImpl) @end +@implementation NSURLConnection (AppDropAsyncImpl) ++ (void)load { + if ([NSURLConnection respondsToSelector:@selector(sendAsynchronousRequest:queue:completionHandler:)]) return; + Class meta = object_getClass((id)[NSURLConnection class]); + class_addMethod(meta, @selector(sendAsynchronousRequest:queue:completionHandler:), + (IMP)AppDropSendAsyncRequest, "v@:@@@?"); +} +@end + +#pragma mark - +[NSPropertyListSerialization propertyListWithData:options:format:error:] + +// iOS 4.0 added the class method +// +propertyListWithData:options:format:error: +// On iOS 3.1.3 NSPropertyListSerialization exists but this selector does not, +// so sending it raises: +// *** +[NSPropertyListSerialization propertyListWithData:options:format:error:]: +// unrecognized selector sent to class … +// which is uncaught → terminate → SIGABRT. AppDrop hits this in IPAPackage.m the +// moment it parses a freshly downloaded IPA's Info.plist (install step), so the +// app crashes right as installation begins. (Pre-v3.1 there was no such call.) +// +// The iOS 2.0-era +propertyListFromData:mutabilityOption:format:errorDescription: +// is present on iOS 3 and parses both binary and XML property lists, so we bridge +// straight to it. `format` is an out-param of the same NSPropertyListFormat type +// in both APIs; `error` (NSError**) is mapped from the legacy errorDescription +// (NSString*). No-op on iOS 4+ where the modern selector already exists. +static id AppDropPropertyListWithData(id cls, SEL _cmd, + NSData *data, + NSUInteger opt, + NSPropertyListFormat *fmtOut, + NSError **errOut) { + if (errOut) *errOut = nil; + if (![data length]) { + if (errOut) *errOut = [NSError errorWithDomain:@"AppDropPlist" code:1 + userInfo:[NSDictionary dictionaryWithObject:@"empty data" + forKey:NSLocalizedDescriptionKey]]; + return nil; + } + NSString *errStr = nil; + NSPropertyListFormat fmt = 0; + id plist = [NSPropertyListSerialization propertyListFromData:data + mutabilityOption:(NSPropertyListMutabilityOptions)opt + format:&fmt + errorDescription:&errStr]; + if (fmtOut) *fmtOut = fmt; + if (!plist && errOut) { + *errOut = [NSError errorWithDomain:@"AppDropPlist" code:2 + userInfo:(errStr + ? [NSDictionary dictionaryWithObject:errStr + forKey:NSLocalizedDescriptionKey] + : nil)]; + } + return plist; +} + +@interface NSPropertyListSerialization (AppDropPlistImpl) @end +@implementation NSPropertyListSerialization (AppDropPlistImpl) ++ (void)load { + if ([NSPropertyListSerialization respondsToSelector:@selector(propertyListWithData:options:format:error:)]) return; + Class meta = object_getClass((id)[NSPropertyListSerialization class]); + // "@@:@L^L^@" = returns id; self + SEL + NSData* + NSUInteger(L) + + // NSPropertyListFormat*(^L, pointer to an NSUInteger-backed enum) + NSError**(^@). + class_addMethod(meta, @selector(propertyListWithData:options:format:error:), + (IMP)AppDropPropertyListWithData, "@@:@L^L^@"); +} +@end diff --git a/ios3/compat/cJSON.c b/ios3/compat/cJSON.c new file mode 100644 index 0000000..61483d9 --- /dev/null +++ b/ios3/compat/cJSON.c @@ -0,0 +1,3143 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +/* disable warnings about old C89 functions in MSVC */ +#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) +#define _CRT_SECURE_NO_DEPRECATE +#endif + +#ifdef __GNUC__ +#pragma GCC visibility push(default) +#endif +#if defined(_MSC_VER) +#pragma warning (push) +/* disable warning about single line comments in system headers */ +#pragma warning (disable : 4001) +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_LOCALES +#include +#endif + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif +#ifdef __GNUC__ +#pragma GCC visibility pop +#endif + +#include "cJSON.h" + +/* define our own boolean type */ +#ifdef true +#undef true +#endif +#define true ((cJSON_bool)1) + +#ifdef false +#undef false +#endif +#define false ((cJSON_bool)0) + +/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */ +#ifndef isinf +#define isinf(d) (isnan((d - d)) && !isnan(d)) +#endif +#ifndef isnan +#define isnan(d) (d != d) +#endif + +#ifndef NAN +#ifdef _WIN32 +#define NAN sqrt(-1.0) +#else +#define NAN 0.0/0.0 +#endif +#endif + +typedef struct { + const unsigned char *json; + size_t position; +} error; +static error global_error = { NULL, 0 }; + +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void) +{ + return (const char*) (global_error.json + global_error.position); +} + +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item) +{ + if (!cJSON_IsString(item)) + { + return NULL; + } + + return item->valuestring; +} + +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) +{ + if (!cJSON_IsNumber(item)) + { + return (double) NAN; + } + + return item->valuedouble; +} + +/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */ +#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 18) + #error cJSON.h and cJSON.c have different versions. Make sure that both have the same. +#endif + +CJSON_PUBLIC(const char*) cJSON_Version(void) +{ + static char version[15]; + sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); + + return version; +} + +/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */ +static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2) +{ + if ((string1 == NULL) || (string2 == NULL)) + { + return 1; + } + + if (string1 == string2) + { + return 0; + } + + for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) + { + if (*string1 == '\0') + { + return 0; + } + } + + return tolower(*string1) - tolower(*string2); +} + +typedef struct internal_hooks +{ + void *(CJSON_CDECL *allocate)(size_t size); + void (CJSON_CDECL *deallocate)(void *pointer); + void *(CJSON_CDECL *reallocate)(void *pointer, size_t size); +} internal_hooks; + +#if defined(_MSC_VER) +/* work around MSVC error C2322: '...' address of dllimport '...' is not static */ +static void * CJSON_CDECL internal_malloc(size_t size) +{ + return malloc(size); +} +static void CJSON_CDECL internal_free(void *pointer) +{ + free(pointer); +} +static void * CJSON_CDECL internal_realloc(void *pointer, size_t size) +{ + return realloc(pointer, size); +} +#else +#define internal_malloc malloc +#define internal_free free +#define internal_realloc realloc +#endif + +/* strlen of character literals resolved at compile time */ +#define static_strlen(string_literal) (sizeof(string_literal) - sizeof("")) + +static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc }; + +static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks) +{ + size_t length = 0; + unsigned char *copy = NULL; + + if (string == NULL) + { + return NULL; + } + + length = strlen((const char*)string) + sizeof(""); + copy = (unsigned char*)hooks->allocate(length); + if (copy == NULL) + { + return NULL; + } + memcpy(copy, string, length); + + return copy; +} + +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks) +{ + if (hooks == NULL) + { + /* Reset hooks */ + global_hooks.allocate = malloc; + global_hooks.deallocate = free; + global_hooks.reallocate = realloc; + return; + } + + global_hooks.allocate = malloc; + if (hooks->malloc_fn != NULL) + { + global_hooks.allocate = hooks->malloc_fn; + } + + global_hooks.deallocate = free; + if (hooks->free_fn != NULL) + { + global_hooks.deallocate = hooks->free_fn; + } + + /* use realloc only if both free and malloc are used */ + global_hooks.reallocate = NULL; + if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) + { + global_hooks.reallocate = realloc; + } +} + +/* Internal constructor. */ +static cJSON *cJSON_New_Item(const internal_hooks * const hooks) +{ + cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON)); + if (node) + { + memset(node, '\0', sizeof(cJSON)); + } + + return node; +} + +/* Delete a cJSON structure. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item) +{ + cJSON *next = NULL; + while (item != NULL) + { + next = item->next; + if (!(item->type & cJSON_IsReference) && (item->child != NULL)) + { + cJSON_Delete(item->child); + } + if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) + { + global_hooks.deallocate(item->valuestring); + item->valuestring = NULL; + } + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + global_hooks.deallocate(item->string); + item->string = NULL; + } + global_hooks.deallocate(item); + item = next; + } +} + +/* get the decimal point character of the current locale */ +static unsigned char get_decimal_point(void) +{ +#ifdef ENABLE_LOCALES + struct lconv *lconv = localeconv(); + return (unsigned char) lconv->decimal_point[0]; +#else + return '.'; +#endif +} + +typedef struct +{ + const unsigned char *content; + size_t length; + size_t offset; + size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */ + internal_hooks hooks; +} parse_buffer; + +/* check if the given size is left to read in a given parse buffer (starting with 1) */ +#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length)) +/* check if the buffer can be accessed at the given index (starting with 0) */ +#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length)) +#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index)) +/* get a pointer to the buffer at the position */ +#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset) + +/* Parse the input text to generate a number, and populate the result into item. */ +static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer) +{ + double number = 0; + unsigned char *after_end = NULL; + unsigned char number_c_string[64]; + unsigned char decimal_point = get_decimal_point(); + size_t i = 0; + + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; + } + + /* copy the number into a temporary buffer and replace '.' with the decimal point + * of the current locale (for strtod) + * This also takes care of '\0' not necessarily being available for marking the end of the input */ + for (i = 0; (i < (sizeof(number_c_string) - 1)) && can_access_at_index(input_buffer, i); i++) + { + switch (buffer_at_offset(input_buffer)[i]) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '+': + case '-': + case 'e': + case 'E': + number_c_string[i] = buffer_at_offset(input_buffer)[i]; + break; + + case '.': + number_c_string[i] = decimal_point; + break; + + default: + goto loop_end; + } + } +loop_end: + number_c_string[i] = '\0'; + + number = strtod((const char*)number_c_string, (char**)&after_end); + if (number_c_string == after_end) + { + return false; /* parse_error */ + } + + item->valuedouble = number; + + /* use saturation in case of overflow */ + if (number >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)number; + } + + item->type = cJSON_Number; + + input_buffer->offset += (size_t)(after_end - number_c_string); + return true; +} + +/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number) +{ + if (number >= INT_MAX) + { + object->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + object->valueint = INT_MIN; + } + else + { + object->valueint = (int)number; + } + + return object->valuedouble = number; +} + +/* Note: when passing a NULL valuestring, cJSON_SetValuestring treats this as an error and return NULL */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring) +{ + char *copy = NULL; + /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */ + if ((object == NULL) || !(object->type & cJSON_String) || (object->type & cJSON_IsReference)) + { + return NULL; + } + /* return NULL if the object is corrupted or valuestring is NULL */ + if (object->valuestring == NULL || valuestring == NULL) + { + return NULL; + } + if (strlen(valuestring) <= strlen(object->valuestring)) + { + strcpy(object->valuestring, valuestring); + return object->valuestring; + } + copy = (char*) cJSON_strdup((const unsigned char*)valuestring, &global_hooks); + if (copy == NULL) + { + return NULL; + } + if (object->valuestring != NULL) + { + cJSON_free(object->valuestring); + } + object->valuestring = copy; + + return copy; +} + +typedef struct +{ + unsigned char *buffer; + size_t length; + size_t offset; + size_t depth; /* current nesting depth (for formatted printing) */ + cJSON_bool noalloc; + cJSON_bool format; /* is this print a formatted print */ + internal_hooks hooks; +} printbuffer; + +/* realloc printbuffer if necessary to have at least "needed" bytes more */ +static unsigned char* ensure(printbuffer * const p, size_t needed) +{ + unsigned char *newbuffer = NULL; + size_t newsize = 0; + + if ((p == NULL) || (p->buffer == NULL)) + { + return NULL; + } + + if ((p->length > 0) && (p->offset >= p->length)) + { + /* make sure that offset is valid */ + return NULL; + } + + if (needed > INT_MAX) + { + /* sizes bigger than INT_MAX are currently not supported */ + return NULL; + } + + needed += p->offset + 1; + if (needed <= p->length) + { + return p->buffer + p->offset; + } + + if (p->noalloc) { + return NULL; + } + + /* calculate new buffer size */ + if (needed > (INT_MAX / 2)) + { + /* overflow of int, use INT_MAX if possible */ + if (needed <= INT_MAX) + { + newsize = INT_MAX; + } + else + { + return NULL; + } + } + else + { + newsize = needed * 2; + } + + if (p->hooks.reallocate != NULL) + { + /* reallocate with realloc if available */ + newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize); + if (newbuffer == NULL) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + } + else + { + /* otherwise reallocate manually */ + newbuffer = (unsigned char*)p->hooks.allocate(newsize); + if (!newbuffer) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + + memcpy(newbuffer, p->buffer, p->offset + 1); + p->hooks.deallocate(p->buffer); + } + p->length = newsize; + p->buffer = newbuffer; + + return newbuffer + p->offset; +} + +/* calculate the new length of the string in a printbuffer and update the offset */ +static void update_offset(printbuffer * const buffer) +{ + const unsigned char *buffer_pointer = NULL; + if ((buffer == NULL) || (buffer->buffer == NULL)) + { + return; + } + buffer_pointer = buffer->buffer + buffer->offset; + + buffer->offset += strlen((const char*)buffer_pointer); +} + +/* securely comparison of floating-point variables */ +static cJSON_bool compare_double(double a, double b) +{ + double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); + return (fabs(a - b) <= maxVal * DBL_EPSILON); +} + +/* Render the number nicely from the given item into a string. */ +static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + double d = item->valuedouble; + int length = 0; + size_t i = 0; + unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */ + unsigned char decimal_point = get_decimal_point(); + double test = 0.0; + + if (output_buffer == NULL) + { + return false; + } + + /* This checks for NaN and Infinity */ + if (isnan(d) || isinf(d)) + { + length = sprintf((char*)number_buffer, "null"); + } + else if(d == (double)item->valueint) + { + length = sprintf((char*)number_buffer, "%d", item->valueint); + } + else + { + /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ + length = sprintf((char*)number_buffer, "%1.15g", d); + + /* Check whether the original double can be recovered */ + if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) + { + /* If not, print with 17 decimal places of precision */ + length = sprintf((char*)number_buffer, "%1.17g", d); + } + } + + /* sprintf failed or buffer overrun occurred */ + if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) + { + return false; + } + + /* reserve appropriate space in the output */ + output_pointer = ensure(output_buffer, (size_t)length + sizeof("")); + if (output_pointer == NULL) + { + return false; + } + + /* copy the printed number to the output and replace locale + * dependent decimal point with '.' */ + for (i = 0; i < ((size_t)length); i++) + { + if (number_buffer[i] == decimal_point) + { + output_pointer[i] = '.'; + continue; + } + + output_pointer[i] = number_buffer[i]; + } + output_pointer[i] = '\0'; + + output_buffer->offset += (size_t)length; + + return true; +} + +/* parse 4 digit hexadecimal number */ +static unsigned parse_hex4(const unsigned char * const input) +{ + unsigned int h = 0; + size_t i = 0; + + for (i = 0; i < 4; i++) + { + /* parse digit */ + if ((input[i] >= '0') && (input[i] <= '9')) + { + h += (unsigned int) input[i] - '0'; + } + else if ((input[i] >= 'A') && (input[i] <= 'F')) + { + h += (unsigned int) 10 + input[i] - 'A'; + } + else if ((input[i] >= 'a') && (input[i] <= 'f')) + { + h += (unsigned int) 10 + input[i] - 'a'; + } + else /* invalid */ + { + return 0; + } + + if (i < 3) + { + /* shift left to make place for the next nibble */ + h = h << 4; + } + } + + return h; +} + +/* converts a UTF-16 literal to UTF-8 + * A literal can be one or two sequences of the form \uXXXX */ +static unsigned char utf16_literal_to_utf8(const unsigned char * const input_pointer, const unsigned char * const input_end, unsigned char **output_pointer) +{ + long unsigned int codepoint = 0; + unsigned int first_code = 0; + const unsigned char *first_sequence = input_pointer; + unsigned char utf8_length = 0; + unsigned char utf8_position = 0; + unsigned char sequence_length = 0; + unsigned char first_byte_mark = 0; + + if ((input_end - first_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + /* get the first utf16 sequence */ + first_code = parse_hex4(first_sequence + 2); + + /* check that the code is valid */ + if (((first_code >= 0xDC00) && (first_code <= 0xDFFF))) + { + goto fail; + } + + /* UTF16 surrogate pair */ + if ((first_code >= 0xD800) && (first_code <= 0xDBFF)) + { + const unsigned char *second_sequence = first_sequence + 6; + unsigned int second_code = 0; + sequence_length = 12; /* \uXXXX\uXXXX */ + + if ((input_end - second_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) + { + /* missing second half of the surrogate pair */ + goto fail; + } + + /* get the second utf16 sequence */ + second_code = parse_hex4(second_sequence + 2); + /* check that the code is valid */ + if ((second_code < 0xDC00) || (second_code > 0xDFFF)) + { + /* invalid second half of the surrogate pair */ + goto fail; + } + + + /* calculate the unicode codepoint from the surrogate pair */ + codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF)); + } + else + { + sequence_length = 6; /* \uXXXX */ + codepoint = first_code; + } + + /* encode as UTF-8 + * takes at maximum 4 bytes to encode: + * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + if (codepoint < 0x80) + { + /* normal ascii, encoding 0xxxxxxx */ + utf8_length = 1; + } + else if (codepoint < 0x800) + { + /* two bytes, encoding 110xxxxx 10xxxxxx */ + utf8_length = 2; + first_byte_mark = 0xC0; /* 11000000 */ + } + else if (codepoint < 0x10000) + { + /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */ + utf8_length = 3; + first_byte_mark = 0xE0; /* 11100000 */ + } + else if (codepoint <= 0x10FFFF) + { + /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */ + utf8_length = 4; + first_byte_mark = 0xF0; /* 11110000 */ + } + else + { + /* invalid unicode codepoint */ + goto fail; + } + + /* encode as utf8 */ + for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) + { + /* 10xxxxxx */ + (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF); + codepoint >>= 6; + } + /* encode first byte */ + if (utf8_length > 1) + { + (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF); + } + else + { + (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F); + } + + *output_pointer += utf8_length; + + return sequence_length; + +fail: + return 0; +} + +/* Parse the input text into an unescaped cinput, and populate item. */ +static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer) +{ + const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1; + const unsigned char *input_end = buffer_at_offset(input_buffer) + 1; + unsigned char *output_pointer = NULL; + unsigned char *output = NULL; + + /* not a string */ + if (buffer_at_offset(input_buffer)[0] != '\"') + { + goto fail; + } + + { + /* calculate approximate size of the output (overestimate) */ + size_t allocation_length = 0; + size_t skipped_bytes = 0; + while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"')) + { + /* is escape sequence */ + if (input_end[0] == '\\') + { + if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) + { + /* prevent buffer overflow when last input character is a backslash */ + goto fail; + } + skipped_bytes++; + input_end++; + } + input_end++; + } + if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"')) + { + goto fail; /* string ended unexpectedly */ + } + + /* This is at most how much we need for the output */ + allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes; + output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof("")); + if (output == NULL) + { + goto fail; /* allocation failure */ + } + } + + output_pointer = output; + /* loop through the string literal */ + while (input_pointer < input_end) + { + if (*input_pointer != '\\') + { + *output_pointer++ = *input_pointer++; + } + /* escape sequence */ + else + { + unsigned char sequence_length = 2; + if ((input_end - input_pointer) < 1) + { + goto fail; + } + + switch (input_pointer[1]) + { + case 'b': + *output_pointer++ = '\b'; + break; + case 'f': + *output_pointer++ = '\f'; + break; + case 'n': + *output_pointer++ = '\n'; + break; + case 'r': + *output_pointer++ = '\r'; + break; + case 't': + *output_pointer++ = '\t'; + break; + case '\"': + case '\\': + case '/': + *output_pointer++ = input_pointer[1]; + break; + + /* UTF-16 literal */ + case 'u': + sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer); + if (sequence_length == 0) + { + /* failed to convert UTF16-literal to UTF-8 */ + goto fail; + } + break; + + default: + goto fail; + } + input_pointer += sequence_length; + } + } + + /* zero terminate the output */ + *output_pointer = '\0'; + + item->type = cJSON_String; + item->valuestring = (char*)output; + + input_buffer->offset = (size_t) (input_end - input_buffer->content); + input_buffer->offset++; + + return true; + +fail: + if (output != NULL) + { + input_buffer->hooks.deallocate(output); + output = NULL; + } + + if (input_pointer != NULL) + { + input_buffer->offset = (size_t)(input_pointer - input_buffer->content); + } + + return false; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + output = ensure(output_buffer, sizeof("\"\"")); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + switch (*input_pointer) + { + case '\"': + case '\\': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + /* one character escape sequence */ + escape_characters++; + break; + default: + if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + break; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + output = ensure(output_buffer, output_length + sizeof("\"\"")); + if (output == NULL) + { + return false; + } + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char*)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + +/* Invoke print_string_ptr (which is useful) on an item. */ +static cJSON_bool print_string(const cJSON * const item, printbuffer * const p) +{ + return print_string_ptr((unsigned char*)item->valuestring, p); +} + +/* Predeclare these prototypes. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer); + +/* Utility to jump whitespace and cr/lf */ +static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL)) + { + return NULL; + } + + if (cannot_access_at_index(buffer, 0)) + { + return buffer; + } + + while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) + { + buffer->offset++; + } + + if (buffer->offset == buffer->length) + { + buffer->offset--; + } + + return buffer; +} + +/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */ +static parse_buffer *skip_utf8_bom(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) + { + return NULL; + } + + if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) + { + buffer->offset += 3; + } + + return buffer; +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + size_t buffer_length; + + if (NULL == value) + { + return NULL; + } + + /* Adding null character size due to require_null_terminated. */ + buffer_length = strlen(value) + sizeof(""); + + return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated); +} + +/* Parse an object - create a new root, and populate. */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } }; + cJSON *item = NULL; + + /* reset error position */ + global_error.json = NULL; + global_error.position = 0; + + if (value == NULL || 0 == buffer_length) + { + goto fail; + } + + buffer.content = (const unsigned char*)value; + buffer.length = buffer_length; + buffer.offset = 0; + buffer.hooks = global_hooks; + + item = cJSON_New_Item(&global_hooks); + if (item == NULL) /* memory fail */ + { + goto fail; + } + + if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) + { + /* parse failure. ep is set. */ + goto fail; + } + + /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ + if (require_null_terminated) + { + buffer_skip_whitespace(&buffer); + if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') + { + goto fail; + } + } + if (return_parse_end) + { + *return_parse_end = (const char*)buffer_at_offset(&buffer); + } + + return item; + +fail: + if (item != NULL) + { + cJSON_Delete(item); + } + + if (value != NULL) + { + error local_error; + local_error.json = (const unsigned char*)value; + local_error.position = 0; + + if (buffer.offset < buffer.length) + { + local_error.position = buffer.offset; + } + else if (buffer.length > 0) + { + local_error.position = buffer.length - 1; + } + + if (return_parse_end != NULL) + { + *return_parse_end = (const char*)local_error.json + local_error.position; + } + + global_error = local_error; + } + + return NULL; +} + +/* Default options for cJSON_Parse */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value) +{ + return cJSON_ParseWithOpts(value, 0, 0); +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length) +{ + return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0); +} + +#define cjson_min(a, b) (((a) < (b)) ? (a) : (b)) + +static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks) +{ + static const size_t default_buffer_size = 256; + printbuffer buffer[1]; + unsigned char *printed = NULL; + + memset(buffer, 0, sizeof(buffer)); + + /* create buffer */ + buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size); + buffer->length = default_buffer_size; + buffer->format = format; + buffer->hooks = *hooks; + if (buffer->buffer == NULL) + { + goto fail; + } + + /* print the value */ + if (!print_value(item, buffer)) + { + goto fail; + } + update_offset(buffer); + + /* check if reallocate is available */ + if (hooks->reallocate != NULL) + { + printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1); + if (printed == NULL) { + goto fail; + } + buffer->buffer = NULL; + } + else /* otherwise copy the JSON over to a new buffer */ + { + printed = (unsigned char*) hooks->allocate(buffer->offset + 1); + if (printed == NULL) + { + goto fail; + } + memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1)); + printed[buffer->offset] = '\0'; /* just to be sure */ + + /* free the buffer */ + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + return printed; + +fail: + if (buffer->buffer != NULL) + { + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + if (printed != NULL) + { + hooks->deallocate(printed); + printed = NULL; + } + + return NULL; +} + +/* Render a cJSON item/entity/structure to text. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item) +{ + return (char*)print(item, true, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item) +{ + return (char*)print(item, false, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if (prebuffer < 0) + { + return NULL; + } + + p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer); + if (!p.buffer) + { + return NULL; + } + + p.length = (size_t)prebuffer; + p.offset = 0; + p.noalloc = false; + p.format = fmt; + p.hooks = global_hooks; + + if (!print_value(item, &p)) + { + global_hooks.deallocate(p.buffer); + p.buffer = NULL; + return NULL; + } + + return (char*)p.buffer; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if ((length < 0) || (buffer == NULL)) + { + return false; + } + + p.buffer = (unsigned char*)buffer; + p.length = (size_t)length; + p.offset = 0; + p.noalloc = true; + p.format = format; + p.hooks = global_hooks; + + return print_value(item, &p); +} + +/* Parser core - when encountering text, process appropriately. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer) +{ + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; /* no input */ + } + + /* parse the different types of values */ + /* null */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0)) + { + item->type = cJSON_NULL; + input_buffer->offset += 4; + return true; + } + /* false */ + if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0)) + { + item->type = cJSON_False; + input_buffer->offset += 5; + return true; + } + /* true */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0)) + { + item->type = cJSON_True; + item->valueint = 1; + input_buffer->offset += 4; + return true; + } + /* string */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) + { + return parse_string(item, input_buffer); + } + /* number */ + if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9')))) + { + return parse_number(item, input_buffer); + } + /* array */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) + { + return parse_array(item, input_buffer); + } + /* object */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) + { + return parse_object(item, input_buffer); + } + + return false; +} + +/* Render a value to text. */ +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output = NULL; + + if ((item == NULL) || (output_buffer == NULL)) + { + return false; + } + + switch ((item->type) & 0xFF) + { + case cJSON_NULL: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "null"); + return true; + + case cJSON_False: + output = ensure(output_buffer, 6); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "false"); + return true; + + case cJSON_True: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "true"); + return true; + + case cJSON_Number: + return print_number(item, output_buffer); + + case cJSON_Raw: + { + size_t raw_length = 0; + if (item->valuestring == NULL) + { + return false; + } + + raw_length = strlen(item->valuestring) + sizeof(""); + output = ensure(output_buffer, raw_length); + if (output == NULL) + { + return false; + } + memcpy(output, item->valuestring, raw_length); + return true; + } + + case cJSON_String: + return print_string(item, output_buffer); + + case cJSON_Array: + return print_array(item, output_buffer); + + case cJSON_Object: + return print_object(item, output_buffer); + + default: + return false; + } +} + +/* Build an array from input text. */ +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* head of the linked list */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (buffer_at_offset(input_buffer)[0] != '[') + { + /* not an array */ + goto fail; + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) + { + /* empty array */ + goto success; + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + /* parse next value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') + { + goto fail; /* expected end of array */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Array; + item->child = head; + + input_buffer->offset++; + + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an array to text */ +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_element = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output array. */ + /* opening square bracket */ + output_pointer = ensure(output_buffer, 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer = '['; + output_buffer->offset++; + output_buffer->depth++; + + while (current_element != NULL) + { + if (!print_value(current_element, output_buffer)) + { + return false; + } + update_offset(output_buffer); + if (current_element->next) + { + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ','; + if(output_buffer->format) + { + *output_pointer++ = ' '; + } + *output_pointer = '\0'; + output_buffer->offset += length; + } + current_element = current_element->next; + } + + output_pointer = ensure(output_buffer, 2); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ']'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Build an object from the text. */ +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* linked list head */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) + { + goto fail; /* not an object */ + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) + { + goto success; /* empty object */ + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + if (cannot_access_at_index(input_buffer, 1)) + { + goto fail; /* nothing comes after the comma */ + } + + /* parse the name of the child */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_string(current_item, input_buffer)) + { + goto fail; /* failed to parse name */ + } + buffer_skip_whitespace(input_buffer); + + /* swap valuestring and string, because we parsed the name */ + current_item->string = current_item->valuestring; + current_item->valuestring = NULL; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) + { + goto fail; /* invalid object */ + } + + /* parse the value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) + { + goto fail; /* expected end of object */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Object; + item->child = head; + + input_buffer->offset++; + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an object to text. */ +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_item = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output: */ + length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */ + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer++ = '{'; + output_buffer->depth++; + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + output_buffer->offset += length; + + while (current_item) + { + if (output_buffer->format) + { + size_t i; + output_pointer = ensure(output_buffer, output_buffer->depth); + if (output_pointer == NULL) + { + return false; + } + for (i = 0; i < output_buffer->depth; i++) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += output_buffer->depth; + } + + /* print key */ + if (!print_string_ptr((unsigned char*)current_item->string, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ':'; + if (output_buffer->format) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += length; + + /* print value */ + if (!print_value(current_item, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + /* print comma if not last */ + length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0)); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + if (current_item->next) + { + *output_pointer++ = ','; + } + + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + *output_pointer = '\0'; + output_buffer->offset += length; + + current_item = current_item->next; + } + + output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2); + if (output_pointer == NULL) + { + return false; + } + if (output_buffer->format) + { + size_t i; + for (i = 0; i < (output_buffer->depth - 1); i++) + { + *output_pointer++ = '\t'; + } + } + *output_pointer++ = '}'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Get Array size/item / object item. */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array) +{ + cJSON *child = NULL; + size_t size = 0; + + if (array == NULL) + { + return 0; + } + + child = array->child; + + while(child != NULL) + { + size++; + child = child->next; + } + + /* FIXME: Can overflow here. Cannot be fixed without breaking the API */ + + return (int)size; +} + +static cJSON* get_array_item(const cJSON *array, size_t index) +{ + cJSON *current_child = NULL; + + if (array == NULL) + { + return NULL; + } + + current_child = array->child; + while ((current_child != NULL) && (index > 0)) + { + index--; + current_child = current_child->next; + } + + return current_child; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index) +{ + if (index < 0) + { + return NULL; + } + + return get_array_item(array, (size_t)index); +} + +static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive) +{ + cJSON *current_element = NULL; + + if ((object == NULL) || (name == NULL)) + { + return NULL; + } + + current_element = object->child; + if (case_sensitive) + { + while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) + { + current_element = current_element->next; + } + } + else + { + while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0)) + { + current_element = current_element->next; + } + } + + if ((current_element == NULL) || (current_element->string == NULL)) { + return NULL; + } + + return current_element; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, false); +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string) +{ + return cJSON_GetObjectItem(object, string) ? 1 : 0; +} + +/* Utility for array list handling. */ +static void suffix_object(cJSON *prev, cJSON *item) +{ + prev->next = item; + item->prev = prev; +} + +/* Utility for handling references. */ +static cJSON *create_reference(const cJSON *item, const internal_hooks * const hooks) +{ + cJSON *reference = NULL; + if (item == NULL) + { + return NULL; + } + + reference = cJSON_New_Item(hooks); + if (reference == NULL) + { + return NULL; + } + + memcpy(reference, item, sizeof(cJSON)); + reference->string = NULL; + reference->type |= cJSON_IsReference; + reference->next = reference->prev = NULL; + return reference; +} + +static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) +{ + cJSON *child = NULL; + + if ((item == NULL) || (array == NULL) || (array == item)) + { + return false; + } + + child = array->child; + /* + * To find the last item in array quickly, we use prev in array + */ + if (child == NULL) + { + /* list is empty, start new one */ + array->child = item; + item->prev = item; + item->next = NULL; + } + else + { + /* append to the end */ + if (child->prev) + { + suffix_object(child->prev, item); + array->child->prev = item; + } + } + + return true; +} + +/* Add item to array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item) +{ + return add_item_to_array(array, item); +} + +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic push +#endif +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif +/* helper function to cast away const */ +static void* cast_away_const(const void* string) +{ + return (void*)string; +} +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic pop +#endif + + +static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key) +{ + char *new_key = NULL; + int new_type = cJSON_Invalid; + + if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) + { + return false; + } + + if (constant_key) + { + new_key = (char*)cast_away_const(string); + new_type = item->type | cJSON_StringIsConst; + } + else + { + new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks); + if (new_key == NULL) + { + return false; + } + + new_type = item->type & ~cJSON_StringIsConst; + } + + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + hooks->deallocate(item->string); + } + + item->string = new_key; + item->type = new_type; + + return add_item_to_array(object, item); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, false); +} + +/* Add an item to an object with constant string as key */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) +{ + if (array == NULL) + { + return false; + } + + return add_item_to_array(array, create_reference(item, &global_hooks)); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item) +{ + if ((object == NULL) || (string == NULL)) + { + return false; + } + + return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false); +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name) +{ + cJSON *null = cJSON_CreateNull(); + if (add_item_to_object(object, name, null, &global_hooks, false)) + { + return null; + } + + cJSON_Delete(null); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name) +{ + cJSON *true_item = cJSON_CreateTrue(); + if (add_item_to_object(object, name, true_item, &global_hooks, false)) + { + return true_item; + } + + cJSON_Delete(true_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name) +{ + cJSON *false_item = cJSON_CreateFalse(); + if (add_item_to_object(object, name, false_item, &global_hooks, false)) + { + return false_item; + } + + cJSON_Delete(false_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean) +{ + cJSON *bool_item = cJSON_CreateBool(boolean); + if (add_item_to_object(object, name, bool_item, &global_hooks, false)) + { + return bool_item; + } + + cJSON_Delete(bool_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number) +{ + cJSON *number_item = cJSON_CreateNumber(number); + if (add_item_to_object(object, name, number_item, &global_hooks, false)) + { + return number_item; + } + + cJSON_Delete(number_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string) +{ + cJSON *string_item = cJSON_CreateString(string); + if (add_item_to_object(object, name, string_item, &global_hooks, false)) + { + return string_item; + } + + cJSON_Delete(string_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw) +{ + cJSON *raw_item = cJSON_CreateRaw(raw); + if (add_item_to_object(object, name, raw_item, &global_hooks, false)) + { + return raw_item; + } + + cJSON_Delete(raw_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name) +{ + cJSON *object_item = cJSON_CreateObject(); + if (add_item_to_object(object, name, object_item, &global_hooks, false)) + { + return object_item; + } + + cJSON_Delete(object_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name) +{ + cJSON *array = cJSON_CreateArray(); + if (add_item_to_object(object, name, array, &global_hooks, false)) + { + return array; + } + + cJSON_Delete(array); + return NULL; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item) +{ + if ((parent == NULL) || (item == NULL)) + { + return NULL; + } + + if (item != parent->child) + { + /* not the first element */ + item->prev->next = item->next; + } + if (item->next != NULL) + { + /* not the last element */ + item->next->prev = item->prev; + } + + if (item == parent->child) + { + /* first element */ + parent->child = item->next; + } + else if (item->next == NULL) + { + /* last element */ + parent->child->prev = item->prev; + } + + /* make sure the detached item doesn't point anywhere anymore */ + item->prev = NULL; + item->next = NULL; + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which) +{ + if (which < 0) + { + return NULL; + } + + return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which) +{ + cJSON_Delete(cJSON_DetachItemFromArray(array, which)); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItem(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObject(object, string)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string)); +} + +/* Replace array/object items with new ones. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem) +{ + cJSON *after_inserted = NULL; + + if (which < 0 || newitem == NULL) + { + return false; + } + + after_inserted = get_array_item(array, (size_t)which); + if (after_inserted == NULL) + { + return add_item_to_array(array, newitem); + } + + if (after_inserted != array->child && after_inserted->prev == NULL) { + /* return false if after_inserted is a corrupted array item */ + return false; + } + + newitem->next = after_inserted; + newitem->prev = after_inserted->prev; + after_inserted->prev = newitem; + if (after_inserted == array->child) + { + array->child = newitem; + } + else + { + newitem->prev->next = newitem; + } + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement) +{ + if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL)) + { + return false; + } + + if (replacement == item) + { + return true; + } + + replacement->next = item->next; + replacement->prev = item->prev; + + if (replacement->next != NULL) + { + replacement->next->prev = replacement; + } + if (parent->child == item) + { + if (parent->child->prev == parent->child) + { + replacement->prev = replacement; + } + parent->child = replacement; + } + else + { /* + * To find the last item in array quickly, we use prev in array. + * We can't modify the last item's next pointer where this item was the parent's child + */ + if (replacement->prev != NULL) + { + replacement->prev->next = replacement; + } + if (replacement->next == NULL) + { + parent->child->prev = replacement; + } + } + + item->next = NULL; + item->prev = NULL; + cJSON_Delete(item); + + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem) +{ + if (which < 0) + { + return false; + } + + return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem); +} + +static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive) +{ + if ((replacement == NULL) || (string == NULL)) + { + return false; + } + + /* replace the name in the replacement */ + if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) + { + cJSON_free(replacement->string); + } + replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if (replacement->string == NULL) + { + return false; + } + + replacement->type &= ~cJSON_StringIsConst; + + return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, false); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, true); +} + +/* Create basic types: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_NULL; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_True; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = boolean ? cJSON_True : cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Number; + item->valuedouble = num; + + /* use saturation in case of overflow */ + if (num >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (num <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)num; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_String; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) + { + item->type = cJSON_String | cJSON_IsReference; + item->valuestring = (char*)cast_away_const(string); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Object | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) { + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Array | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Raw; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type=cJSON_Array; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Object; + } + + return item; +} + +/* Create Arrays: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber((double)numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (strings == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateString(strings[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p,n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +/* Duplication */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse) +{ + cJSON *newitem = NULL; + cJSON *child = NULL; + cJSON *next = NULL; + cJSON *newchild = NULL; + + /* Bail on bad ptr */ + if (!item) + { + goto fail; + } + /* Create new item */ + newitem = cJSON_New_Item(&global_hooks); + if (!newitem) + { + goto fail; + } + /* Copy over all vars */ + newitem->type = item->type & (~cJSON_IsReference); + newitem->valueint = item->valueint; + newitem->valuedouble = item->valuedouble; + if (item->valuestring) + { + newitem->valuestring = (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks); + if (!newitem->valuestring) + { + goto fail; + } + } + if (item->string) + { + newitem->string = (item->type&cJSON_StringIsConst) ? item->string : (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks); + if (!newitem->string) + { + goto fail; + } + } + /* If non-recursive, then we're done! */ + if (!recurse) + { + return newitem; + } + /* Walk the ->next chain for the child. */ + child = item->child; + while (child != NULL) + { + newchild = cJSON_Duplicate(child, true); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) + { + goto fail; + } + if (next != NULL) + { + /* If newitem->child already set, then crosswire ->prev and ->next and move on */ + next->next = newchild; + newchild->prev = next; + next = newchild; + } + else + { + /* Set newitem->child and move to it */ + newitem->child = newchild; + next = newchild; + } + child = child->next; + } + if (newitem && newitem->child) + { + newitem->child->prev = newchild; + } + + return newitem; + +fail: + if (newitem != NULL) + { + cJSON_Delete(newitem); + } + + return NULL; +} + +static void skip_oneline_comment(char **input) +{ + *input += static_strlen("//"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if ((*input)[0] == '\n') { + *input += static_strlen("\n"); + return; + } + } +} + +static void skip_multiline_comment(char **input) +{ + *input += static_strlen("/*"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if (((*input)[0] == '*') && ((*input)[1] == '/')) + { + *input += static_strlen("*/"); + return; + } + } +} + +static void minify_string(char **input, char **output) { + (*output)[0] = (*input)[0]; + *input += static_strlen("\""); + *output += static_strlen("\""); + + + for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) { + (*output)[0] = (*input)[0]; + + if ((*input)[0] == '\"') { + (*output)[0] = '\"'; + *input += static_strlen("\""); + *output += static_strlen("\""); + return; + } else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) { + (*output)[1] = (*input)[1]; + *input += static_strlen("\""); + *output += static_strlen("\""); + } + } +} + +CJSON_PUBLIC(void) cJSON_Minify(char *json) +{ + char *into = json; + + if (json == NULL) + { + return; + } + + while (json[0] != '\0') + { + switch (json[0]) + { + case ' ': + case '\t': + case '\r': + case '\n': + json++; + break; + + case '/': + if (json[1] == '/') + { + skip_oneline_comment(&json); + } + else if (json[1] == '*') + { + skip_multiline_comment(&json); + } else { + json++; + } + break; + + case '\"': + minify_string(&json, (char**)&into); + break; + + default: + into[0] = json[0]; + json++; + into++; + } + } + + /* and null-terminate. */ + *into = '\0'; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Invalid; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_False; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xff) == cJSON_True; +} + + +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & (cJSON_True | cJSON_False)) != 0; +} +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_NULL; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Number; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_String; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Array; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Object; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Raw; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) +{ + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) + { + return false; + } + + /* check if type is valid */ + switch (a->type & 0xFF) + { + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + case cJSON_Number: + case cJSON_String: + case cJSON_Raw: + case cJSON_Array: + case cJSON_Object: + break; + + default: + return false; + } + + /* identical objects are equal */ + if (a == b) + { + return true; + } + + switch (a->type & 0xFF) + { + /* in these cases and equal type is enough */ + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + return true; + + case cJSON_Number: + if (compare_double(a->valuedouble, b->valuedouble)) + { + return true; + } + return false; + + case cJSON_String: + case cJSON_Raw: + if ((a->valuestring == NULL) || (b->valuestring == NULL)) + { + return false; + } + if (strcmp(a->valuestring, b->valuestring) == 0) + { + return true; + } + + return false; + + case cJSON_Array: + { + cJSON *a_element = a->child; + cJSON *b_element = b->child; + + for (; (a_element != NULL) && (b_element != NULL);) + { + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + + a_element = a_element->next; + b_element = b_element->next; + } + + /* one of the arrays is longer than the other */ + if (a_element != b_element) { + return false; + } + + return true; + } + + case cJSON_Object: + { + cJSON *a_element = NULL; + cJSON *b_element = NULL; + cJSON_ArrayForEach(a_element, a) + { + /* TODO This has O(n^2) runtime, which is horrible! */ + b_element = get_object_item(b, a_element->string, case_sensitive); + if (b_element == NULL) + { + return false; + } + + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + } + + /* doing this twice, once on a and b to prevent true comparison if a subset of b + * TODO: Do this the proper way, this is just a fix for now */ + cJSON_ArrayForEach(b_element, b) + { + a_element = get_object_item(a, b_element->string, case_sensitive); + if (a_element == NULL) + { + return false; + } + + if (!cJSON_Compare(b_element, a_element, case_sensitive)) + { + return false; + } + } + + return true; + } + + default: + return false; + } +} + +CJSON_PUBLIC(void *) cJSON_malloc(size_t size) +{ + return global_hooks.allocate(size); +} + +CJSON_PUBLIC(void) cJSON_free(void *object) +{ + global_hooks.deallocate(object); + object = NULL; +} diff --git a/ios3/compat/cJSON.h b/ios3/compat/cJSON.h new file mode 100644 index 0000000..88cf0bc --- /dev/null +++ b/ios3/compat/cJSON.h @@ -0,0 +1,300 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON__h +#define cJSON__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32)) +#define __WINDOWS__ +#endif + +#ifdef __WINDOWS__ + +/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options: + +CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols +CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default) +CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol + +For *nix builds that support visibility attribute, you can define similar behavior by + +setting default visibility to hidden by adding +-fvisibility=hidden (for gcc) +or +-xldscope=hidden (for sun cc) +to CFLAGS + +then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does + +*/ + +#define CJSON_CDECL __cdecl +#define CJSON_STDCALL __stdcall + +/* export symbols by default, this is necessary for copy pasting the C and header file */ +#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_EXPORT_SYMBOLS +#endif + +#if defined(CJSON_HIDE_SYMBOLS) +#define CJSON_PUBLIC(type) type CJSON_STDCALL +#elif defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL +#elif defined(CJSON_IMPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL +#endif +#else /* !__WINDOWS__ */ +#define CJSON_CDECL +#define CJSON_STDCALL + +#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY) +#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type +#else +#define CJSON_PUBLIC(type) type +#endif +#endif + +/* project version */ +#define CJSON_VERSION_MAJOR 1 +#define CJSON_VERSION_MINOR 7 +#define CJSON_VERSION_PATCH 18 + +#include + +/* cJSON Types: */ +#define cJSON_Invalid (0) +#define cJSON_False (1 << 0) +#define cJSON_True (1 << 1) +#define cJSON_NULL (1 << 2) +#define cJSON_Number (1 << 3) +#define cJSON_String (1 << 4) +#define cJSON_Array (1 << 5) +#define cJSON_Object (1 << 6) +#define cJSON_Raw (1 << 7) /* raw json */ + +#define cJSON_IsReference 256 +#define cJSON_StringIsConst 512 + +/* The cJSON structure: */ +typedef struct cJSON +{ + /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ + struct cJSON *next; + struct cJSON *prev; + /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ + struct cJSON *child; + + /* The type of the item, as above. */ + int type; + + /* The item's string, if type==cJSON_String and type == cJSON_Raw */ + char *valuestring; + /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ + int valueint; + /* The item's number, if type==cJSON_Number */ + double valuedouble; + + /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ + char *string; +} cJSON; + +typedef struct cJSON_Hooks +{ + /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */ + void *(CJSON_CDECL *malloc_fn)(size_t sz); + void (CJSON_CDECL *free_fn)(void *ptr); +} cJSON_Hooks; + +typedef int cJSON_bool; + +/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_NESTING_LIMIT +#define CJSON_NESTING_LIMIT 1000 +#endif + +/* returns the version of cJSON as a string */ +CJSON_PUBLIC(const char*) cJSON_Version(void); + +/* Supply malloc, realloc and free functions to cJSON */ +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks); + +/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ +/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length); +/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ +/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); + +/* Render a cJSON entity to text for transfer/storage. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item); +/* Render a cJSON entity to text for transfer/storage without any formatting. */ +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item); +/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */ +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt); +/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */ +/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */ +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format); +/* Delete a cJSON entity and all subentities. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item); + +/* Returns the number of items in an array (or object). */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array); +/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */ +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index); +/* Get item "string" from object. Case insensitive. */ +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string); +/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void); + +/* Check item type and return its value */ +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item); +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item); + +/* These functions check the type of an item */ +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item); + +/* These calls create a cJSON item of the appropriate type. */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean); +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num); +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string); +/* raw json */ +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw); +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void); + +/* Create a string where valuestring references a string so + * it will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string); +/* Create an object/array that only references it's elements so + * they will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child); +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child); + +/* These utilities create an Array of count items. + * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count); + +/* Append item to the specified array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); +/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object. + * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before + * writing to `item->string` */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item); +/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item); + +/* Remove/Detach items from Arrays/Objects. */ +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string); + +/* Update array items. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */ +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem); + +/* Duplicate a cJSON item */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); +/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will + * need to be released. With recurse!=0, it will duplicate any children connected to the item. + * The item->next and ->prev pointers are always zero on return from Duplicate. */ +/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. + * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive); + +/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. + * The input pointer json cannot point to a read-only address area, such as a string constant, + * but should point to a readable and writable address area. */ +CJSON_PUBLIC(void) cJSON_Minify(char *json); + +/* Helper functions for creating and adding items to an object at the same time. + * They return the added item or NULL on failure. */ +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean); +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number); +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string); +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw); +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name); + +/* When assigning an integer value, it needs to be propagated to valuedouble too. */ +#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number)) +/* helper for the cJSON_SetNumberValue macro */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number); +#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number)) +/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring); + +/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/ +#define cJSON_SetBoolValue(object, boolValue) ( \ + (object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \ + (object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \ + cJSON_Invalid\ +) + +/* Macro for iterating over an array or object */ +#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) + +/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ +CJSON_PUBLIC(void *) cJSON_malloc(size_t size); +CJSON_PUBLIC(void) cJSON_free(void *object); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ios3/compat/mbed_platform_compat.c b/ios3/compat/mbed_platform_compat.c new file mode 100644 index 0000000..6357a3d --- /dev/null +++ b/ios3/compat/mbed_platform_compat.c @@ -0,0 +1,30 @@ +// mbedtls platform_util replacements that avoid clock_gettime/CLOCK_MONOTONIC, +// which do not exist on iOS 3.1. Built instead of mbedtls library/platform_util.c. +#include +#include +#include +#include +#include + +typedef int64_t mbedtls_ms_time_t; + +void mbedtls_platform_zeroize(void *buf, size_t len) { + if (buf != NULL && len > 0) { + volatile unsigned char *p = (volatile unsigned char *)buf; + while (len--) *p++ = 0; + } +} + +void mbedtls_zeroize_and_free(void *buf, size_t len) { + if (buf != NULL) { mbedtls_platform_zeroize(buf, len); free(buf); } +} + +struct tm *mbedtls_platform_gmtime_r(const time_t *tt, struct tm *tm_buf) { + return gmtime_r(tt, tm_buf); +} + +mbedtls_ms_time_t mbedtls_ms_time(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + return (mbedtls_ms_time_t)tv.tv_sec * 1000 + tv.tv_usec / 1000; +} diff --git a/ios3/compat/shim/blocks/Block.h b/ios3/compat/shim/blocks/Block.h new file mode 100644 index 0000000..55cdd01 --- /dev/null +++ b/ios3/compat/shim/blocks/Block.h @@ -0,0 +1,59 @@ +/* + * Block.h + * + * Copyright 2008-2010 Apple, Inc. Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#ifndef _BLOCK_H_ +#define _BLOCK_H_ + +#if !defined(BLOCK_EXPORT) +# if defined(__cplusplus) +# define BLOCK_EXPORT extern "C" +# else +# define BLOCK_EXPORT extern +# endif +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +/* Create a heap based copy of a Block or simply add a reference to an existing one. + * This must be paired with Block_release to recover memory, even when running + * under Objective-C Garbage Collection. + */ +BLOCK_EXPORT void *_Block_copy(const void *aBlock); + +/* Lose the reference, and if heap based and last reference, recover the memory. */ +BLOCK_EXPORT void _Block_release(const void *aBlock); + +#if defined(__cplusplus) +} +#endif + +/* Type correct macros. */ + +#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__))) +#define Block_release(...) _Block_release((const void *)(__VA_ARGS__)) + + +#endif diff --git a/ios3/compat/shim/blocks/Block_private.h b/ios3/compat/shim/blocks/Block_private.h new file mode 100644 index 0000000..8ae8218 --- /dev/null +++ b/ios3/compat/shim/blocks/Block_private.h @@ -0,0 +1,179 @@ +/* + * Block_private.h + * + * Copyright 2008-2010 Apple, Inc. Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#ifndef _BLOCK_PRIVATE_H_ +#define _BLOCK_PRIVATE_H_ + +#if !defined(BLOCK_EXPORT) +# if defined(__cplusplus) +# define BLOCK_EXPORT extern "C" +# else +# define BLOCK_EXPORT extern +# endif +#endif + +#ifndef _MSC_VER +#include +#else +/* MSVC doesn't have . Compensate. */ +typedef char bool; +#define true (bool)1 +#define false (bool)0 +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + + +enum { + BLOCK_REFCOUNT_MASK = (0xffff), + BLOCK_NEEDS_FREE = (1 << 24), + BLOCK_HAS_COPY_DISPOSE = (1 << 25), + BLOCK_HAS_CTOR = (1 << 26), /* Helpers have C++ code. */ + BLOCK_IS_GC = (1 << 27), + BLOCK_IS_GLOBAL = (1 << 28), + BLOCK_HAS_DESCRIPTOR = (1 << 29) +}; + + +/* Revised new layout. */ +struct Block_descriptor { + unsigned long int reserved; + unsigned long int size; + void (*copy)(void *dst, void *src); + void (*dispose)(void *); +}; + + +struct Block_layout { + void *isa; + int flags; + int reserved; + void (*invoke)(void *, ...); + struct Block_descriptor *descriptor; + /* Imported variables. */ +}; + + +struct Block_byref { + void *isa; + struct Block_byref *forwarding; + int flags; /* refcount; */ + int size; + void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src); + void (*byref_destroy)(struct Block_byref *); + /* long shared[0]; */ +}; + + +struct Block_byref_header { + void *isa; + struct Block_byref *forwarding; + int flags; + int size; +}; + + +/* Runtime support functions used by compiler when generating copy/dispose helpers. */ + +enum { + /* See function implementation for a more complete description of these fields and combinations */ + BLOCK_FIELD_IS_OBJECT = 3, /* id, NSObject, __attribute__((NSObject)), block, ... */ + BLOCK_FIELD_IS_BLOCK = 7, /* a block variable */ + BLOCK_FIELD_IS_BYREF = 8, /* the on stack structure holding the __block variable */ + BLOCK_FIELD_IS_WEAK = 16, /* declared __weak, only used in byref copy helpers */ + BLOCK_BYREF_CALLER = 128 /* called from __block (byref) copy/dispose support routines. */ +}; + +/* Runtime entry point called by compiler when assigning objects inside copy helper routines */ +BLOCK_EXPORT void _Block_object_assign(void *destAddr, const void *object, const int flags); + /* BLOCK_FIELD_IS_BYREF is only used from within block copy helpers */ + + +/* runtime entry point called by the compiler when disposing of objects inside dispose helper routine */ +BLOCK_EXPORT void _Block_object_dispose(const void *object, const int flags); + + + +/* Other support functions */ + +/* Runtime entry to get total size of a closure */ +BLOCK_EXPORT unsigned long int Block_size(void *block_basic); + + + +/* the raw data space for runtime classes for blocks */ +/* class+meta used for stack, malloc, and collectable based blocks */ +BLOCK_EXPORT void * _NSConcreteStackBlock[32]; +BLOCK_EXPORT void * _NSConcreteMallocBlock[32]; +BLOCK_EXPORT void * _NSConcreteAutoBlock[32]; +BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32]; +BLOCK_EXPORT void * _NSConcreteGlobalBlock[32]; +BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32]; + + +/* the intercept routines that must be used under GC */ +BLOCK_EXPORT void _Block_use_GC( void *(*alloc)(const unsigned long, const bool isOne, const bool isObject), + void (*setHasRefcount)(const void *, const bool), + void (*gc_assign_strong)(void *, void **), + void (*gc_assign_weak)(const void *, void *), + void (*gc_memmove)(void *, void *, unsigned long)); + +/* earlier version, now simply transitional */ +BLOCK_EXPORT void _Block_use_GC5( void *(*alloc)(const unsigned long, const bool isOne, const bool isObject), + void (*setHasRefcount)(const void *, const bool), + void (*gc_assign_strong)(void *, void **), + void (*gc_assign_weak)(const void *, void *)); + +BLOCK_EXPORT void _Block_use_RR( void (*retain)(const void *), + void (*release)(const void *)); + +/* make a collectable GC heap based Block. Not useful under non-GC. */ +BLOCK_EXPORT void *_Block_copy_collectable(const void *aBlock); + +/* thread-unsafe diagnostic */ +BLOCK_EXPORT const char *_Block_dump(const void *block); + + +/* Obsolete */ + +/* first layout */ +struct Block_basic { + void *isa; + int Block_flags; /* int32_t */ + int Block_size; /* XXX should be packed into Block_flags */ + void (*Block_invoke)(void *); + void (*Block_copy)(void *dst, void *src); /* iff BLOCK_HAS_COPY_DISPOSE */ + void (*Block_dispose)(void *); /* iff BLOCK_HAS_COPY_DISPOSE */ + /* long params[0]; // where const imports, __block storage references, etc. get laid down */ +}; + + +#if defined(__cplusplus) +} +#endif + + +#endif /* _BLOCK_PRIVATE_H_ */ diff --git a/ios3/compat/shim/blocks/config.h b/ios3/compat/shim/blocks/config.h new file mode 100644 index 0000000..f338567 --- /dev/null +++ b/ios3/compat/shim/blocks/config.h @@ -0,0 +1,2 @@ +#define HAVE_OBJC 1 +#define HAVE_LIBDISPATCH 0 diff --git a/ios3/compat/shim/blocks/data.c b/ios3/compat/shim/blocks/data.c new file mode 100644 index 0000000..b4eb02e --- /dev/null +++ b/ios3/compat/shim/blocks/data.c @@ -0,0 +1,41 @@ +/* + * data.c + * + * Copyright 2008-2010 Apple, Inc. Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +/******************** +NSBlock support + +We allocate space and export a symbol to be used as the Class for the on-stack and malloc'ed copies until ObjC arrives on the scene. These data areas are set up by Foundation to link in as real classes post facto. + +We keep these in a separate file so that we can include the runtime code in test subprojects but not include the data so that compiled code that sees the data in libSystem doesn't get confused by a second copy. Somehow these don't get unified in a common block. +**********************/ + +void * _NSConcreteStackBlock[32] = { 0 }; +void * _NSConcreteMallocBlock[32] = { 0 }; +void * _NSConcreteAutoBlock[32] = { 0 }; +void * _NSConcreteFinalizingBlock[32] = { 0 }; +void * _NSConcreteGlobalBlock[32] = { 0 }; +void * _NSConcreteWeakBlockVariable[32] = { 0 }; + +void _Block_copy_error(void) { +} diff --git a/ios3/compat/shim/blocks/runtime.c b/ios3/compat/shim/blocks/runtime.c new file mode 100644 index 0000000..ed3fa66 --- /dev/null +++ b/ios3/compat/shim/blocks/runtime.c @@ -0,0 +1,712 @@ +/* + * runtime.c + * + * Copyright 2008-2010 Apple, Inc. Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#include "Block_private.h" +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#ifdef HAVE_AVAILABILITY_MACROS_H +#include +#endif /* HAVE_AVAILABILITY_MACROS_H */ + +#ifdef HAVE_TARGET_CONDITIONALS_H +#include +#endif /* HAVE_TARGET_CONDITIONALS_H */ + +#if defined(HAVE_OSATOMIC_COMPARE_AND_SWAP_INT) && defined(HAVE_OSATOMIC_COMPARE_AND_SWAP_LONG) + +#ifdef HAVE_LIBKERN_OSATOMIC_H +#include +#endif /* HAVE_LIBKERN_OSATOMIC_H */ + +#elif defined(__WIN32__) || defined(_WIN32) +#define _CRT_SECURE_NO_WARNINGS 1 +#include + +static __inline bool OSAtomicCompareAndSwapLong(long oldl, long newl, long volatile *dst) { + /* fixme barrier is overkill -- see objc-os.h */ + long original = InterlockedCompareExchange(dst, newl, oldl); + return (original == oldl); +} + +static __inline bool OSAtomicCompareAndSwapInt(int oldi, int newi, int volatile *dst) { + /* fixme barrier is overkill -- see objc-os.h */ + int original = InterlockedCompareExchange(dst, newi, oldi); + return (original == oldi); +} + +/* + * Check to see if the GCC atomic built-ins are available. If we're on + * a 64-bit system, make sure we have an 8-byte atomic function + * available. + * + */ + +#elif defined(HAVE_SYNC_BOOL_COMPARE_AND_SWAP_INT) && defined(HAVE_SYNC_BOOL_COMPARE_AND_SWAP_LONG) + +static __inline bool OSAtomicCompareAndSwapLong(long oldl, long newl, long volatile *dst) { + return __sync_bool_compare_and_swap(dst, oldl, newl); +} + +static __inline bool OSAtomicCompareAndSwapInt(int oldi, int newi, int volatile *dst) { + return __sync_bool_compare_and_swap(dst, oldi, newi); +} + +#else +#error unknown atomic compare-and-swap primitive +#endif /* HAVE_OSATOMIC_COMPARE_AND_SWAP_INT && HAVE_OSATOMIC_COMPARE_AND_SWAP_LONG */ + + +/* + * Globals: + */ + +static void *_Block_copy_class = _NSConcreteMallocBlock; +static void *_Block_copy_finalizing_class = _NSConcreteMallocBlock; +static int _Block_copy_flag = BLOCK_NEEDS_FREE; +static int _Byref_flag_initial_value = BLOCK_NEEDS_FREE | 2; + +static const int WANTS_ONE = (1 << 16); + +static bool isGC = false; + +/* + * Internal Utilities: + */ + +#if 0 +static unsigned long int latching_incr_long(unsigned long int *where) { + while (1) { + unsigned long int old_value = *(volatile unsigned long int *)where; + if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) { + return BLOCK_REFCOUNT_MASK; + } + if (OSAtomicCompareAndSwapLong(old_value, old_value+1, (volatile long int *)where)) { + return old_value+1; + } + } +} +#endif /* if 0 */ + +static int latching_incr_int(int *where) { + while (1) { + int old_value = *(volatile int *)where; + if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) { + return BLOCK_REFCOUNT_MASK; + } + if (OSAtomicCompareAndSwapInt(old_value, old_value+1, (volatile int *)where)) { + return old_value+1; + } + } +} + +#if 0 +static int latching_decr_long(unsigned long int *where) { + while (1) { + unsigned long int old_value = *(volatile int *)where; + if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) { + return BLOCK_REFCOUNT_MASK; + } + if ((old_value & BLOCK_REFCOUNT_MASK) == 0) { + return 0; + } + if (OSAtomicCompareAndSwapLong(old_value, old_value-1, (volatile long int *)where)) { + return old_value-1; + } + } +} +#endif /* if 0 */ + +static int latching_decr_int(int *where) { + while (1) { + int old_value = *(volatile int *)where; + if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) { + return BLOCK_REFCOUNT_MASK; + } + if ((old_value & BLOCK_REFCOUNT_MASK) == 0) { + return 0; + } + if (OSAtomicCompareAndSwapInt(old_value, old_value-1, (volatile int *)where)) { + return old_value-1; + } + } +} + + +/* + * GC support stub routines: + */ +#if 0 +#pragma mark GC Support Routines +#endif /* if 0 */ + + +static void *_Block_alloc_default(const unsigned long size, const bool initialCountIsOne, const bool isObject) { + return malloc(size); +} + +static void _Block_assign_default(void *value, void **destptr) { + *destptr = value; +} + +static void _Block_setHasRefcount_default(const void *ptr, const bool hasRefcount) { +} + +static void _Block_do_nothing(const void *aBlock) { } + +static void _Block_retain_object_default(const void *ptr) { + if (!ptr) return; + // On a stock iOS the objc runtime calls _Block_use_RR(objc_retain, + // objc_release) at startup so Block_copy retains captured objects. This + // static shim is never handed those callbacks, so we must send -retain + // ourselves; otherwise objects captured by a block are not retained on + // copy and are dead by the time the block runs (EXC_BAD_ACCESS in + // objc_msgSend on the main thread when a dispatch_async block fires). + id (*msg)(id, SEL) = (id (*)(id, SEL))objc_msgSend; + msg((id)ptr, sel_registerName("retain")); +} + +static void _Block_release_object_default(const void *ptr) { + if (!ptr) return; + void (*msg)(id, SEL) = (void (*)(id, SEL))objc_msgSend; + msg((id)ptr, sel_registerName("release")); +} + +static void _Block_assign_weak_default(const void *ptr, void *dest) { + *(void **)dest = (void *)ptr; +} + +static void _Block_memmove_default(void *dst, void *src, unsigned long size) { + memmove(dst, src, (size_t)size); +} + +static void _Block_memmove_gc_broken(void *dest, void *src, unsigned long size) { + void **destp = (void **)dest; + void **srcp = (void **)src; + while (size) { + _Block_assign_default(*srcp, destp); + destp++; + srcp++; + size -= sizeof(void *); + } +} + +/* + * GC support callout functions - initially set to stub routines: + */ + +static void *(*_Block_allocator)(const unsigned long, const bool isOne, const bool isObject) = _Block_alloc_default; +static void (*_Block_deallocator)(const void *) = (void (*)(const void *))free; +static void (*_Block_assign)(void *value, void **destptr) = _Block_assign_default; +static void (*_Block_setHasRefcount)(const void *ptr, const bool hasRefcount) = _Block_setHasRefcount_default; +static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default; +static void (*_Block_release_object)(const void *ptr) = _Block_release_object_default; +static void (*_Block_assign_weak)(const void *dest, void *ptr) = _Block_assign_weak_default; +static void (*_Block_memmove)(void *dest, void *src, unsigned long size) = _Block_memmove_default; + + +/* + * GC support SPI functions - called from ObjC runtime and CoreFoundation: + */ + +/* Public SPI + * Called from objc-auto to turn on GC. + * version 3, 4 arg, but changed 1st arg + */ +void _Block_use_GC( void *(*alloc)(const unsigned long, const bool isOne, const bool isObject), + void (*setHasRefcount)(const void *, const bool), + void (*gc_assign)(void *, void **), + void (*gc_assign_weak)(const void *, void *), + void (*gc_memmove)(void *, void *, unsigned long)) { + + isGC = true; + _Block_allocator = alloc; + _Block_deallocator = _Block_do_nothing; + _Block_assign = gc_assign; + _Block_copy_flag = BLOCK_IS_GC; + _Block_copy_class = _NSConcreteAutoBlock; + /* blocks with ctors & dtors need to have the dtor run from a class with a finalizer */ + _Block_copy_finalizing_class = _NSConcreteFinalizingBlock; + _Block_setHasRefcount = setHasRefcount; + _Byref_flag_initial_value = BLOCK_IS_GC; // no refcount + _Block_retain_object = _Block_do_nothing; + _Block_release_object = _Block_do_nothing; + _Block_assign_weak = gc_assign_weak; + _Block_memmove = gc_memmove; +} + +/* transitional */ +void _Block_use_GC5( void *(*alloc)(const unsigned long, const bool isOne, const bool isObject), + void (*setHasRefcount)(const void *, const bool), + void (*gc_assign)(void *, void **), + void (*gc_assign_weak)(const void *, void *)) { + /* until objc calls _Block_use_GC it will call us; supply a broken internal memmove implementation until then */ + _Block_use_GC(alloc, setHasRefcount, gc_assign, gc_assign_weak, _Block_memmove_gc_broken); +} + + +/* + * Called from objc-auto to alternatively turn on retain/release. + * Prior to this the only "object" support we can provide is for those + * super special objects that live in libSystem, namely dispatch queues. + * Blocks and Block_byrefs have their own special entry points. + * + */ +void _Block_use_RR( void (*retain)(const void *), + void (*release)(const void *)) { + _Block_retain_object = retain; + _Block_release_object = release; +} + +/* + * Internal Support routines for copying: + */ + +#if 0 +#pragma mark Copy/Release support +#endif /* if 0 */ + +/* Copy, or bump refcount, of a block. If really copying, call the copy helper if present. */ +static void *_Block_copy_internal(const void *arg, const int flags) { + struct Block_layout *aBlock; + const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE; + + //printf("_Block_copy_internal(%p, %x)\n", arg, flags); + if (!arg) return NULL; + + + // The following would be better done as a switch statement + aBlock = (struct Block_layout *)arg; + if (aBlock->flags & BLOCK_NEEDS_FREE) { + // latches on high + latching_incr_int(&aBlock->flags); + return aBlock; + } + else if (aBlock->flags & BLOCK_IS_GC) { + // GC refcounting is expensive so do most refcounting here. + if (wantsOne && ((latching_incr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK) == 1)) { + // Tell collector to hang on this - it will bump the GC refcount version + _Block_setHasRefcount(aBlock, true); + } + return aBlock; + } + else if (aBlock->flags & BLOCK_IS_GLOBAL) { + return aBlock; + } + + // Its a stack block. Make a copy. + if (!isGC) { + struct Block_layout *result = malloc(aBlock->descriptor->size); + if (!result) return (void *)0; + memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first + // reset refcount + result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed + result->flags |= BLOCK_NEEDS_FREE | 1; + result->isa = _NSConcreteMallocBlock; + if (result->flags & BLOCK_HAS_COPY_DISPOSE) { + //printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock); + (*aBlock->descriptor->copy)(result, aBlock); // do fixup + } + return result; + } + else { + // Under GC want allocation with refcount 1 so we ask for "true" if wantsOne + // This allows the copy helper routines to make non-refcounted block copies under GC + unsigned long int flags = aBlock->flags; + bool hasCTOR = (flags & BLOCK_HAS_CTOR) != 0; + struct Block_layout *result = _Block_allocator(aBlock->descriptor->size, wantsOne, hasCTOR); + if (!result) return (void *)0; + memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first + // reset refcount + // if we copy a malloc block to a GC block then we need to clear NEEDS_FREE. + flags &= ~(BLOCK_NEEDS_FREE|BLOCK_REFCOUNT_MASK); // XXX not needed + if (wantsOne) + flags |= BLOCK_IS_GC | 1; + else + flags |= BLOCK_IS_GC; + result->flags = flags; + if (flags & BLOCK_HAS_COPY_DISPOSE) { + //printf("calling block copy helper...\n"); + (*aBlock->descriptor->copy)(result, aBlock); // do fixup + } + if (hasCTOR) { + result->isa = _NSConcreteFinalizingBlock; + } + else { + result->isa = _NSConcreteAutoBlock; + } + return result; + } +} + + +/* + * Runtime entry points for maintaining the sharing knowledge of byref data blocks. + * + * A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data + * Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr. + * We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment it. + * Otherwise we need to copy it and update the stack forwarding pointer + * XXX We need to account for weak/nonretained read-write barriers. + */ + +static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) { + struct Block_byref **destp = (struct Block_byref **)dest; + struct Block_byref *src = (struct Block_byref *)arg; + + //printf("_Block_byref_assign_copy called, byref destp %p, src %p, flags %x\n", destp, src, flags); + //printf("src dump: %s\n", _Block_byref_dump(src)); + if (src->forwarding->flags & BLOCK_IS_GC) { + ; // don't need to do any more work + } + else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) { + //printf("making copy\n"); + // src points to stack + bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)); + // if its weak ask for an object (only matters under GC) + struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak); + copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack + copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier) + src->forwarding = copy; // patch stack to point to heap copy + copy->size = src->size; + if (isWeak) { + copy->isa = &_NSConcreteWeakBlockVariable; // mark isa field so it gets weak scanning + } + if (src->flags & BLOCK_HAS_COPY_DISPOSE) { + // Trust copy helper to copy everything of interest + // If more than one field shows up in a byref block this is wrong XXX + copy->byref_keep = src->byref_keep; + copy->byref_destroy = src->byref_destroy; + (*src->byref_keep)(copy, src); + } + else { + // just bits. Blast 'em using _Block_memmove in case they're __strong + _Block_memmove( + (void *)©->byref_keep, + (void *)&src->byref_keep, + src->size - sizeof(struct Block_byref_header)); + } + } + // already copied to heap + else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) { + latching_incr_int(&src->forwarding->flags); + } + // assign byref data block pointer into new Block + _Block_assign(src->forwarding, (void **)destp); +} + +// Old compiler SPI +static void _Block_byref_release(const void *arg) { + struct Block_byref *shared_struct = (struct Block_byref *)arg; + int refcount; + + // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?) + shared_struct = shared_struct->forwarding; + + //printf("_Block_byref_release %p called, flags are %x\n", shared_struct, shared_struct->flags); + // To support C++ destructors under GC we arrange for there to be a finalizer for this + // by using an isa that directs the code to a finalizer that calls the byref_destroy method. + if ((shared_struct->flags & BLOCK_NEEDS_FREE) == 0) { + return; // stack or GC or global + } + refcount = shared_struct->flags & BLOCK_REFCOUNT_MASK; + if (refcount <= 0) { + printf("_Block_byref_release: Block byref data structure at %p underflowed\n", arg); + } + else if ((latching_decr_int(&shared_struct->flags) & BLOCK_REFCOUNT_MASK) == 0) { + //printf("disposing of heap based byref block\n"); + if (shared_struct->flags & BLOCK_HAS_COPY_DISPOSE) { + //printf("calling out to helper\n"); + (*shared_struct->byref_destroy)(shared_struct); + } + _Block_deallocator((struct Block_layout *)shared_struct); + } +} + + +/* + * + * API supporting SPI + * _Block_copy, _Block_release, and (old) _Block_destroy + * + */ + +#if 0 +#pragma mark SPI/API +#endif /* if 0 */ + +void *_Block_copy(const void *arg) { + return _Block_copy_internal(arg, WANTS_ONE); +} + + +// API entry point to release a copied Block +void _Block_release(void *arg) { + struct Block_layout *aBlock = (struct Block_layout *)arg; + int32_t newCount; + if (!aBlock) return; + newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK; + if (newCount > 0) return; + // Hit zero + if (aBlock->flags & BLOCK_IS_GC) { + // Tell GC we no longer have our own refcounts. GC will decr its refcount + // and unless someone has done a CFRetain or marked it uncollectable it will + // now be subject to GC reclamation. + _Block_setHasRefcount(aBlock, false); + } + else if (aBlock->flags & BLOCK_NEEDS_FREE) { + if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock); + _Block_deallocator(aBlock); + } + else if (aBlock->flags & BLOCK_IS_GLOBAL) { + ; + } + else { + printf("Block_release called upon a stack Block: %p, ignored\n", (void *)aBlock); + } +} + + + +// Old Compiler SPI point to release a copied Block used by the compiler in dispose helpers +static void _Block_destroy(const void *arg) { + struct Block_layout *aBlock; + if (!arg) return; + aBlock = (struct Block_layout *)arg; + if (aBlock->flags & BLOCK_IS_GC) { + // assert(aBlock->Block_flags & BLOCK_HAS_CTOR); + return; // ignore, we are being called because of a DTOR + } + _Block_release(aBlock); +} + + + +/* + * + * SPI used by other layers + * + */ + +// SPI, also internal. Called from NSAutoBlock only under GC +void *_Block_copy_collectable(const void *aBlock) { + return _Block_copy_internal(aBlock, 0); +} + + +// SPI +unsigned long int Block_size(void *arg) { + return ((struct Block_layout *)arg)->descriptor->size; +} + + +#if 0 +#pragma mark Compiler SPI entry points +#endif /* if 0 */ + + +/******************************************************* + +Entry points used by the compiler - the real API! + + +A Block can reference four different kinds of things that require help when the Block is copied to the heap. +1) C++ stack based objects +2) References to Objective-C objects +3) Other Blocks +4) __block variables + +In these cases helper functions are synthesized by the compiler for use in Block_copy and Block_release, called the copy and dispose helpers. The copy helper emits a call to the C++ const copy constructor for C++ stack based objects and for the rest calls into the runtime support function _Block_object_assign. The dispose helper has a call to the C++ destructor for case 1 and a call into _Block_object_dispose for the rest. + +The flags parameter of _Block_object_assign and _Block_object_dispose is set to + * BLOCK_FIELD_IS_OBJECT (3), for the case of an Objective-C Object, + * BLOCK_FIELD_IS_BLOCK (7), for the case of another Block, and + * BLOCK_FIELD_IS_BYREF (8), for the case of a __block variable. +If the __block variable is marked weak the compiler also or's in BLOCK_FIELD_IS_WEAK (16). + +So the Block copy/dispose helpers should only ever generate the four flag values of 3, 7, 8, and 24. + +When a __block variable is either a C++ object, an Objective-C object, or another Block then the compiler also generates copy/dispose helper functions. Similarly to the Block copy helper, the "__block" copy helper (formerly and still a.k.a. "byref" copy helper) will do a C++ copy constructor (not a const one though!) and the dispose helper will do the destructor. And similarly the helpers will call into the same two support functions with the same values for objects and Blocks with the additional BLOCK_BYREF_CALLER (128) bit of information supplied. + +So the __block copy/dispose helpers will generate flag values of 3 or 7 for objects and Blocks respectively, with BLOCK_FIELD_IS_WEAK (16) or'ed as appropriate and always 128 or'd in, for the following set of possibilities: + __block id 128+3 + __weak block id 128+3+16 + __block (^Block) 128+7 + __weak __block (^Block) 128+7+16 + +The implementation of the two routines would be improved by switch statements enumerating the eight cases. + +********************************************************/ + +/* + * When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point + * to do the assignment. + */ +void _Block_object_assign(void *destAddr, const void *object, const int flags) { + //printf("_Block_object_assign(*%p, %p, %x)\n", destAddr, object, flags); + if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) { + if ((flags & BLOCK_FIELD_IS_WEAK) == BLOCK_FIELD_IS_WEAK) { + _Block_assign_weak(object, destAddr); + } + else { + // do *not* retain or *copy* __block variables whatever they are + _Block_assign((void *)object, destAddr); + } + } + else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF) { + // copying a __block reference from the stack Block to the heap + // flags will indicate if it holds a __weak reference and needs a special isa + _Block_byref_assign_copy(destAddr, object, flags); + } + // (this test must be before next one) + else if ((flags & BLOCK_FIELD_IS_BLOCK) == BLOCK_FIELD_IS_BLOCK) { + // copying a Block declared variable from the stack Block to the heap + _Block_assign(_Block_copy_internal(object, flags), destAddr); + } + // (this test must be after previous one) + else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) { + //printf("retaining object at %p\n", object); + _Block_retain_object(object); + //printf("done retaining object at %p\n", object); + _Block_assign((void *)object, destAddr); + } +} + +// When Blocks or Block_byrefs hold objects their destroy helper routines call this entry point +// to help dispose of the contents +// Used initially only for __attribute__((NSObject)) marked pointers. +void _Block_object_dispose(const void *object, const int flags) { + //printf("_Block_object_dispose(%p, %x)\n", object, flags); + if (flags & BLOCK_FIELD_IS_BYREF) { + // get rid of the __block data structure held in a Block + _Block_byref_release(object); + } + else if ((flags & (BLOCK_FIELD_IS_BLOCK|BLOCK_BYREF_CALLER)) == BLOCK_FIELD_IS_BLOCK) { + // get rid of a referenced Block held by this Block + // (ignore __block Block variables, compiler doesn't need to call us) + _Block_destroy(object); + } + else if ((flags & (BLOCK_FIELD_IS_WEAK|BLOCK_FIELD_IS_BLOCK|BLOCK_BYREF_CALLER)) == BLOCK_FIELD_IS_OBJECT) { + // get rid of a referenced object held by this Block + // (ignore __block object variables, compiler doesn't need to call us) + _Block_release_object(object); + } +} + + +/* + * Debugging support: + */ +#if 0 +#pragma mark Debugging +#endif /* if 0 */ + + +const char *_Block_dump(const void *block) { + struct Block_layout *closure = (struct Block_layout *)block; + static char buffer[512]; + char *cp = buffer; + if (closure == NULL) { + sprintf(cp, "NULL passed to _Block_dump\n"); + return buffer; + } + if (! (closure->flags & BLOCK_HAS_DESCRIPTOR)) { + printf("Block compiled by obsolete compiler, please recompile source for this Block\n"); + exit(1); + } + cp += sprintf(cp, "^%p (new layout) =\n", (void *)closure); + if (closure->isa == NULL) { + cp += sprintf(cp, "isa: NULL\n"); + } + else if (closure->isa == _NSConcreteStackBlock) { + cp += sprintf(cp, "isa: stack Block\n"); + } + else if (closure->isa == _NSConcreteMallocBlock) { + cp += sprintf(cp, "isa: malloc heap Block\n"); + } + else if (closure->isa == _NSConcreteAutoBlock) { + cp += sprintf(cp, "isa: GC heap Block\n"); + } + else if (closure->isa == _NSConcreteGlobalBlock) { + cp += sprintf(cp, "isa: global Block\n"); + } + else if (closure->isa == _NSConcreteFinalizingBlock) { + cp += sprintf(cp, "isa: finalizing Block\n"); + } + else { + cp += sprintf(cp, "isa?: %p\n", (void *)closure->isa); + } + cp += sprintf(cp, "flags:"); + if (closure->flags & BLOCK_HAS_DESCRIPTOR) { + cp += sprintf(cp, " HASDESCRIPTOR"); + } + if (closure->flags & BLOCK_NEEDS_FREE) { + cp += sprintf(cp, " FREEME"); + } + if (closure->flags & BLOCK_IS_GC) { + cp += sprintf(cp, " ISGC"); + } + if (closure->flags & BLOCK_HAS_COPY_DISPOSE) { + cp += sprintf(cp, " HASHELP"); + } + if (closure->flags & BLOCK_HAS_CTOR) { + cp += sprintf(cp, " HASCTOR"); + } + cp += sprintf(cp, "\nrefcount: %u\n", closure->flags & BLOCK_REFCOUNT_MASK); + cp += sprintf(cp, "invoke: %p\n", (void *)(uintptr_t)closure->invoke); + { + struct Block_descriptor *dp = closure->descriptor; + cp += sprintf(cp, "descriptor: %p\n", (void *)dp); + cp += sprintf(cp, "descriptor->reserved: %lu\n", dp->reserved); + cp += sprintf(cp, "descriptor->size: %lu\n", dp->size); + + if (closure->flags & BLOCK_HAS_COPY_DISPOSE) { + cp += sprintf(cp, "descriptor->copy helper: %p\n", (void *)(uintptr_t)dp->copy); + cp += sprintf(cp, "descriptor->dispose helper: %p\n", (void *)(uintptr_t)dp->dispose); + } + } + return buffer; +} + + +const char *_Block_byref_dump(struct Block_byref *src) { + static char buffer[256]; + char *cp = buffer; + cp += sprintf(cp, "byref data block %p contents:\n", (void *)src); + cp += sprintf(cp, " forwarding: %p\n", (void *)src->forwarding); + cp += sprintf(cp, " flags: 0x%x\n", src->flags); + cp += sprintf(cp, " size: %d\n", src->size); + if (src->flags & BLOCK_HAS_COPY_DISPOSE) { + cp += sprintf(cp, " copy helper: %p\n", (void *)(uintptr_t)src->byref_keep); + cp += sprintf(cp, " dispose helper: %p\n", (void *)(uintptr_t)src->byref_destroy); + } + return buffer; +} + diff --git a/ios3/compat/shim/gcd_mainq.c b/ios3/compat/shim/gcd_mainq.c new file mode 100644 index 0000000..6e8cc5c --- /dev/null +++ b/ios3/compat/shim/gcd_mainq.c @@ -0,0 +1 @@ +__attribute__((aligned(16))) char _dispatch_main_q[256]; diff --git a/ios3/compat/shim/gcd_shim.c b/ios3/compat/shim/gcd_shim.c new file mode 100644 index 0000000..f3f21b6 --- /dev/null +++ b/ios3/compat/shim/gcd_shim.c @@ -0,0 +1,438 @@ +// gcd_shim.c — minimal libdispatch (GCD) implementation for iOS 3.x, which +// ships no libdispatch at all. Backed by pthreads + a CFRunLoop hop for the +// main queue. Covers exactly the GCD surface AppDrop uses: +// dispatch_async / dispatch_after +// dispatch_get_global_queue / dispatch_queue_create / dispatch_get_main_queue +// dispatch_once +// dispatch_group_{create,enter,leave,notify,async} +// dispatch_semaphore_{create,signal,wait} +// dispatch_time / dispatch_walltime +// +// NOTE: this is intentionally small. Queues created with dispatch_queue_create +// are treated as concurrent (each async spawns a detached pthread). If AppDrop +// relies anywhere on a *serial* queue for ordering, that queue needs a real +// FIFO+worker; see README open items. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "blocks/Block.h" + +// --------------------------------------------------------------------------- +// Per-thread autorelease pool (iOS 3 / MRC correctness) +// --------------------------------------------------------------------------- +// Every block this shim runs on a *spawned* pthread (the detached trampoline, +// the serial worker, the dispatch_after thread, group async/notify threads) +// executes Objective-C code that autoreleases Foundation objects (NSURL, +// NSData, NSString, file ops, JSON parsing, SQLite row wrappers, …). On iOS 3 +// under MRC there is NO implicit per-thread pool: only the main thread's +// CFRunLoop wraps each iteration in one. A worker thread with no +// NSAutoreleasePool floods the log with +// *** _NSAutoreleaseNoPool(): Object 0x… autoreleased with no pool in place +// - just leaking +// and, once enough objects pile up / a leaked-then-touched object is messaged, +// faults in objc_msgSend with EXC_BAD_ACCESS (the ~2s-after-launch crash on +// Thread 3). Wrapping each worker block in a real NSAutoreleasePool fixes both +// the leak storm and the crash. This file is C, so the pool is driven through +// the objc runtime C API (NSAutoreleasePool is iOS 2.0, always present). +static void *ad_pool_push(void) { + Class cls = objc_getClass("NSAutoreleasePool"); + if (!cls) return NULL; + id (*msg)(id, SEL) = (id (*)(id, SEL))objc_msgSend; + id pool = msg((id)cls, sel_registerName("alloc")); + pool = msg(pool, sel_registerName("init")); + return (void *)pool; +} +static void ad_pool_pop(void *pool) { + if (!pool) return; + void (*msg)(id, SEL) = (void (*)(id, SEL))objc_msgSend; + msg((id)pool, sel_registerName("release")); +} + +// Run an Objective-C block wrapped in its own NSAutoreleasePool. Used for every +// block executed on a shim-spawned worker thread (which otherwise has no pool). +static void ad_invoke(dispatch_block_t b) { + void *pool = ad_pool_push(); + b(); + ad_pool_pop(pool); +} + +#undef dispatch_once +#undef dispatch_once_f + +// --------------------------------------------------------------------------- +// Queues +// --------------------------------------------------------------------------- +// Three kinds of queue exist in this shim: +// * the main queue -> work hops onto the main CFRunLoop (UIKit-safe) +// * the global queue -> concurrent: every async spawns a detached thread +// * dispatch_queue_create -> a REAL FIFO serial queue (single worker thread), +// unless created DISPATCH_QUEUE_CONCURRENT. +// The serial path matters: LocalCatalog.m relies on its _searchQueue +// serializing every SQLite query on one db handle (no locking otherwise). The +// old shim ran those as concurrent detached threads, which would race the +// single sqlite3* — corrupting results or crashing on a real iOS 3 device. + +static char _global_q_storage[64]; + +#define AD_QUEUE_MAGIC 0x51444551u // 'QDEQ' + +typedef struct ad_node { + dispatch_block_t b; // already Block_copy'd + struct ad_node *next; +} ad_node; + +typedef struct { + unsigned int magic; // AD_QUEUE_MAGIC sentinel + int concurrent;// 1 = concurrent, 0 = serial FIFO + pthread_mutex_t m; + pthread_cond_t cv; + ad_node *head, *tail; + int started; // worker thread launched? + pthread_t worker; +} ad_queue; + +dispatch_queue_t dispatch_get_global_queue(long pri, unsigned long flags) { + (void)pri; (void)flags; + return (dispatch_queue_t)_global_q_storage; +} + +dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr) { + (void)label; + ad_queue *q = (ad_queue *)calloc(1, sizeof(ad_queue)); + q->magic = AD_QUEUE_MAGIC; + // libdispatch: DISPATCH_QUEUE_SERIAL is NULL; anything else (e.g. + // DISPATCH_QUEUE_CONCURRENT) means concurrent. AppDrop only asks for SERIAL. + q->concurrent = (attr != NULL) ? 1 : 0; + pthread_mutex_init(&q->m, NULL); + pthread_cond_init(&q->cv, NULL); + return (dispatch_queue_t)q; +} + +static void *trampoline(void *ctx) { + dispatch_block_t b = (dispatch_block_t)ctx; + ad_invoke(b); + Block_release(b); + return NULL; +} + +// run_on_main: schedule `b` (already Block_copy'd; we own it) to run on the +// main thread's run loop. iOS 3.1.3 has NO CFRunLoopPerformBlock (that symbol +// first appears in iOS 4.0 / CoreFoundation 550), so using it makes dyld abort +// the process with "Symbol not found: _CFRunLoopPerformBlock" at first call. +// Instead we use a one-shot CFRunLoopTimer (available since iOS 2.0) with an +// immediate fire date: CF retains the timer while it's scheduled, fires it on +// the next main-loop pass, we run+release the block, then invalidate. +static void ad_main_timer_cb(CFRunLoopTimerRef timer, void *info) { + dispatch_block_t b = (dispatch_block_t)info; + if (b) { b(); Block_release(b); } + CFRunLoopTimerInvalidate(timer); +} + +static void run_on_main(dispatch_block_t b) { + CFRunLoopRef rl = CFRunLoopGetMain(); + CFRunLoopTimerContext ctx = { 0, (void *)b, NULL, NULL, NULL }; + CFRunLoopTimerRef t = CFRunLoopTimerCreate( + kCFAllocatorDefault, + CFAbsoluteTimeGetCurrent(), // fire immediately on next loop pass + 0, // non-repeating + 0, 0, + ad_main_timer_cb, + &ctx); + if (!t) { if (b) { ad_invoke(b); Block_release(b); } return; } // fallback: run inline + CFRunLoopAddTimer(rl, t, kCFRunLoopCommonModes); + CFRelease(t); // run loop keeps it alive until it fires + CFRunLoopWakeUp(rl); +} + +static void run_detached(dispatch_block_t b) { + pthread_t t; + pthread_attr_t at; + pthread_attr_init(&at); + pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED); + if (pthread_create(&t, &at, trampoline, b) != 0) { ad_invoke(b); Block_release(b); } + pthread_attr_destroy(&at); +} + +// Is this pointer a queue made by dispatch_queue_create (vs main/global storage)? +static ad_queue *as_created_queue(dispatch_queue_t q) { + if (!q) return NULL; + if (q == dispatch_get_main_queue()) return NULL; + if (q == (dispatch_queue_t)_global_q_storage) return NULL; + ad_queue *aq = (ad_queue *)q; + return (aq->magic == AD_QUEUE_MAGIC) ? aq : NULL; +} + +// Serial worker: drains the FIFO in order, one block at a time, forever. +static void *serial_worker(void *ctx) { + ad_queue *q = (ad_queue *)ctx; + for (;;) { + pthread_mutex_lock(&q->m); + while (q->head == NULL) pthread_cond_wait(&q->cv, &q->m); + ad_node *n = q->head; + q->head = n->next; + if (q->head == NULL) q->tail = NULL; + pthread_mutex_unlock(&q->m); + + ad_invoke(n->b); + Block_release(n->b); + free(n); + } + return NULL; +} + +// Enqueue onto a created queue: concurrent -> detached thread; serial -> FIFO. +static void queue_enqueue(ad_queue *q, dispatch_block_t b /* owned */) { + if (q->concurrent) { run_detached(b); return; } + ad_node *n = (ad_node *)malloc(sizeof(ad_node)); + n->b = b; + n->next = NULL; + pthread_mutex_lock(&q->m); + if (q->tail) q->tail->next = n; else q->head = n; + q->tail = n; + if (!q->started) { + q->started = 1; + if (pthread_create(&q->worker, NULL, serial_worker, q) != 0) { + // Couldn't spawn worker — drain inline as a last resort. + q->started = 0; + ad_node *cur = q->head; q->head = q->tail = NULL; + pthread_mutex_unlock(&q->m); + while (cur) { ad_node *nx = cur->next; ad_invoke(cur->b); Block_release(cur->b); free(cur); cur = nx; } + return; + } + } + pthread_cond_signal(&q->cv); + pthread_mutex_unlock(&q->m); +} + +void dispatch_async(dispatch_queue_t q, dispatch_block_t block) { + dispatch_block_t b = Block_copy(block); + if (q == dispatch_get_main_queue()) { run_on_main(b); return; } + ad_queue *aq = as_created_queue(q); + if (aq) { queue_enqueue(aq, b); return; } + run_detached(b); // global / unknown -> concurrent +} + +void dispatch_sync(dispatch_queue_t q, dispatch_block_t block) { + (void)q; + block(); // executes inline on the calling thread (FIFO ordering preserved + // because the caller blocks; AppDrop does not use dispatch_sync) +} + +// --------------------------------------------------------------------------- +// dispatch_once +// --------------------------------------------------------------------------- +void dispatch_once(dispatch_once_t *pred, dispatch_block_t block) { + static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&m); + if (*pred != ~0l) { block(); *pred = ~0l; } + pthread_mutex_unlock(&m); +} + +// --------------------------------------------------------------------------- +// Time +// --------------------------------------------------------------------------- +dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta) { + struct timeval tv; + gettimeofday(&tv, NULL); + uint64_t now = (uint64_t)tv.tv_sec * 1000000000ull + (uint64_t)tv.tv_usec * 1000ull; + if (when == DISPATCH_TIME_NOW) when = now; + int64_t r = (int64_t)when + delta; + return (dispatch_time_t)(r < 0 ? 0 : r); +} + +dispatch_time_t dispatch_walltime(const struct timespec *w, int64_t delta) { + struct timeval tv; + gettimeofday(&tv, NULL); + uint64_t base = w ? ((uint64_t)w->tv_sec * 1000000000ull + (uint64_t)w->tv_nsec) + : ((uint64_t)tv.tv_sec * 1000000000ull + (uint64_t)tv.tv_usec * 1000ull); + int64_t r = (int64_t)base + delta; + return (dispatch_time_t)(r < 0 ? 0 : r); +} + +typedef struct { dispatch_time_t when; dispatch_block_t b; int is_main; } after_ctx; +static void *after_thread(void *p) { + after_ctx *c = (after_ctx *)p; + struct timeval tv; + gettimeofday(&tv, NULL); + uint64_t now = (uint64_t)tv.tv_sec * 1000000000ull + (uint64_t)tv.tv_usec * 1000ull; + if (c->when > now) { + uint64_t ns = c->when - now; + struct timespec ts; + ts.tv_sec = (time_t)(ns / 1000000000ull); + ts.tv_nsec = (long)(ns % 1000000000ull); + nanosleep(&ts, NULL); + } + if (c->is_main) { + // Hop the final call onto the main run loop. run_on_main takes ownership + // of the block (it releases it after the one-shot timer fires), so we do + // NOT release it here. + run_on_main(c->b); + } else { + ad_invoke(c->b); + Block_release(c->b); + } + free(c); + return NULL; +} + +void dispatch_after(dispatch_time_t when, dispatch_queue_t q, dispatch_block_t block) { + dispatch_block_t b = Block_copy(block); + after_ctx *c = (after_ctx *)malloc(sizeof(after_ctx)); + c->when = when; + c->b = b; // we hand this single ref off to after_thread + c->is_main = (q == dispatch_get_main_queue()); + pthread_t t; pthread_attr_t at; pthread_attr_init(&at); + pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED); + if (pthread_create(&t, &at, after_thread, c) != 0) { + if (c->is_main) run_on_main(c->b); else { ad_invoke(c->b); Block_release(c->b); } + free(c); + } + pthread_attr_destroy(&at); +} + +// --------------------------------------------------------------------------- +// Groups (counter + condvar) +// --------------------------------------------------------------------------- +typedef struct { + pthread_mutex_t m; + pthread_cond_t cv; + long count; +} grp_t; + +dispatch_group_t dispatch_group_create(void) { + grp_t *g = (grp_t *)calloc(1, sizeof(grp_t)); + pthread_mutex_init(&g->m, NULL); + pthread_cond_init(&g->cv, NULL); + g->count = 0; + return (dispatch_group_t)g; +} + +void dispatch_group_enter(dispatch_group_t group) { + grp_t *g = (grp_t *)group; + pthread_mutex_lock(&g->m); + g->count++; + pthread_mutex_unlock(&g->m); +} + +void dispatch_group_leave(dispatch_group_t group) { + grp_t *g = (grp_t *)group; + pthread_mutex_lock(&g->m); + if (--g->count <= 0) pthread_cond_broadcast(&g->cv); + pthread_mutex_unlock(&g->m); +} + +long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout) { + grp_t *g = (grp_t *)group; + pthread_mutex_lock(&g->m); + while (g->count > 0) { + if (timeout == DISPATCH_TIME_FOREVER) { + pthread_cond_wait(&g->cv, &g->m); + } else { + struct timespec ts; + ts.tv_sec = (time_t)(timeout / 1000000000ull); + ts.tv_nsec = (long)(timeout % 1000000000ull); + if (pthread_cond_timedwait(&g->cv, &g->m, &ts) != 0) break; + } + } + long r = g->count; + pthread_mutex_unlock(&g->m); + return r; // 0 = all done, non-zero = timed out +} + +typedef struct { grp_t *g; dispatch_queue_t q; dispatch_block_t b; int is_main; } gasync_ctx; +static void *group_async_thread(void *p) { + gasync_ctx *c = (gasync_ctx *)p; + ad_invoke(c->b); + Block_release(c->b); + dispatch_group_leave((dispatch_group_t)c->g); + free(c); + return NULL; +} + +void dispatch_group_async(dispatch_group_t group, dispatch_queue_t q, dispatch_block_t block) { + dispatch_group_enter(group); + gasync_ctx *c = (gasync_ctx *)malloc(sizeof(gasync_ctx)); + c->g = (grp_t *)group; c->q = q; c->b = Block_copy(block); + pthread_t t; pthread_attr_t at; pthread_attr_init(&at); + pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED); + if (pthread_create(&t, &at, group_async_thread, c) != 0) { + ad_invoke(c->b); Block_release(c->b); dispatch_group_leave(group); free(c); + } + pthread_attr_destroy(&at); +} + +typedef struct { grp_t *g; dispatch_block_t b; int is_main; } gnotify_ctx; +static void *group_notify_thread(void *p) { + gnotify_ctx *c = (gnotify_ctx *)p; + dispatch_group_wait((dispatch_group_t)c->g, DISPATCH_TIME_FOREVER); + if (c->is_main) run_on_main(c->b); + else { ad_invoke(c->b); Block_release(c->b); } + free(c); + return NULL; +} + +void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t q, dispatch_block_t block) { + gnotify_ctx *c = (gnotify_ctx *)malloc(sizeof(gnotify_ctx)); + c->g = (grp_t *)group; c->b = Block_copy(block); + c->is_main = (q == dispatch_get_main_queue()); + pthread_t t; pthread_attr_t at; pthread_attr_init(&at); + pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED); + if (pthread_create(&t, &at, group_notify_thread, c) != 0) { + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + ad_invoke(c->b); Block_release(c->b); free(c); + } + pthread_attr_destroy(&at); +} + +// --------------------------------------------------------------------------- +// Semaphores +// --------------------------------------------------------------------------- +typedef struct { + pthread_mutex_t m; + pthread_cond_t cv; + long value; +} sem_t_shim; + +dispatch_semaphore_t dispatch_semaphore_create(long value) { + sem_t_shim *s = (sem_t_shim *)calloc(1, sizeof(sem_t_shim)); + pthread_mutex_init(&s->m, NULL); + pthread_cond_init(&s->cv, NULL); + s->value = value; + return (dispatch_semaphore_t)s; +} + +long dispatch_semaphore_signal(dispatch_semaphore_t dsema) { + sem_t_shim *s = (sem_t_shim *)dsema; + pthread_mutex_lock(&s->m); + s->value++; + pthread_cond_signal(&s->cv); + pthread_mutex_unlock(&s->m); + return 0; +} + +long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) { + sem_t_shim *s = (sem_t_shim *)dsema; + pthread_mutex_lock(&s->m); + while (s->value <= 0) { + if (timeout == DISPATCH_TIME_FOREVER) { + pthread_cond_wait(&s->cv, &s->m); + } else { + struct timespec ts; + ts.tv_sec = (time_t)(timeout / 1000000000ull); + ts.tv_nsec = (long)(timeout % 1000000000ull); + if (pthread_cond_timedwait(&s->cv, &s->m, &ts) != 0) { + pthread_mutex_unlock(&s->m); + return ~0l; // timed out + } + } + } + s->value--; + pthread_mutex_unlock(&s->m); + return 0; +} diff --git a/tools/strings2json.py b/tools/strings2json.py new file mode 100644 index 0000000..8b8eadc --- /dev/null +++ b/tools/strings2json.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# strings2json.py — Linux replacement for `plutil -convert json` on .strings files. +# Used by IPAInstaller/Makefile (after-stage) when the macOS/`build-toolchain/bin` +# plutil shim is unavailable (e.g. on a fresh CI runner). Same conversion the +# ios3 build (build-ios3.sh) performs inline. +# +# Usage: strings2json.py +import sys, re, json + +src, dst = sys.argv[1], sys.argv[2] +raw = open(src, "rb").read() +for enc in ("utf-8", "utf-16"): + try: + text = raw.decode(enc) + break + except UnicodeDecodeError: + continue +else: + sys.exit("cannot decode %s" % src) + +text = re.sub(r"/\*.*?\*/", "", text, flags=re.S) +text = re.sub(r"//[^\n]*", "", text) +pat = re.compile(r'"((?:[^"\\]|\\.)*)"\s*=\s*"((?:[^"\\]|\\.)*)"\s*;', re.S) + +def unesc(s): + return s.encode().decode("unicode_escape") if "\\" in s else s + +d = {} +for k, v in pat.findall(text): + d[unesc(k)] = unesc(v) +json.dump(d, open(dst, "w", encoding="utf-8"), ensure_ascii=False) +print(" %d strings" % len(d))