@devicecloud.dev/dcd 5.7.0 → 5.7.1
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.
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { parseBuffer } from 'bplist-parser';
|
|
2
|
-
import nodeApk from 'node-apk';
|
|
3
2
|
import { readFile, rm } from 'node:fs/promises';
|
|
4
3
|
import * as path from 'node:path';
|
|
5
4
|
import StreamZip from 'node-stream-zip';
|
|
6
5
|
import { parse } from 'plist';
|
|
7
|
-
|
|
8
|
-
// (cjs-module-lexer) is version-dependent, so destructure off the default import
|
|
9
|
-
// instead — that interop is guaranteed on every Node version. bplist-parser 0.5
|
|
10
|
-
// is a real ESM/CJS dual package with named exports, so it imports directly.
|
|
11
|
-
const { Apk } = nodeApk;
|
|
6
|
+
import { readManifestPackage } from '../utils/android-manifest.js';
|
|
12
7
|
/**
|
|
13
8
|
* Parses an Info.plist buffer (XML, UTF-8 BOM'd XML, or binary bplist).
|
|
14
9
|
* Shared by the .app and .zip extractors.
|
|
@@ -36,13 +31,13 @@ export class AndroidMetadataExtractor {
|
|
|
36
31
|
return filePath.endsWith('.apk');
|
|
37
32
|
}
|
|
38
33
|
async extract(filePath) {
|
|
39
|
-
const
|
|
34
|
+
const zip = new StreamZip.async({ file: filePath });
|
|
40
35
|
try {
|
|
41
|
-
const manifest = await
|
|
42
|
-
return { appId: manifest
|
|
36
|
+
const manifest = await zip.entryData('AndroidManifest.xml');
|
|
37
|
+
return { appId: readManifestPackage(manifest), platform: 'android' };
|
|
43
38
|
}
|
|
44
39
|
finally {
|
|
45
|
-
|
|
40
|
+
await zip.close().catch(() => { });
|
|
46
41
|
}
|
|
47
42
|
}
|
|
48
43
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Reads the package name out of an APK's compiled AndroidManifest.xml (Android
|
|
2
|
+
// binary XML). This replaced node-apk, whose node-forge dependency carried an
|
|
3
|
+
// unpatched advisory for certificate code the CLI never used. Layouts follow
|
|
4
|
+
// the ResChunk_header / ResStringPool_header / ResXMLTree_* structs in AOSP's
|
|
5
|
+
// libandroidfw ResourceTypes.h; every integer is little-endian.
|
|
6
|
+
const RES_STRING_POOL_TYPE = 0x0001;
|
|
7
|
+
const RES_XML_TYPE = 0x0003;
|
|
8
|
+
const RES_XML_START_ELEMENT_TYPE = 0x0102;
|
|
9
|
+
const UTF8_FLAG = 0x0100;
|
|
10
|
+
const TYPE_STRING = 0x03;
|
|
11
|
+
const NO_INDEX = 0xffff_ffff;
|
|
12
|
+
const CHUNK_HEADER_SIZE = 8;
|
|
13
|
+
/**
|
|
14
|
+
* Returns a lookup for the strings in the string pool chunk at `offset`.
|
|
15
|
+
* Strings are decoded on demand, so a malformed entry the manifest never
|
|
16
|
+
* refers to can't fail the parse.
|
|
17
|
+
*/
|
|
18
|
+
function stringPool(xml, offset) {
|
|
19
|
+
const headerSize = xml.readUInt16LE(offset + 2);
|
|
20
|
+
const count = xml.readUInt32LE(offset + 8);
|
|
21
|
+
const utf8 = (xml.readUInt32LE(offset + 16) & UTF8_FLAG) !== 0;
|
|
22
|
+
const stringsStart = offset + xml.readUInt32LE(offset + 20);
|
|
23
|
+
return (index) => {
|
|
24
|
+
if (index >= count) {
|
|
25
|
+
throw new Error(`String index ${index} is out of range`);
|
|
26
|
+
}
|
|
27
|
+
let pos = stringsStart + xml.readUInt32LE(offset + headerSize + index * 4);
|
|
28
|
+
if (utf8) {
|
|
29
|
+
// Two lengths, each 1 byte or 2 when the high bit is set: the UTF-16
|
|
30
|
+
// length (unused here), then the UTF-8 byte length.
|
|
31
|
+
pos += xml[pos] & 0x80 ? 2 : 1;
|
|
32
|
+
let length = xml[pos++];
|
|
33
|
+
if (length & 0x80)
|
|
34
|
+
length = ((length & 0x7f) << 8) | xml[pos++];
|
|
35
|
+
return xml.toString('utf8', pos, pos + length);
|
|
36
|
+
}
|
|
37
|
+
// UTF-16 length in code units: 2 bytes, or 4 when the high bit is set.
|
|
38
|
+
let length = xml.readUInt16LE(pos);
|
|
39
|
+
pos += 2;
|
|
40
|
+
if (length & 0x80_00) {
|
|
41
|
+
length = ((length & 0x7f_ff) << 16) | xml.readUInt16LE(pos);
|
|
42
|
+
pos += 2;
|
|
43
|
+
}
|
|
44
|
+
return xml.toString('utf16le', pos, pos + length * 2);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reads the `package` attribute of the root `<manifest>` element.
|
|
49
|
+
* @param xml - Contents of the APK's AndroidManifest.xml entry
|
|
50
|
+
* @returns The application id, e.g. `org.wikipedia`
|
|
51
|
+
*/
|
|
52
|
+
export function readManifestPackage(xml) {
|
|
53
|
+
if (xml.length < CHUNK_HEADER_SIZE || xml.readUInt16LE(0) !== RES_XML_TYPE) {
|
|
54
|
+
throw new Error('AndroidManifest.xml is not Android binary XML');
|
|
55
|
+
}
|
|
56
|
+
const end = Math.min(xml.length, xml.readUInt32LE(4));
|
|
57
|
+
let offset = xml.readUInt16LE(2);
|
|
58
|
+
let stringAt;
|
|
59
|
+
while (offset + CHUNK_HEADER_SIZE <= end) {
|
|
60
|
+
const type = xml.readUInt16LE(offset);
|
|
61
|
+
const headerSize = xml.readUInt16LE(offset + 2);
|
|
62
|
+
const size = xml.readUInt32LE(offset + 4);
|
|
63
|
+
if (size < CHUNK_HEADER_SIZE) {
|
|
64
|
+
throw new Error(`Malformed binary XML chunk at offset ${offset}`);
|
|
65
|
+
}
|
|
66
|
+
if (type === RES_STRING_POOL_TYPE) {
|
|
67
|
+
stringAt = stringPool(xml, offset);
|
|
68
|
+
}
|
|
69
|
+
else if (type === RES_XML_START_ELEMENT_TYPE) {
|
|
70
|
+
// The first element is the document root.
|
|
71
|
+
if (!stringAt)
|
|
72
|
+
throw new Error('Binary XML has no string pool');
|
|
73
|
+
const ext = offset + headerSize;
|
|
74
|
+
if (stringAt(xml.readUInt32LE(ext + 4)) !== 'manifest') {
|
|
75
|
+
throw new Error('Root element of AndroidManifest.xml is not <manifest>');
|
|
76
|
+
}
|
|
77
|
+
const attributeStart = ext + xml.readUInt16LE(ext + 8);
|
|
78
|
+
const attributeSize = xml.readUInt16LE(ext + 10);
|
|
79
|
+
const attributeCount = xml.readUInt16LE(ext + 12);
|
|
80
|
+
for (let i = 0; i < attributeCount; i++) {
|
|
81
|
+
const attr = attributeStart + i * attributeSize;
|
|
82
|
+
if (stringAt(xml.readUInt32LE(attr + 4)) !== 'package')
|
|
83
|
+
continue;
|
|
84
|
+
// Use the typed string value when there is one, else the raw string
|
|
85
|
+
// the attribute was compiled from.
|
|
86
|
+
const raw = xml.readUInt32LE(attr + 8);
|
|
87
|
+
const index = xml[attr + 15] === TYPE_STRING ? xml.readUInt32LE(attr + 16) : raw;
|
|
88
|
+
if (index === NO_INDEX)
|
|
89
|
+
break;
|
|
90
|
+
return stringAt(index);
|
|
91
|
+
}
|
|
92
|
+
throw new Error('<manifest> has no package attribute');
|
|
93
|
+
}
|
|
94
|
+
offset += size;
|
|
95
|
+
}
|
|
96
|
+
throw new Error('AndroidManifest.xml has no <manifest> element');
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -6,13 +6,12 @@
|
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@clack/prompts": "^1.6.0",
|
|
9
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
9
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
10
10
|
"@supabase/supabase-js": "^2.108.2",
|
|
11
11
|
"bplist-parser": "^0.5.0",
|
|
12
12
|
"chalk": "^6.0.0",
|
|
13
13
|
"citty": "^0.2.2",
|
|
14
14
|
"js-yaml": "^5.2.2",
|
|
15
|
-
"node-apk": "^1.2.1",
|
|
16
15
|
"node-stream-zip": "^1.15.0",
|
|
17
16
|
"plist": "^5.0.0",
|
|
18
17
|
"tar": "^7.5.16",
|
|
@@ -69,7 +68,7 @@
|
|
|
69
68
|
"test:unit": "node scripts/test-runner.mjs --unit",
|
|
70
69
|
"typecheck": "tsc --noEmit -p tsconfig.test.json"
|
|
71
70
|
},
|
|
72
|
-
"version": "5.7.
|
|
71
|
+
"version": "5.7.1",
|
|
73
72
|
"bugs": {
|
|
74
73
|
"url": "https://discord.gg/gm3mJwcNw8"
|
|
75
74
|
},
|
|
@@ -112,12 +111,14 @@
|
|
|
112
111
|
"qs@<6.16.0": "6.16.0",
|
|
113
112
|
"fast-uri@>=3.0.0 <3.1.8": "3.1.8",
|
|
114
113
|
"hono@>=4.0.0 <4.13.7": "4.13.7",
|
|
114
|
+
"source-map-js@<1.2.2": "1.2.2",
|
|
115
115
|
"micromatch>picomatch": "^2.3.2",
|
|
116
116
|
"tinyglobby>picomatch": "^4.0.4"
|
|
117
117
|
},
|
|
118
118
|
"auditConfig": {
|
|
119
119
|
"ignoreGhsas": [
|
|
120
|
-
"GHSA-frvp-7c67-39w9"
|
|
120
|
+
"GHSA-frvp-7c67-39w9",
|
|
121
|
+
"GHSA-vfj7-8cjw-p6xm"
|
|
121
122
|
]
|
|
122
123
|
}
|
|
123
124
|
}
|