@kakuzu_aon/apkz 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +392 -0
- package/package.json +53 -0
- package/src/commands/analyze.js +261 -0
- package/src/commands/batch.js +549 -0
- package/src/commands/build.js +134 -0
- package/src/commands/clean.js +159 -0
- package/src/commands/compile.js +285 -0
- package/src/commands/config.js +343 -0
- package/src/commands/decode.js +133 -0
- package/src/commands/decompile.js +444 -0
- package/src/commands/diff.js +334 -0
- package/src/commands/extract.js +410 -0
- package/src/commands/info.js +886 -0
- package/src/commands/install.js +258 -0
- package/src/commands/modify-enhanced.js +1077 -0
- package/src/commands/modify.js +375 -0
- package/src/commands/monitor.js +421 -0
- package/src/commands/plugin.js +239 -0
- package/src/commands/sign.js +169 -0
- package/src/commands/vulnerability-scan.js +404 -0
- package/src/commands/web.js +97 -0
- package/src/index.js +139 -0
- package/src/utils/config.js +492 -0
- package/src/utils/config.json +118 -0
- package/src/utils/icon-manager.js +544 -0
- package/src/utils/manifest-parser.js +506 -0
- package/src/utils/network-analyzer.js +461 -0
- package/src/utils/obfuscation-detector.js +819 -0
- package/src/utils/plugin-system.js +390 -0
- package/src/utils/smali-editor.js +480 -0
- package/src/utils/vulnerability-scanner.js +838 -0
- package/src/web/public/index.html +1017 -0
- package/src/web/web-server.js +587 -0
- package/test_files/test.js +131 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"general": {
|
|
4
|
+
"logLevel": "info",
|
|
5
|
+
"outputDir": "./output",
|
|
6
|
+
"tempDir": "./temp",
|
|
7
|
+
"maxFileSize": 104857600,
|
|
8
|
+
"parallel": 4,
|
|
9
|
+
"timeout": 300000,
|
|
10
|
+
"autoCleanup": true,
|
|
11
|
+
"showBanner": true
|
|
12
|
+
},
|
|
13
|
+
"security": {
|
|
14
|
+
"defaultSeverity": "medium",
|
|
15
|
+
"scanNativeLibs": true,
|
|
16
|
+
"scanResources": true,
|
|
17
|
+
"scanManifest": true,
|
|
18
|
+
"checkKnownVulns": true,
|
|
19
|
+
"generateCVSS": true,
|
|
20
|
+
"complianceChecks": ["owasp-top10", "mobile-top10"],
|
|
21
|
+
"excludePatterns": [
|
|
22
|
+
"schemas.android.com",
|
|
23
|
+
"android:layout",
|
|
24
|
+
"android:drawable",
|
|
25
|
+
"android:color"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"web": {
|
|
29
|
+
"port": 3000,
|
|
30
|
+
"host": "localhost",
|
|
31
|
+
"cors": true,
|
|
32
|
+
"maxUploadSize": 104857600,
|
|
33
|
+
"sessionTimeout": 3600000,
|
|
34
|
+
"enableAuth": false,
|
|
35
|
+
"authSecret": "apkz-secret-key",
|
|
36
|
+
"enableWebhook": false,
|
|
37
|
+
"webhookUrl": ""
|
|
38
|
+
},
|
|
39
|
+
"batch": {
|
|
40
|
+
"parallel": 4,
|
|
41
|
+
"recursive": true,
|
|
42
|
+
"includeSubdirs": true,
|
|
43
|
+
"filter": "*.apk",
|
|
44
|
+
"autoAnalyze": true,
|
|
45
|
+
"generateSummary": true,
|
|
46
|
+
"saveIndividualReports": true,
|
|
47
|
+
"cleanupTempFiles": true
|
|
48
|
+
},
|
|
49
|
+
"monitoring": {
|
|
50
|
+
"interval": 5000,
|
|
51
|
+
"autoAnalyze": true,
|
|
52
|
+
"enableWebhook": false,
|
|
53
|
+
"webhookUrl": "",
|
|
54
|
+
"logEvents": true,
|
|
55
|
+
"maxEvents": 1000,
|
|
56
|
+
"eventRetention": 86400000
|
|
57
|
+
},
|
|
58
|
+
"reports": {
|
|
59
|
+
"defaultFormat": "json",
|
|
60
|
+
"includeScreenshots": false,
|
|
61
|
+
"includeStrings": true,
|
|
62
|
+
"includeNetwork": true,
|
|
63
|
+
"includeObfuscation": true,
|
|
64
|
+
"template": "default",
|
|
65
|
+
"customTemplates": {},
|
|
66
|
+
"exportFormats": ["json", "html", "csv"],
|
|
67
|
+
"compression": false,
|
|
68
|
+
"encryption": false
|
|
69
|
+
},
|
|
70
|
+
"modification": {
|
|
71
|
+
"autoBackup": true,
|
|
72
|
+
"backupDir": "./backups",
|
|
73
|
+
"preserveOriginal": true,
|
|
74
|
+
"validateAfterModification": true,
|
|
75
|
+
"autoSign": false,
|
|
76
|
+
"debugKeystore": {
|
|
77
|
+
"path": "./debug.keystore",
|
|
78
|
+
"alias": "androiddebugkey",
|
|
79
|
+
"password": "android",
|
|
80
|
+
"keyPassword": "android"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"analysis": {
|
|
84
|
+
"deepScan": false,
|
|
85
|
+
"extractStrings": true,
|
|
86
|
+
"analyzeNetwork": true,
|
|
87
|
+
"detectObfuscation": true,
|
|
88
|
+
"scanNativeLibs": true,
|
|
89
|
+
"analyzeResources": true,
|
|
90
|
+
"extractSignatures": true,
|
|
91
|
+
"generateReport": true
|
|
92
|
+
},
|
|
93
|
+
"plugins": {
|
|
94
|
+
"enabled": true,
|
|
95
|
+
"autoLoad": true,
|
|
96
|
+
"pluginDir": "./plugins",
|
|
97
|
+
"maxPlugins": 50,
|
|
98
|
+
"allowRemote": false,
|
|
99
|
+
"trustedSources": []
|
|
100
|
+
},
|
|
101
|
+
"ui": {
|
|
102
|
+
"theme": "default",
|
|
103
|
+
"colors": true,
|
|
104
|
+
"progressBars": true,
|
|
105
|
+
"animations": true,
|
|
106
|
+
"compact": false,
|
|
107
|
+
"showTimestamps": false,
|
|
108
|
+
"showMemoryUsage": false
|
|
109
|
+
},
|
|
110
|
+
"performance": {
|
|
111
|
+
"cacheResults": true,
|
|
112
|
+
"cacheSize": 104857600,
|
|
113
|
+
"compressionLevel": 6,
|
|
114
|
+
"memoryLimit": 1073741824,
|
|
115
|
+
"cpuLimit": 80,
|
|
116
|
+
"enableProfiling": false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
// ────────────[ KAKUZU ]────────────────────────────
|
|
2
|
+
// | Discord : kakuzu_aon
|
|
3
|
+
// | Telegram : kakuzu_aon
|
|
4
|
+
// | Github : kakuzu-aon
|
|
5
|
+
// | File : icon-manager.js
|
|
6
|
+
// | License : MIT License © 2026 Kakuzu
|
|
7
|
+
// | Brief : APK icon replacement utilities
|
|
8
|
+
// ────────────────★─────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const fs = require('fs-extra');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const sharp = require('sharp');
|
|
13
|
+
|
|
14
|
+
class IconManager {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.iconSizes = {
|
|
17
|
+
// Android launcher icon sizes
|
|
18
|
+
mdpi: 48,
|
|
19
|
+
hdpi: 72,
|
|
20
|
+
xhdpi: 96,
|
|
21
|
+
xxhdpi: 144,
|
|
22
|
+
xxxhdpi: 192,
|
|
23
|
+
// Adaptive icon sizes
|
|
24
|
+
adaptive_mdpi: 80,
|
|
25
|
+
adaptive_hdpi: 120,
|
|
26
|
+
adaptive_xhdpi: 160,
|
|
27
|
+
adaptive_xxhdpi: 240,
|
|
28
|
+
adaptive_xxxhdpi: 320,
|
|
29
|
+
// Notification icons
|
|
30
|
+
notification_mdpi: 24,
|
|
31
|
+
notification_hdpi: 36,
|
|
32
|
+
notification_xhdpi: 48,
|
|
33
|
+
notification_xxhdpi: 72,
|
|
34
|
+
notification_xxxhdpi: 96
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Replace app icons in decoded APK
|
|
40
|
+
*/
|
|
41
|
+
async replaceIcons(decodedPath, iconPath, options = {}) {
|
|
42
|
+
const results = {
|
|
43
|
+
replaced: [],
|
|
44
|
+
errors: [],
|
|
45
|
+
generated: []
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Validate input icon
|
|
50
|
+
if (!fs.existsSync(iconPath)) {
|
|
51
|
+
throw new Error(`Icon file not found: ${iconPath}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if icon is valid image
|
|
55
|
+
const iconMetadata = await sharp(iconPath).metadata();
|
|
56
|
+
if (!['png', 'jpeg', 'jpg', 'webp'].includes(iconMetadata.format)) {
|
|
57
|
+
throw new Error(`Unsupported icon format: ${iconMetadata.format}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const resDir = path.join(decodedPath, 'res');
|
|
61
|
+
if (!fs.existsSync(resDir)) {
|
|
62
|
+
throw new Error('Resources directory not found in decoded APK');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Generate different sizes
|
|
66
|
+
const tempDir = path.join(decodedPath, '.temp_icons');
|
|
67
|
+
await fs.ensureDir(tempDir);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Generate all required sizes
|
|
71
|
+
await this.generateIconSizes(iconPath, tempDir, options);
|
|
72
|
+
|
|
73
|
+
// Replace launcher icons
|
|
74
|
+
await this.replaceLauncherIcons(resDir, tempDir, results);
|
|
75
|
+
|
|
76
|
+
// Replace adaptive icons
|
|
77
|
+
await this.replaceAdaptiveIcons(resDir, tempDir, results);
|
|
78
|
+
|
|
79
|
+
// Replace notification icons if requested
|
|
80
|
+
if (options.includeNotifications) {
|
|
81
|
+
await this.replaceNotificationIcons(resDir, tempDir, results);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Update manifest if requested
|
|
85
|
+
if (options.updateManifest) {
|
|
86
|
+
await this.updateManifestIcons(decodedPath, iconPath, results);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
} finally {
|
|
90
|
+
// Clean up temp directory
|
|
91
|
+
await fs.remove(tempDir);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
results.errors.push({
|
|
96
|
+
error: error.message,
|
|
97
|
+
step: 'icon_replacement'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate icon sizes from source image
|
|
106
|
+
*/
|
|
107
|
+
async generateIconSizes(sourcePath, outputDir, options = {}) {
|
|
108
|
+
const { format = 'png', quality = 90, adaptive = true } = options;
|
|
109
|
+
|
|
110
|
+
for (const [sizeName, size] of Object.entries(this.iconSizes)) {
|
|
111
|
+
try {
|
|
112
|
+
let outputPath = path.join(outputDir, `${sizeName}.${format}`);
|
|
113
|
+
|
|
114
|
+
const resizeOptions = {
|
|
115
|
+
width: size,
|
|
116
|
+
height: size,
|
|
117
|
+
fit: 'cover',
|
|
118
|
+
position: 'center'
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// For adaptive icons, add padding
|
|
122
|
+
if (sizeName.startsWith('adaptive_')) {
|
|
123
|
+
const adaptiveSize = parseInt(sizeName.split('_')[1]);
|
|
124
|
+
const canvasSize = this.iconSizes[`adaptive_${sizeName.split('_')[1]}`];
|
|
125
|
+
|
|
126
|
+
await sharp(sourcePath)
|
|
127
|
+
.resize(adaptiveSize * 0.8, adaptiveSize * 0.8, { fit: 'cover', position: 'center' })
|
|
128
|
+
.extend({
|
|
129
|
+
top: adaptiveSize * 0.1,
|
|
130
|
+
bottom: adaptiveSize * 0.1,
|
|
131
|
+
left: adaptiveSize * 0.1,
|
|
132
|
+
right: adaptiveSize * 0.1,
|
|
133
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
134
|
+
})
|
|
135
|
+
.toFile(outputPath);
|
|
136
|
+
} else {
|
|
137
|
+
await sharp(sourcePath)
|
|
138
|
+
.resize(resizeOptions)
|
|
139
|
+
.toFormat(format, { quality })
|
|
140
|
+
.toFile(outputPath);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.warn(`Warning: Could not generate ${sizeName}: ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Replace launcher icons
|
|
151
|
+
*/
|
|
152
|
+
async replaceLauncherIcons(resDir, tempDir, results) {
|
|
153
|
+
const launcherDirs = [
|
|
154
|
+
'mipmap-mdpi', 'mipmap-hdpi', 'mipmap-xhdpi', 'mipmap-xxhdpi', 'mipmap-xxxhdpi',
|
|
155
|
+
'drawable-mdpi', 'drawable-hdpi', 'drawable-xhdpi', 'drawable-xxhdpi', 'drawable-xxxhdpi'
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const iconNames = ['ic_launcher.png', 'ic_launcher.webp', 'icon.png', 'icon.webp'];
|
|
159
|
+
|
|
160
|
+
for (const dir of launcherDirs) {
|
|
161
|
+
const dirPath = path.join(resDir, dir);
|
|
162
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
163
|
+
|
|
164
|
+
const density = dir.split('-')[1];
|
|
165
|
+
const sizeName = density;
|
|
166
|
+
|
|
167
|
+
for (const iconName of iconNames) {
|
|
168
|
+
const iconPath = path.join(dirPath, iconName);
|
|
169
|
+
const sourceIcon = path.join(tempDir, `${sizeName}.png`);
|
|
170
|
+
|
|
171
|
+
if (fs.existsSync(iconPath) && fs.existsSync(sourceIcon)) {
|
|
172
|
+
try {
|
|
173
|
+
await fs.copy(sourceIcon, iconPath);
|
|
174
|
+
results.replaced.push({
|
|
175
|
+
type: 'launcher',
|
|
176
|
+
file: path.relative(resDir, iconPath),
|
|
177
|
+
size: this.iconSizes[sizeName]
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
results.errors.push({
|
|
181
|
+
error: error.message,
|
|
182
|
+
file: path.relative(resDir, iconPath)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Replace adaptive icons
|
|
192
|
+
*/
|
|
193
|
+
async replaceAdaptiveIcons(resDir, tempDir, results) {
|
|
194
|
+
const adaptiveDirs = [
|
|
195
|
+
'mipmap-mdpi-v26', 'mipmap-hdpi-v26', 'mipmap-xhdpi-v26',
|
|
196
|
+
'mipmap-xxhdpi-v26', 'mipmap-xxxhdpi-v26'
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
for (const dir of adaptiveDirs) {
|
|
200
|
+
const dirPath = path.join(resDir, dir);
|
|
201
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
202
|
+
|
|
203
|
+
const density = dir.split('-')[1];
|
|
204
|
+
const sizeName = `adaptive_${density}`;
|
|
205
|
+
|
|
206
|
+
// Replace foreground icons
|
|
207
|
+
const foregroundIcon = path.join(dirPath, 'ic_launcher_foreground.png');
|
|
208
|
+
const sourceForeground = path.join(tempDir, `${sizeName}.png`);
|
|
209
|
+
|
|
210
|
+
if (fs.existsSync(foregroundIcon) && fs.existsSync(sourceForeground)) {
|
|
211
|
+
try {
|
|
212
|
+
await fs.copy(sourceForeground, foregroundIcon);
|
|
213
|
+
results.replaced.push({
|
|
214
|
+
type: 'adaptive_foreground',
|
|
215
|
+
file: path.relative(resDir, foregroundIcon),
|
|
216
|
+
size: this.iconSizes[sizeName]
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
results.errors.push({
|
|
220
|
+
error: error.message,
|
|
221
|
+
file: path.relative(resDir, foregroundIcon)
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Replace notification icons
|
|
230
|
+
*/
|
|
231
|
+
async replaceNotificationIcons(resDir, tempDir, results) {
|
|
232
|
+
const notificationDirs = [
|
|
233
|
+
'drawable-mdpi', 'drawable-hdpi', 'drawable-xhdpi',
|
|
234
|
+
'drawable-xxhdpi', 'drawable-xxxhdpi'
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
for (const dir of notificationDirs) {
|
|
238
|
+
const dirPath = path.join(resDir, dir);
|
|
239
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
240
|
+
|
|
241
|
+
const density = dir.split('-')[1];
|
|
242
|
+
const sizeName = `notification_${density}`;
|
|
243
|
+
|
|
244
|
+
const notificationIcon = path.join(dirPath, 'ic_notification.png');
|
|
245
|
+
const sourceIcon = path.join(tempDir, `${sizeName}.png`);
|
|
246
|
+
|
|
247
|
+
if (fs.existsSync(notificationIcon) && fs.existsSync(sourceIcon)) {
|
|
248
|
+
try {
|
|
249
|
+
await fs.copy(sourceIcon, notificationIcon);
|
|
250
|
+
results.replaced.push({
|
|
251
|
+
type: 'notification',
|
|
252
|
+
file: path.relative(resDir, notificationIcon),
|
|
253
|
+
size: this.iconSizes[sizeName]
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
results.errors.push({
|
|
257
|
+
error: error.message,
|
|
258
|
+
file: path.relative(resDir, notificationIcon)
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Update manifest to reference new icons
|
|
267
|
+
*/
|
|
268
|
+
async updateManifestIcons(decodedPath, iconPath, results) {
|
|
269
|
+
const manifestPath = path.join(decodedPath, 'AndroidManifest.xml');
|
|
270
|
+
|
|
271
|
+
if (!fs.existsSync(manifestPath)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
let manifestContent = await fs.readFile(manifestPath, 'utf8');
|
|
277
|
+
|
|
278
|
+
// Update application icon if needed
|
|
279
|
+
if (manifestContent.includes('android:icon')) {
|
|
280
|
+
// This is a simplified approach - in reality, manifest might be binary
|
|
281
|
+
console.log('Manifest icon references detected - manual update may be required');
|
|
282
|
+
results.replaced.push({
|
|
283
|
+
type: 'manifest_note',
|
|
284
|
+
file: 'AndroidManifest.xml',
|
|
285
|
+
message: 'Manual icon reference update may be needed'
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
} catch (error) {
|
|
290
|
+
results.errors.push({
|
|
291
|
+
error: error.message,
|
|
292
|
+
file: 'AndroidManifest.xml'
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Extract existing icons from APK
|
|
299
|
+
*/
|
|
300
|
+
async extractIcons(decodedPath, outputDir) {
|
|
301
|
+
const results = {
|
|
302
|
+
extracted: [],
|
|
303
|
+
errors: []
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const resDir = path.join(decodedPath, 'res');
|
|
307
|
+
if (!fs.existsSync(resDir)) {
|
|
308
|
+
results.errors.push('Resources directory not found');
|
|
309
|
+
return results;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await fs.ensureDir(outputDir);
|
|
313
|
+
|
|
314
|
+
// Find all icon files
|
|
315
|
+
const iconPatterns = [
|
|
316
|
+
'**/ic_launcher.*',
|
|
317
|
+
'**/ic_launcher_foreground.*',
|
|
318
|
+
'**/ic_launcher_background.*',
|
|
319
|
+
'**/icon.*',
|
|
320
|
+
'**/ic_notification.*'
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
for (const pattern of iconPatterns) {
|
|
324
|
+
try {
|
|
325
|
+
const files = await this.findFiles(resDir, pattern);
|
|
326
|
+
|
|
327
|
+
for (const file of files) {
|
|
328
|
+
const relativePath = path.relative(resDir, file);
|
|
329
|
+
const outputPath = path.join(outputDir, relativePath.replace(/[\/\\]/g, '_'));
|
|
330
|
+
|
|
331
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
332
|
+
await fs.copy(file, outputPath);
|
|
333
|
+
|
|
334
|
+
results.extracted.push({
|
|
335
|
+
source: relativePath,
|
|
336
|
+
output: path.basename(outputPath)
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
results.errors.push({
|
|
341
|
+
error: error.message,
|
|
342
|
+
pattern
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return results;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Create adaptive icon from single image
|
|
352
|
+
*/
|
|
353
|
+
async createAdaptiveIcon(iconPath, outputPath, options = {}) {
|
|
354
|
+
const {
|
|
355
|
+
backgroundColor = '#FFFFFF',
|
|
356
|
+
size = 108,
|
|
357
|
+
padding = 0.2
|
|
358
|
+
} = options;
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const iconSize = Math.floor(size * (1 - padding));
|
|
362
|
+
|
|
363
|
+
await sharp(iconPath)
|
|
364
|
+
.resize(iconSize, iconSize, { fit: 'cover', position: 'center' })
|
|
365
|
+
.extend({
|
|
366
|
+
top: size * padding / 2,
|
|
367
|
+
bottom: size * padding / 2,
|
|
368
|
+
left: size * padding / 2,
|
|
369
|
+
right: size * padding / 2,
|
|
370
|
+
background: backgroundColor
|
|
371
|
+
})
|
|
372
|
+
.toFile(outputPath);
|
|
373
|
+
|
|
374
|
+
return { success: true, outputPath };
|
|
375
|
+
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return { success: false, error: error.message };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Validate icon file
|
|
383
|
+
*/
|
|
384
|
+
async validateIcon(iconPath) {
|
|
385
|
+
try {
|
|
386
|
+
if (!fs.existsSync(iconPath)) {
|
|
387
|
+
return { valid: false, error: 'File not found' };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const metadata = await sharp(iconPath).metadata();
|
|
391
|
+
|
|
392
|
+
if (!['png', 'jpeg', 'jpg', 'webp'].includes(metadata.format)) {
|
|
393
|
+
return { valid: false, error: `Unsupported format: ${metadata.format}` };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (metadata.width < 48 || metadata.height < 48) {
|
|
397
|
+
return { valid: false, error: 'Icon too small (minimum 48x48)' };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (metadata.width > 2048 || metadata.height > 2048) {
|
|
401
|
+
return { valid: false, error: 'Icon too large (maximum 2048x2048)' };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (Math.abs(metadata.width - metadata.height) > metadata.width * 0.1) {
|
|
405
|
+
return { valid: false, error: 'Icon should be roughly square' };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
valid: true,
|
|
410
|
+
metadata: {
|
|
411
|
+
format: metadata.format,
|
|
412
|
+
width: metadata.width,
|
|
413
|
+
height: metadata.height,
|
|
414
|
+
size: metadata.size
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
} catch (error) {
|
|
419
|
+
return { valid: false, error: error.message };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Find files matching pattern
|
|
425
|
+
*/
|
|
426
|
+
async findFiles(dir, pattern) {
|
|
427
|
+
const files = [];
|
|
428
|
+
const items = await fs.readdir(dir);
|
|
429
|
+
|
|
430
|
+
for (const item of items) {
|
|
431
|
+
const fullPath = path.join(dir, item);
|
|
432
|
+
const stat = await fs.stat(fullPath);
|
|
433
|
+
|
|
434
|
+
if (stat.isDirectory()) {
|
|
435
|
+
const subFiles = await this.findFiles(fullPath, pattern);
|
|
436
|
+
files.push(...subFiles);
|
|
437
|
+
} else {
|
|
438
|
+
// Simple pattern matching
|
|
439
|
+
if (this.matchesPattern(item, pattern)) {
|
|
440
|
+
files.push(fullPath);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return files;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Simple pattern matching
|
|
450
|
+
*/
|
|
451
|
+
matchesPattern(filename, pattern) {
|
|
452
|
+
const regex = new RegExp(
|
|
453
|
+
pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]'),
|
|
454
|
+
'i'
|
|
455
|
+
);
|
|
456
|
+
return regex.test(filename);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get icon information from decoded APK
|
|
461
|
+
*/
|
|
462
|
+
async getIconInfo(decodedPath) {
|
|
463
|
+
const info = {
|
|
464
|
+
launcherIcons: [],
|
|
465
|
+
adaptiveIcons: [],
|
|
466
|
+
notificationIcons: [],
|
|
467
|
+
totalIcons: 0
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const resDir = path.join(decodedPath, 'res');
|
|
471
|
+
if (!fs.existsSync(resDir)) {
|
|
472
|
+
return info;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Scan for icon files
|
|
476
|
+
const items = await fs.readdir(resDir, { withFileTypes: true });
|
|
477
|
+
|
|
478
|
+
for (const item of items) {
|
|
479
|
+
if (!item.isDirectory()) continue;
|
|
480
|
+
|
|
481
|
+
const dirPath = path.join(resDir, item.name);
|
|
482
|
+
const files = await fs.readdir(dirPath);
|
|
483
|
+
|
|
484
|
+
for (const file of files) {
|
|
485
|
+
if (this.isIconFile(file)) {
|
|
486
|
+
const filePath = path.join(dirPath, file);
|
|
487
|
+
const stat = await fs.stat(filePath);
|
|
488
|
+
|
|
489
|
+
const iconInfo = {
|
|
490
|
+
path: path.relative(resDir, filePath),
|
|
491
|
+
size: stat.size,
|
|
492
|
+
type: this.getIconType(file),
|
|
493
|
+
density: this.getDensityFromDir(item.name)
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
if (iconInfo.type === 'launcher') {
|
|
497
|
+
info.launcherIcons.push(iconInfo);
|
|
498
|
+
} else if (iconInfo.type === 'adaptive') {
|
|
499
|
+
info.adaptiveIcons.push(iconInfo);
|
|
500
|
+
} else if (iconInfo.type === 'notification') {
|
|
501
|
+
info.notificationIcons.push(iconInfo);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
info.totalIcons++;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return info;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Check if file is an icon
|
|
514
|
+
*/
|
|
515
|
+
isIconFile(filename) {
|
|
516
|
+
const iconNames = [
|
|
517
|
+
'ic_launcher', 'ic_launcher_foreground', 'ic_launcher_background',
|
|
518
|
+
'icon', 'ic_notification', 'ic_stat_notify'
|
|
519
|
+
];
|
|
520
|
+
|
|
521
|
+
return iconNames.some(name => filename.toLowerCase().startsWith(name.toLowerCase()));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Get icon type from filename
|
|
526
|
+
*/
|
|
527
|
+
getIconType(filename) {
|
|
528
|
+
if (filename.includes('launcher_foreground')) return 'adaptive';
|
|
529
|
+
if (filename.includes('launcher_background')) return 'adaptive';
|
|
530
|
+
if (filename.includes('launcher') || filename.includes('icon')) return 'launcher';
|
|
531
|
+
if (filename.includes('notification') || filename.includes('ic_stat_')) return 'notification';
|
|
532
|
+
return 'unknown';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Get density from directory name
|
|
537
|
+
*/
|
|
538
|
+
getDensityFromDir(dirname) {
|
|
539
|
+
const densities = ['mdpi', 'hdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi', 'nodpi', 'anydpi'];
|
|
540
|
+
return densities.find(d => dirname.includes(d)) || 'unknown';
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
module.exports = IconManager;
|