diff --git a/bun.lock b/bun.lock index 1fe0c62..418a93a 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "dependencies": { @@ -23,6 +24,7 @@ "plist": "^3.1.0", "progress": "^2.0.3", "properties": "^1.2.1", + "protobufjs": "^7.5.4", "read": "^4.1.0", "registry-auth-token": "^5.1.0", "semver": "^7.7.2", @@ -121,6 +123,26 @@ "@pnpm/npm-conf": ["@pnpm/npm-conf@2.3.1", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], "@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], @@ -589,6 +611,8 @@ "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "queue-tick": ["queue-tick@1.0.1", "", {}, "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="], @@ -827,6 +851,8 @@ "object.assign/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="], + "protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "safe-array-concat/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="], "safe-array-concat/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="], diff --git a/cli.json b/cli.json index 9ede2ee..48ce238 100644 --- a/cli.json +++ b/cli.json @@ -55,6 +55,7 @@ "parseApp": {}, "parseIpa": {}, "parseApk": {}, + "parseAab": {}, "packages": { "options": { "platform": { diff --git a/package.json b/package.json index 57a043d..64f3866 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "react-native-update-cli", - "version": "2.4.2", + "version": "2.5.0", "description": "command line tool for react-native-update (remote updates for react native)", "main": "index.js", "bin": { "pushy": "lib/index.js", "cresc": "lib/index.js" }, - "files": ["lib", "src", "cli.json"], + "files": ["lib", "src", "proto", "cli.json"], "scripts": { "build": "swc src -d lib --strip-leading-paths", "prepublishOnly": "npm run build && chmod +x lib/index.js", @@ -45,6 +45,7 @@ "plist": "^3.1.0", "progress": "^2.0.3", "properties": "^1.2.1", + "protobufjs": "^7.5.4", "read": "^4.1.0", "registry-auth-token": "^5.1.0", "semver": "^7.7.2", diff --git a/proto/Configuration.proto b/proto/Configuration.proto new file mode 100644 index 0000000..8d43a23 --- /dev/null +++ b/proto/Configuration.proto @@ -0,0 +1,183 @@ +syntax = "proto3"; + +package aapt.pb; + +option java_package = "com.android.aapt"; + +message Configuration { + enum LayoutDirection { + LAYOUT_DIRECTION_UNSET = 0; + LAYOUT_DIRECTION_LTR = 1; + LAYOUT_DIRECTION_RTL = 2; + } + + enum ScreenLayoutSize { + SCREEN_LAYOUT_SIZE_UNSET = 0; + SCREEN_LAYOUT_SIZE_SMALL = 1; + SCREEN_LAYOUT_SIZE_NORMAL = 2; + SCREEN_LAYOUT_SIZE_LARGE = 3; + SCREEN_LAYOUT_SIZE_XLARGE = 4; + } + + enum ScreenLayoutLong { + SCREEN_LAYOUT_LONG_UNSET = 0; + SCREEN_LAYOUT_LONG_LONG = 1; + SCREEN_LAYOUT_LONG_NOTLONG = 2; + } + + enum ScreenRound { + SCREEN_ROUND_UNSET = 0; + SCREEN_ROUND_ROUND = 1; + SCREEN_ROUND_NOTROUND = 2; + } + + enum WideColorGamut { + WIDE_COLOR_GAMUT_UNSET = 0; + WIDE_COLOR_GAMUT_WIDECG = 1; + WIDE_COLOR_GAMUT_NOWIDECG = 2; + } + + enum Hdr { + HDR_UNSET = 0; + HDR_HIGHDR = 1; + HDR_LOWDR = 2; + } + + enum Orientation { + ORIENTATION_UNSET = 0; + ORIENTATION_PORT = 1; + ORIENTATION_LAND = 2; + ORIENTATION_SQUARE = 3; + } + + enum UiModeType { + UI_MODE_TYPE_UNSET = 0; + UI_MODE_TYPE_NORMAL = 1; + UI_MODE_TYPE_DESK = 2; + UI_MODE_TYPE_CAR = 3; + UI_MODE_TYPE_TELEVISION = 4; + UI_MODE_TYPE_APPLIANCE = 5; + UI_MODE_TYPE_WATCH = 6; + UI_MODE_TYPE_VR_HEADSET = 7; + } + + enum UiModeNight { + UI_MODE_NIGHT_UNSET = 0; + UI_MODE_NIGHT_NIGHT = 1; + UI_MODE_NIGHT_NOTNIGHT = 2; + } + + enum Touchscreen { + TOUCHSCREEN_UNSET = 0; + TOUCHSCREEN_NOTOUCH = 1; + TOUCHSCREEN_STYLUS = 2; + TOUCHSCREEN_FINGER = 3; + } + + enum KeysHidden { + KEYS_HIDDEN_UNSET = 0; + KEYS_HIDDEN_KEYSEXPOSED = 1; + KEYS_HIDDEN_KEYSHIDDEN = 2; + KEYS_HIDDEN_KEYSSOFT = 3; + } + + enum Keyboard { + KEYBOARD_UNSET = 0; + KEYBOARD_NOKEYS = 1; + KEYBOARD_QWERTY = 2; + KEYBOARD_12KEY = 3; + } + + enum NavHidden { + NAV_HIDDEN_UNSET = 0; + NAV_HIDDEN_NAVEXPOSED = 1; + NAV_HIDDEN_NAVHIDDEN = 2; + } + + enum Navigation { + NAVIGATION_UNSET = 0; + NAVIGATION_NONAV = 1; + NAVIGATION_DPAD = 2; + NAVIGATION_TRACKBALL = 3; + NAVIGATION_WHEEL = 4; + } + + enum GrammaticalGender { + GRAMMATICAL_GENDER_UNSET = 0; + GRAMMATICAL_GENDER_NEUTER = 1; + GRAMMATICAL_GENDER_FEMININE = 2; + GRAMMATICAL_GENDER_MASCULINE = 3; + } + + // Mobile country code. + uint32 mcc = 1; + + // Mobile network code. + uint32 mnc = 2; + + // Locale. + string locale = 3; + + // Layout direction. + LayoutDirection layout_direction = 4; + + // Screen width in dp. + uint32 screen_width = 5; + + // Screen height in dp. + uint32 screen_height = 6; + + // Smallest screen width in dp. + uint32 smallest_screen_width = 7; + + // Screen layout size. + ScreenLayoutSize screen_layout_size = 8; + + // Screen layout long. + ScreenLayoutLong screen_layout_long = 9; + + // Screen round. + ScreenRound screen_round = 10; + + // Wide color gamut. + WideColorGamut wide_color_gamut = 11; + + // HDR. + Hdr hdr = 12; + + // Orientation. + Orientation orientation = 13; + + // UI mode type. + UiModeType ui_mode_type = 14; + + // UI mode night. + UiModeNight ui_mode_night = 15; + + // Density in dpi. + uint32 density = 16; + + // Touchscreen. + Touchscreen touchscreen = 17; + + // Keys hidden. + KeysHidden keys_hidden = 18; + + // Keyboard. + Keyboard keyboard = 19; + + // Nav hidden. + NavHidden nav_hidden = 20; + + // Navigation. + Navigation navigation = 21; + + // SDK version. + uint32 sdk_version = 22; + + // Product. + string product = 23; + + // Grammatical gender. + GrammaticalGender grammatical_gender = 24; +} \ No newline at end of file diff --git a/proto/Resources.proto b/proto/Resources.proto new file mode 100644 index 0000000..1f1ab52 --- /dev/null +++ b/proto/Resources.proto @@ -0,0 +1,569 @@ +syntax = "proto3"; + +package aapt.pb; + +option java_package = "com.android.aapt"; + +import "Configuration.proto"; + +// A string pool that wraps the binary form of the C++ class android::ResStringPool. +message StringPool { + bytes data = 1; +} + +// The position of a declared entity in a file. +message SourcePosition { + uint32 line_number = 1; + uint32 column_number = 2; +} + +// Developer friendly source file information for an entity in the resource table. +message Source { + // The index of the string path within the source StringPool. + uint32 path_idx = 1; + + SourcePosition position = 2; +} + +// Top level message representing a resource table. +message ResourceTable { + // The string pool containing source paths referenced by Source messages. + StringPool source_pool = 1; + + // The packages declared in this resource table. + repeated Package package = 2; + + // The overlayable declarations in this resource table. + repeated Overlayable overlayable = 3; + + // The tool fingerprints of the tools that built this resource table. + repeated ToolFingerprint tool_fingerprint = 4; +} + +// A package declaration. +message Package { + // The package ID of this package. + // This is the first byte of a resource ID (0xPPTTEEEE). + // This is optional, and if not set, the package ID is assigned by the runtime. + // For the framework, this is 0x01. For the application, this is 0x7f. + uint32 package_id = 1; + + // The Java compatible package name. + string package_name = 2; + + // The types declared in this package. + repeated Type type = 3; +} + +// A set of resources grouped under a common type (string, layout, xml, etc). +// This maps to the second byte of a resource ID (0xPPTTEEEE). +message Type { + // The type ID of this type. + // This is optional, and if not set, the type ID is assigned by the runtime. + uint32 type_id = 1; + + // The name of this type. + string name = 2; + + // The entries declared in this type. + repeated Entry entry = 3; +} + +// A resource entry declaration. +// This maps to the last two bytes of a resource ID (0xPPTTEEEE). +message Entry { + // The entry ID of this entry. + // This is optional, and if not set, the entry ID is assigned by the runtime. + uint32 entry_id = 1; + + // The name of this entry. + string name = 2; + + // The visibility of this entry. + Visibility visibility = 3; + + // The allow-new state of this entry. + AllowNew allow_new = 4; + + // The overlayable state of this entry. + OverlayableItem overlayable_item = 5; + + // The set of values defined for this entry, each with a different configuration. + repeated ConfigValue config_value = 6; +} + +// The visibility of a resource entry. +message Visibility { + enum Level { + UNKNOWN = 0; + PRIVATE = 1; + PUBLIC = 2; + } + + // The visibility level. + Level level = 1; + + // The source of the visibility declaration. + Source source = 2; + + // The comment associated with the visibility declaration. + string comment = 3; +} + +// The allow-new state of a resource entry. +message AllowNew { + // The source of the allow-new declaration. + Source source = 1; + + // The comment associated with the allow-new declaration. + string comment = 2; +} + +// The overlayable state of a resource entry. +message OverlayableItem { + enum Policy { + NONE = 0; + PUBLIC = 1; + SYSTEM = 2; + VENDOR = 3; + PRODUCT = 4; + SIGNATURE = 5; + ODM = 6; + OEM = 7; + ACTOR = 8; + CONFIG_SIGNATURE = 9; + } + + // The source of the overlayable declaration. + Source source = 1; + + // The comment associated with the overlayable declaration. + string comment = 2; + + // The policy of the overlayable declaration. + repeated Policy policy = 3; + + // The index of the overlayable declaration in the overlayable list. + uint32 overlayable_idx = 4; +} + +// A value defined for a resource entry with a specific configuration. +message ConfigValue { + // The configuration for which this value is defined. + Configuration config = 1; + + // The value of the resource. + Value value = 2; +} + +// A generic value of a resource. +message Value { + // The source of the value declaration. + Source source = 1; + + // The comment associated with the value declaration. + string comment = 2; + + // The value is a weak reference to another resource. + bool weak = 3; + + // The value is a raw string. + Item item = 4; + + // The value is a compound value. + CompoundValue compound_value = 5; +} + +// A value that is a single item. +message Item { + // The value is a reference to another resource. + Reference ref = 1; + + // The value is a string. + String str = 2; + + // The value is a raw string. + RawString raw_str = 3; + + // The value is a styled string. + StyledString styled_str = 4; + + // The value is a file. + File file = 5; + + // The value is an integer. + Id id = 6; + + // The value is a primitive. + Primitive prim = 7; +} + +// A value that is a compound value. +message CompoundValue { + // The value is an attribute. + Attribute attr = 1; + + // The value is a style. + Style style = 2; + + // The value is a styleable. + Styleable styleable = 3; + + // The value is an array. + Array array = 4; + + // The value is a plural. + Plural plural = 5; + + // The value is a macro. + MacroBody macro = 6; +} + +// A reference to another resource. +message Reference { + enum Type { + REFERENCE = 0; + ATTRIBUTE = 1; + } + + // The type of reference. + Type type = 1; + + // The resource ID being referenced. + uint32 id = 2; + + // The name of the resource being referenced. + string name = 3; + + // The private state of the reference. + bool private = 4; + + // The dynamic reference state of the reference. + bool is_dynamic = 5; +} + +// A string value. +message String { + // The string value. + string value = 1; +} + +// A raw string value. +message RawString { + // The raw string value. + string value = 1; +} + +// A styled string value. +message StyledString { + // The raw string value. + string value = 1; + + // The spans of the styled string. + repeated Span span = 2; +} + +// A span of a styled string. +message Span { + // The tag of the span. + string tag = 1; + + // The first character index of the span. + uint32 first_char = 2; + + // The last character index of the span. + uint32 last_char = 3; +} + +// A file value. +message File { + // The path to the file. + string path = 1; +} + +// An ID value. +message Id { +} + +// A primitive value. +message Primitive { + // The null value. + message NullType {} + // The empty value. + message EmptyType {} + + oneof oneof_value { + NullType null_value = 1; + EmptyType empty_value = 2; + float float_value = 3; + uint32 dimension_value = 4; + uint32 fraction_value = 5; + int32 int_decimal_value = 6; + uint32 int_hexadecimal_value = 7; + bool boolean_value = 8; + uint32 color_argb8_value = 9; + uint32 color_rgb8_value = 10; + uint32 color_argb4_value = 11; + uint32 color_rgb4_value = 12; + } +} + +// An attribute value. +message Attribute { + enum FormatFlags { + NONE = 0x0; + REFERENCE = 0x1; + STRING = 0x2; + INTEGER = 0x4; + BOOLEAN = 0x8; + COLOR = 0x10; + FLOAT = 0x20; + DIMENSION = 0x40; + FRACTION = 0x80; + ENUM = 0x10000; + FLAGS = 0x20000; + } + + // The format flags of the attribute. + uint32 format_flags = 1; + + // The minimum value of the attribute. + int32 min_int = 2; + + // The maximum value of the attribute. + int32 max_int = 3; + + // The symbols of the attribute. + repeated Symbol symbol = 4; +} + +// A symbol of an attribute. +message Symbol { + // The source of the symbol declaration. + Source source = 1; + + // The comment associated with the symbol declaration. + string comment = 2; + + // The name of the symbol. + Reference name = 3; + + // The value of the symbol. + uint32 value = 4; + + // The type of the symbol. + uint32 type = 5; +} + +// A style value. +message Style { + // The parent of the style. + Reference parent = 1; + + // The source of the parent declaration. + Source parent_source = 2; + + // The entries of the style. + repeated Entry entry = 3; + + // An entry of a style. + message Entry { + // The source of the entry declaration. + Source source = 1; + + // The comment associated with the entry declaration. + string comment = 2; + + // The key of the entry. + Reference key = 3; + + // The item of the entry. + Item item = 4; + } +} + +// A styleable value. +message Styleable { + // The entries of the styleable. + repeated Entry entry = 1; + + // An entry of a styleable. + message Entry { + // The source of the entry declaration. + Source source = 1; + + // The comment associated with the entry declaration. + string comment = 2; + + // The attribute of the entry. + Reference attr = 3; + } +} + +// An array value. +message Array { + // The elements of the array. + repeated Element element = 1; + + // An element of an array. + message Element { + // The source of the element declaration. + Source source = 1; + + // The comment associated with the element declaration. + string comment = 2; + + // The item of the element. + Item item = 3; + } +} + +// A plural value. +message Plural { + enum Arity { + ZERO = 0; + ONE = 1; + TWO = 2; + FEW = 3; + MANY = 4; + OTHER = 5; + } + + // The entries of the plural. + repeated Entry entry = 1; + + // An entry of a plural. + message Entry { + // The source of the entry declaration. + Source source = 1; + + // The comment associated with the entry declaration. + string comment = 2; + + // The arity of the entry. + Arity arity = 3; + + // The item of the entry. + Item item = 4; + } +} + +// A macro body value. +message MacroBody { + // The raw string value. + string raw_string = 1; + + // The style string value. + StyleString style_string = 2; + + // The untranslatable sections of the macro. + repeated UntranslatableSection untranslatable_section = 3; + + // The namespace declarations of the macro. + repeated NamespaceDeclaration namespace_declaration = 4; +} + +message StyleString { + string str = 1; + repeated Span spans = 2; +} + +message UntranslatableSection { + uint64 start_index = 1; + uint64 end_index = 2; +} + +message NamespaceDeclaration { + string prefix = 1; + string uri = 2; + Source source = 3; +} + +// An overlayable declaration. +message Overlayable { + // The name of the overlayable. + string name = 1; + + // The source of the overlayable declaration. + Source source = 2; + + // The actor of the overlayable declaration. + string actor = 3; +} + +// A tool fingerprint. +message ToolFingerprint { + // The tool name. + string tool = 1; + + // The version of the tool. + string version = 2; +} + +// A dynamic reference table. +message DynamicRefTable { + // The package ID to package name mapping. + repeated PackageId packageName = 1; +} + +message PackageId { + uint32 id = 1; + string name = 2; +} + +// A generic XML node. +message XmlNode { + // The element node. + XmlElement element = 1; + + // The text node. + string text = 2; + + // The source of the node. + SourcePosition source = 3; +} + +message XmlNamespace { + string prefix = 1; + string uri = 2; + SourcePosition source = 3; +} + +// An XML element. +message XmlElement { + // The namespace declarations of the element. + repeated XmlNamespace namespace_declaration = 1; + + // The namespace URI of the element. + string namespace_uri = 2; + + // The name of the element. + string name = 3; + + // The attributes of the element. + repeated XmlAttribute attribute = 4; + + // The children of the element. + repeated XmlNode child = 5; +} + +// An XML attribute. +message XmlAttribute { + // The namespace URI of the attribute. + string namespace_uri = 1; + + // The name of the attribute. + string name = 2; + + // The value of the attribute. + string value = 3; + + // The source of the attribute. + SourcePosition source = 4; + + // The resource ID of the attribute. + uint32 resource_id = 5; + + // The compiled item of the attribute. + Item compiled_item = 6; +} \ No newline at end of file diff --git a/src/locales/en.ts b/src/locales/en.ts index 1a45a11..7b6e36e 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -115,6 +115,7 @@ This can reduce the risk of inconsistent dependencies and supply chain attacks. uploadingSourcemap: 'Uploading sourcemap', usageDiff: 'Usage: cresc {{command}} ', usageParseApk: 'Usage: cresc parseApk ', + usageParseAab: 'Usage: cresc parseAab ', usageParseApp: 'Usage: cresc parseApp ', usageParseIpa: 'Usage: cresc parseIpa ', usageUnderDevelopment: 'Usage is under development now.', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index ae705a2..d5265e3 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -109,6 +109,7 @@ export default { uploadingSourcemap: '正在上传 sourcemap', usageDiff: '用法:pushy {{command}} ', usageParseApk: '使用方法: pushy parseApk apk后缀文件', + usageParseAab: '使用方法: pushy parseAab aab后缀文件', usageParseApp: '使用方法: pushy parseApp app后缀文件', usageParseIpa: '使用方法: pushy parseIpa ipa后缀文件', usageUploadApk: '使用方法: pushy uploadApk apk后缀文件', diff --git a/src/package.ts b/src/package.ts index d77d1cc..657a773 100644 --- a/src/package.ts +++ b/src/package.ts @@ -6,7 +6,7 @@ import { getPlatform, getSelectedApp } from './app'; import Table from 'tty-table'; import type { Platform } from './types'; -import { getApkInfo, getAppInfo, getIpaInfo } from './utils'; +import { getApkInfo, getAppInfo, getIpaInfo, getAabInfo } from './utils'; import { depVersions } from './utils/dep-versions'; import { getCommitInfo } from './utils/git'; @@ -207,6 +207,13 @@ export const packageCommands = { } console.log(await getApkInfo(fn)); }, + parseAab: async ({ args }: { args: string[] }) => { + const fn = args[0]; + if (!fn || !fn.endsWith('.aab')) { + throw new Error(t('usageParseAab')); + } + console.log(await getAabInfo(fn)); + }, packages: async ({ options }: { options: { platform: Platform } }) => { const platform = await getPlatform(options.platform); const { appId } = await getSelectedApp(platform); @@ -217,7 +224,7 @@ export const packageCommands = { options, }: { args: string[]; - options: { appId?: string, packageId?: string, packageVersion?: string }; + options: { appId?: string; packageId?: string; packageVersion?: string }; }) => { let { appId, packageId, packageVersion } = options; @@ -232,7 +239,9 @@ export const packageCommands = { if (!allPkgs) { throw new Error(t('noPackagesFound', { appId })); } - const selectedPackage = allPkgs.find((pkg) => pkg.name === packageVersion); + const selectedPackage = allPkgs.find( + (pkg) => pkg.name === packageVersion, + ); if (!selectedPackage) { throw new Error(t('packageNotFound', { packageVersion })); } diff --git a/src/utils/app-info-parser/aab.js b/src/utils/app-info-parser/aab.js new file mode 100644 index 0000000..29a47bc --- /dev/null +++ b/src/utils/app-info-parser/aab.js @@ -0,0 +1,326 @@ +const Zip = require('./zip'); +const yazl = require('yazl'); +const fs = require('fs-extra'); +const path = require('path'); +const { open: openZipFile } = require('yauzl'); +const os = require('os'); + +class AabParser extends Zip { + /** + * parser for parsing .aab file + * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser + */ + constructor(file) { + super(file); + if (!(this instanceof AabParser)) { + return new AabParser(file); + } + } + + /** + * 从 AAB 提取通用 APK + * 这个方法会合并 base/ 和所有 split/ 目录的内容 + * + * @param {String} outputPath - 输出 APK 文件路径 + * @param {Object} options - 选项 + * @param {Boolean} options.includeAllSplits - 是否包含所有 split APK(默认 false,只提取 base) + * @param {Array} options.splits - 指定要包含的 split APK 名称(如果指定,则只包含这些) + * @returns {Promise} 返回输出文件路径 + */ + async extractApk(outputPath, options = {}) { + const { includeAllSplits = false, splits = null } = options; + + return new Promise((resolve, reject) => { + if (typeof this.file !== 'string') { + return reject( + new Error('AAB file path must be a string in Node.js environment'), + ); + } + + openZipFile(this.file, { lazyEntries: true }, async (err, zipfile) => { + if (err) { + return reject(err); + } + + try { + // 1. 收集所有条目及其数据 + const baseEntries = []; + const splitEntries = []; + const metaInfEntries = []; + let pendingReads = 0; + let hasError = false; + + const processEntry = (entry, fileName) => { + return new Promise((resolve, reject) => { + zipfile.openReadStream(entry, (err, readStream) => { + if (err) { + return reject(err); + } + + const chunks = []; + readStream.on('data', (chunk) => chunks.push(chunk)); + readStream.on('end', () => { + const buffer = Buffer.concat(chunks); + resolve(buffer); + }); + readStream.on('error', reject); + }); + }); + }; + + zipfile.on('entry', async (entry) => { + const fileName = entry.fileName; + + // 跳过目录 + if (fileName.endsWith('/')) { + zipfile.readEntry(); + return; + } + + pendingReads++; + try { + const buffer = await processEntry(entry, fileName); + + if (fileName.startsWith('base/')) { + // 将 base/manifest/AndroidManifest.xml 转换为 androidmanifest.xml(APK 中通常是小写) + // 将 base/resources.arsc 转换为 resources.arsc + let apkPath = fileName.replace(/^base\//, ''); + if (apkPath === 'manifest/AndroidManifest.xml') { + apkPath = 'androidmanifest.xml'; + } + + baseEntries.push({ + buffer, + zipPath: fileName, + apkPath, + }); + } else if (fileName.startsWith('split/')) { + splitEntries.push({ + buffer, + zipPath: fileName, + }); + } else if (fileName.startsWith('META-INF/')) { + metaInfEntries.push({ + buffer, + zipPath: fileName, + apkPath: fileName, + }); + } + // BundleConfig.pb 和其他文件不需要包含在 APK 中 + + pendingReads--; + zipfile.readEntry(); + } catch (error) { + pendingReads--; + if (!hasError) { + hasError = true; + reject(error); + } + zipfile.readEntry(); + } + }); + + zipfile.on('end', async () => { + // 等待所有读取完成 + while (pendingReads > 0) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + if (hasError) { + return; + } + + try { + // 2. 创建新的 APK 文件 + const zipFile = new yazl.ZipFile(); + + // 3. 添加 base 目录的所有文件 + for (const { buffer, apkPath } of baseEntries) { + zipFile.addBuffer(buffer, apkPath); + } + + // 4. 添加 split APK 的内容(如果需要) + if (includeAllSplits || splits) { + const splitsToInclude = splits + ? splitEntries.filter((se) => + splits.some((s) => se.zipPath.includes(s)), + ) + : splitEntries; + + await this.mergeSplitApksFromBuffers(zipFile, splitsToInclude); + } + + // 5. 添加 META-INF(签名信息,虽然可能无效,但保留结构) + for (const { buffer, apkPath } of metaInfEntries) { + zipFile.addBuffer(buffer, apkPath); + } + + // 6. 写入文件 + zipFile.outputStream + .pipe(fs.createWriteStream(outputPath)) + .on('close', () => { + resolve(outputPath); + }) + .on('error', (err) => { + reject(err); + }); + + zipFile.end(); + } catch (error) { + reject(error); + } + }); + + zipfile.on('error', reject); + zipfile.readEntry(); + } catch (error) { + reject(error); + } + }); + }); + } + + /** + * 合并 split APK 的内容(从已读取的 buffer) + */ + async mergeSplitApksFromBuffers(zipFile, splitEntries) { + for (const { buffer: splitBuffer } of splitEntries) { + if (splitBuffer) { + // 创建一个临时的 ZIP 文件来读取 split APK + const tempSplitPath = path.join( + os.tmpdir(), + `split_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.apk`, + ); + + try { + await fs.writeFile(tempSplitPath, splitBuffer); + + await new Promise((resolve, reject) => { + openZipFile( + tempSplitPath, + { lazyEntries: true }, + async (err, splitZipfile) => { + if (err) { + return reject(err); + } + + splitZipfile.on('entry', (splitEntry) => { + // 跳过 META-INF,因为签名信息不需要合并 + if (splitEntry.fileName.startsWith('META-INF/')) { + splitZipfile.readEntry(); + return; + } + + splitZipfile.openReadStream(splitEntry, (err, readStream) => { + if (err) { + splitZipfile.readEntry(); + return; + } + + const chunks = []; + readStream.on('data', (chunk) => chunks.push(chunk)); + readStream.on('end', () => { + const buffer = Buffer.concat(chunks); + // 注意:如果文件已存在(在 base 中),split 中的会覆盖 base 中的 + zipFile.addBuffer(buffer, splitEntry.fileName); + splitZipfile.readEntry(); + }); + readStream.on('error', () => { + splitZipfile.readEntry(); + }); + }); + }); + + splitZipfile.on('end', resolve); + splitZipfile.on('error', reject); + splitZipfile.readEntry(); + }, + ); + }); + } finally { + // 清理临时文件 + await fs.remove(tempSplitPath).catch(() => {}); + } + } + } + } + + /** + * 解析 AAB 文件信息(类似 APK parser 的 parse 方法) + * 注意:AAB 中的 AndroidManifest.xml 在 base/manifest/AndroidManifest.xml + */ + async parse() { + // 尝试从 base/manifest/AndroidManifest.xml 读取 manifest + // 但 AAB 中的 manifest 可能是二进制格式,需要特殊处理 + const manifestPath = 'base/manifest/AndroidManifest.xml'; + const ResourceName = /^base\/resources\.arsc$/; + + try { + const manifestBuffer = await this.getEntry( + new RegExp(`^${manifestPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`), + ); + + if (!manifestBuffer) { + throw new Error( + "AndroidManifest.xml can't be found in AAB base/manifest/", + ); + } + + let apkInfo = this._parseManifest(manifestBuffer); + + // 尝试解析 resources.arsc + try { + const resourceBuffer = await this.getEntry(ResourceName); + if (resourceBuffer) { + const resourceMap = this._parseResourceMap(resourceBuffer); + const { mapInfoResource } = require('./utils'); + apkInfo = mapInfoResource(apkInfo, resourceMap); + } + } catch (e) { + // resources.arsc 解析失败不影响基本信息 + console.warn('[Warning] Failed to parse resources.arsc:', e.message); + } + + return apkInfo; + } catch (error) { + throw new Error(`Failed to parse AAB: ${error.message}`); + } + } + + /** + * Parse manifest + * @param {Buffer} buffer // manifest file's buffer + */ + _parseManifest(buffer) { + try { + const ManifestXmlParser = require('./xml-parser/manifest'); + const parser = new ManifestXmlParser(buffer, { + ignore: [ + 'application.activity', + 'application.service', + 'application.receiver', + 'application.provider', + 'permission-group', + ], + }); + return parser.parse(); + } catch (e) { + throw new Error('Parse AndroidManifest.xml error: ' + e.message); + } + } + + /** + * Parse resourceMap + * @param {Buffer} buffer // resourceMap file's buffer + */ + _parseResourceMap(buffer) { + try { + const ResourceFinder = require('./resource-finder'); + return new ResourceFinder().processResourceTable(buffer); + } catch (e) { + throw new Error('Parser resources.arsc error: ' + e.message); + } + } +} + +module.exports = AabParser; diff --git a/src/utils/app-info-parser/apk.js b/src/utils/app-info-parser/apk.js index fda468e..c1d3b14 100644 --- a/src/utils/app-info-parser/apk.js +++ b/src/utils/app-info-parser/apk.js @@ -81,7 +81,7 @@ class ApkParser extends Zip { }); return parser.parse(); } catch (e) { - throw new Error('Parse AndroidManifest.xml error: ', e); + throw new Error('Parse AndroidManifest.xml error: ' + (e.message || e)); } } /** diff --git a/src/utils/app-info-parser/index.ts b/src/utils/app-info-parser/index.ts index b94e515..fdbf8d2 100644 --- a/src/utils/app-info-parser/index.ts +++ b/src/utils/app-info-parser/index.ts @@ -1,7 +1,8 @@ const ApkParser = require('./apk'); const IpaParser = require('./ipa'); const AppParser = require('./app'); -const supportFileTypes = ['ipa', 'apk', 'app']; +const AabParser = require('./aab'); +const supportFileTypes = ['ipa', 'apk', 'app', 'aab']; class AppInfoParser { file: string | File; @@ -20,7 +21,7 @@ class AppInfoParser { const fileType = splits[splits.length - 1].toLowerCase(); if (!supportFileTypes.includes(fileType)) { throw new Error( - 'Unsupported file type, only support .ipa or .apk or .app file.', + 'Unsupported file type, only support .ipa, .apk, .app, or .aab file.', ); } this.file = file; @@ -35,6 +36,9 @@ class AppInfoParser { case 'app': this.parser = new AppParser(this.file); break; + case 'aab': + this.parser = new AabParser(this.file); + break; } } parse() { diff --git a/src/utils/index.ts b/src/utils/index.ts index f325afd..f01500e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -149,6 +149,160 @@ export async function getIpaInfo(fn: string) { return { versionName, buildTime, ...appCredential }; } +export async function getAabInfo(fn: string) { + const protobuf = require('protobufjs'); + const root = await protobuf.load( + path.join(__dirname, '../../proto/Resources.proto'), + ); + const XmlNode = root.lookupType('aapt.pb.XmlNode'); + + const buffer = await readZipEntry(fn, 'base/manifest/AndroidManifest.xml'); + + const message = XmlNode.decode(buffer); + const object = XmlNode.toObject(message, { + enums: String, + longs: String, + bytes: String, + defaults: true, + arrays: true, + }); + + const manifestElement = object.element; + if (manifestElement.name !== 'manifest') { + throw new Error('Invalid manifest'); + } + + let versionName = ''; + for (const attr of manifestElement.attribute) { + if (attr.name === 'versionName') { + versionName = attr.value; + } + } + + let buildTime = 0; + const appCredential = {}; + + // Find application node + const applicationNode = manifestElement.child.find( + (c: any) => c.element && c.element.name === 'application', + ); + if (applicationNode) { + const metaDataNodes = applicationNode.element.child.filter( + (c: any) => c.element && c.element.name === 'meta-data', + ); + for (const meta of metaDataNodes) { + let name = ''; + let value = ''; + let resourceId = 0; + + for (const attr of meta.element.attribute) { + if (attr.name === 'name') { + name = attr.value; + } + if (attr.name === 'value') { + value = attr.value; + if (attr.compiledItem?.ref?.id) { + resourceId = attr.compiledItem.ref.id; + } else if (attr.compiledItem?.prim?.intDecimalValue) { + value = attr.compiledItem.prim.intDecimalValue.toString(); + } + } + } + + if (name === 'pushy_build_time') { + if (resourceId > 0) { + const resolvedValue = await resolveResource(fn, resourceId, root); + if (resolvedValue) { + value = resolvedValue; + } + } + buildTime = Number(value); + } + } + } + + if (buildTime === 0) { + throw new Error(t('buildTimeNotFound')); + } + + return { versionName, buildTime, ...appCredential }; +} + +async function readZipEntry(fn: string, entryName: string): Promise { + const yauzl = require('yauzl'); + return new Promise((resolve, reject) => { + yauzl.open(fn, { lazyEntries: true }, (err: any, zipfile: any) => { + if (err) return reject(err); + let found = false; + zipfile.readEntry(); + zipfile.on('entry', (entry: any) => { + if (entry.fileName === entryName) { + found = true; + zipfile.openReadStream(entry, (err: any, readStream: any) => { + if (err) return reject(err); + const chunks: any[] = []; + readStream.on('data', (chunk: any) => chunks.push(chunk)); + readStream.on('end', () => resolve(Buffer.concat(chunks))); + readStream.on('error', reject); + }); + } else { + zipfile.readEntry(); + } + }); + zipfile.on('end', () => { + if (!found) reject(new Error(`${entryName} not found in AAB`)); + }); + zipfile.on('error', reject); + }); + }); +} + +async function resolveResource( + fn: string, + resourceId: number, + root: any, +): Promise { + const pkgId = (resourceId >> 24) & 0xff; + const typeId = (resourceId >> 16) & 0xff; + const entryId = resourceId & 0xffff; + + try { + const buffer = await readZipEntry(fn, 'base/resources.pb'); + const ResourceTable = root.lookupType('aapt.pb.ResourceTable'); + const message = ResourceTable.decode(buffer); + const object = ResourceTable.toObject(message, { + enums: String, + longs: String, + bytes: String, + defaults: true, + arrays: true, + }); + + // Find package + const pkg = object.package.find((p: any) => p.packageId === pkgId); + if (!pkg) return null; + + // Find type + const type = pkg.type.find((t: any) => t.typeId === typeId); + if (!type) return null; + + // Find entry + const entry = type.entry.find((e: any) => e.entryId === entryId); + if (!entry) return null; + + // Get value from configValue + if (entry.configValue && entry.configValue.length > 0) { + const val = entry.configValue[0].value; + if (val.item?.str) { + return val.item.str.value; + } + } + } catch (e) { + console.warn('Failed to resolve resource:', e); + } + return null; +} + const localDir = path.resolve(os.homedir(), tempDir); fs.ensureDirSync(localDir); export function saveToLocal(originPath: string, destName: string) {