<?php
class
ApkParser{
public
function
open(
$apk_file
,
$xml_file
=
'AndroidManifest.xml'
){
$zip
=
new
\ZipArchive;
if
(
$zip
->open(
$apk_file
) === TRUE) {
$xml
=
$zip
->getFromName(
$xml_file
);
$zip
->close();
if
(
$xml
){
try
{
return
$this
->parseString(
$xml
);
}
catch
(Exception
$e
){
}
}
}
return
false;
}
public
function
parseString(
$xml
){
$this
->xml =
$xml
;
$this
->length =
strlen
(
$xml
);
$this
->root =
$this
->parseBlock(self::AXML_FILE);
return
true;
}
public
function
getXML(
$node
=NULL,
$lv
=-1){
if
(
$lv
== -1)
$node
=
$this
->root;
if
(!
$node
)
return
''
;
if
(
$node
[
'type'
] == self::END_TAG)
$lv
--;
$xml
= @(
$node
[
'line'
] == 0 ||
$node
[
'line'
] ==
$this
->line) ?
''
:
"\n"
.
str_repeat
(
' '
,
$lv
);
$xml
.=
$node
[
'tag'
];
$this
->line = @
$node
[
'line'
];
foreach
(
$node
[
'child'
]
as
$c
){
$xml
.=
$this
->getXML(
$c
,
$lv
+1);
}
return
$xml
;
}
public
function
getPackage(){
return
$this
->getAttribute(
'manifest'
,
'package'
);
}
public
function
getVersionName(){
return
$this
->getAttribute(
'manifest'
,
'android:versionName'
);
}
public
function
getVersionCode(){
return
$this
->getAttribute(
'manifest'
,
'android:versionCode'
);
}
public
function
getAppName(){
return
$this
->getAttribute(
'manifest/application'
,
'android:name'
);
}
public
function
getMainActivity(){
for
(
$id
=0; true;
$id
++){
$act
=
$this
->getAttribute(
"manifest/application/activity[{$id}]/intent-filter/action"
,
'android:name'
);
if
(!
$act
)
break
;
if
(
$act
==
'android.intent.action.MAIN'
)
return
$this
->getActivity(
$id
);
}
return
NULL;
}
public
function
getActivity(
$idx
=0){
$idx
=
intval
(
$idx
);
return
$this
->getAttribute(
"manifest/application/activity[{$idx}]"
,
'android:name'
);
}
public
function
getAttribute(
$path
,
$name
){
$r
=
$this
->getElement(
$path
);
if
(
is_null
(
$r
))
return
NULL;
if
(isset(
$r
[
'attrs'
])){
foreach
(
$r
[
'attrs'
]
as
$a
){
if
(
$a
[
'ns_name'
] ==
$name
)
return
$this
->getAttributeValue(
$a
);
}
}
return
NULL;
}
const
AXML_FILE = 0x00080003;
const
STRING_BLOCK = 0x001C0001;
const
RESOURCEIDS = 0x00080180;
const
START_NAMESPACE = 0x00100100;
const
END_NAMESPACE = 0x00100101;
const
START_TAG = 0x00100102;
const
END_TAG = 0x00100103;
const
TEXT = 0x00100104;
const
TYPE_NULL =0;
const
TYPE_REFERENCE =1;
const
TYPE_ATTRIBUTE =2;
const
TYPE_STRING =3;
const
TYPE_FLOAT =4;
const
TYPE_DIMENSION =5;
const
TYPE_FRACTION =6;
const
TYPE_INT_DEC =16;
const
TYPE_INT_HEX =17;
const
TYPE_INT_BOOLEAN =18;
const
TYPE_INT_COLOR_ARGB8 =28;
const
TYPE_INT_COLOR_RGB8 =29;
const
TYPE_INT_COLOR_ARGB4 =30;
const
TYPE_INT_COLOR_RGB4 =31;
const
UNIT_MASK = 15;
private
static
$RADIX_MULTS
=
array
(0.00390625, 3.051758E-005, 1.192093E-007, 4.656613E-010);
private
static
$DIMENSION_UNITS
=
array
(
"px"
,
"dip"
,
"sp"
,
"pt"
,
"in"
,
"mm"
,
""
,
""
);
private
static
$FRACTION_UNITS
=
array
(
"%"
,
"%p"
,
""
,
""
,
""
,
""
,
""
,
""
);
private
$xml
=
''
;
private
$length
= 0;
private
$stringCount
= 0;
private
$styleCount
= 0;
private
$stringTab
=
array
();
private
$styleTab
=
array
();
private
$resourceIDs
=
array
();
private
$ns
=
array
();
private
$cur_ns
= NULL;
private
$root
= NULL;
private
$line
= 0;
private
function
getElement(
$path
){
if
(!
$this
->root)
return
NULL;
$ps
=
explode
(
'/'
,
$path
);
$r
=
$this
->root;
foreach
(
$ps
as
$v
){
if
(preg_match(
'/([^\[]+)\[([0-9]+)\]$/'
,
$v
,
$ms
)){
$v
=
$ms
[1];
$off
=
$ms
[2];
}
else
{
$off
= 0;
}
foreach
(
$r
[
'child'
]
as
$c
){
if
(
$c
[
'type'
] == self::START_TAG &&
$c
[
'ns_name'
] ==
$v
){
if
(
$off
== 0){
$r
=
$c
;
continue
2;
}
else
{
$off
--;
}
}
}
return
NULL;
}
return
$r
;
}
private
function
parseBlock(
$need
= 0){
$o
= 0;
$type
=
$this
->get32(
$o
);
if
(
$need
&&
$type
!=
$need
)
throw
new
Exception(
'Block Type Error'
, 1);
$size
=
$this
->get32(
$o
);
if
(
$size
< 8 ||
$size
>
$this
->length)
throw
new
Exception(
'Block Size Error'
, 2);
$left
=
$this
->length -
$size
;
$props
= false;
switch
(
$type
){
case
self::AXML_FILE:
$props
=
array
(
'line'
=> 0,
'tag'
=>
'<?xml version="1.0" encoding="utf-8"?>'
);
break
;
case
self::STRING_BLOCK:
$this
->stringCount =
$this
->get32(
$o
);
$this
->styleCount =
$this
->get32(
$o
);
$o
+= 4;
$strOffset
=
$this
->get32(
$o
);
$styOffset
=
$this
->get32(
$o
);
$strListOffset
=
$this
->get32array(
$o
,
$this
->stringCount);
$styListOffset
=
$this
->get32array(
$o
,
$this
->styleCount);
$this
->stringTab =
$this
->stringCount > 0 ?
$this
->getStringTab(
$strOffset
,
$strListOffset
) :
array
();
$this
->styleTab =
$this
->styleCount > 0 ?
$this
->getStringTab(
$styOffset
,
$styListOffset
) :
array
();
$o
=
$size
;
break
;
case
self::RESOURCEIDS:
$count
=
$size
/ 4 - 2;
$this
->resourceIDs =
$this
->get32array(
$o
,
$count
);
break
;
case
self::START_NAMESPACE:
$o
+= 8;
$prefix
=
$this
->get32(
$o
);
$uri
=
$this
->get32(
$o
);
if
(
empty
(
$this
->cur_ns)){
$this
->cur_ns =
array
();
$this
->ns[] = &
$this
->cur_ns;
}
$this
->cur_ns[
$uri
] =
$prefix
;
break
;
case
self::END_NAMESPACE:
$o
+= 8;
$prefix
=
$this
->get32(
$o
);
$uri
=
$this
->get32(
$o
);
if
(
empty
(
$this
->cur_ns))
break
;
unset(
$this
->cur_ns[
$uri
]);
break
;
case
self::START_TAG:
$line
=
$this
->get32(
$o
);
$o
+= 4;
$attrs
=
array
();
$props
=
array
(
'line'
=>
$line
,
'ns'
=>
$this
->getNameSpace(
$this
->get32(
$o
)),
'name'
=>
$this
->getString(
$this
->get32(
$o
)),
'flag'
=>
$this
->get32(
$o
),
'count'
=>
$this
->get16(
$o
),
'id'
=>
$this
->get16(
$o
)-1,
'class'
=>
$this
->get16(
$o
)-1,
'style'
=>
$this
->get16(
$o
)-1,
'attrs'
=> &
$attrs
);
$props
[
'ns_name'
] =
$props
[
'ns'
].
$props
[
'name'
];
for
(
$i
=0;
$i
<
$props
[
'count'
];
$i
++){
$a
=
array
(
'ns'
=>
$this
->getNameSpace(
$this
->get32(
$o
)),
'name'
=>
$this
->getString(
$this
->get32(
$o
)),
'val_str'
=>
$this
->get32(
$o
),
'val_type'
=>
$this
->get32(
$o
),
'val_data'
=>
$this
->get32(
$o
)
);
$a
[
'ns_name'
] =
$a
[
'ns'
].
$a
[
'name'
];
$a
[
'val_type'
] >>= 24;
$attrs
[] =
$a
;
}
$tag
=
"<{$props['ns_name']}"
;
foreach
(
$this
->cur_ns
as
$uri
=>
$prefix
){
$uri
=
$this
->getString(
$uri
);
$prefix
=
$this
->getString(
$prefix
);
$tag
.=
" xmlns:{$prefix}=\"{$uri}\""
;
}
foreach
(
$props
[
'attrs'
]
as
$a
){
$tag
.=
" {$a['ns_name']}=\""
.
$this
->getAttributeValue(
$a
).
'"'
;
}
$tag
.=
'>'
;
$props
[
'tag'
] =
$tag
;
unset(
$this
->cur_ns);
$this
->cur_ns =
array
();
$this
->ns[] = &
$this
->cur_ns;
$left
= -1;
break
;
case
self::END_TAG:
$line
=
$this
->get32(
$o
);
$o
+= 4;
$props
=
array
(
'line'
=>
$line
,
'ns'
=>
$this
->getNameSpace(
$this
->get32(
$o
)),
'name'
=>
$this
->getString(
$this
->get32(
$o
))
);
$props
[
'ns_name'
] =
$props
[
'ns'
].
$props
[
'name'
];
$props
[
'tag'
] =
"</{$props['ns_name']}>"
;
if
(
count
(
$this
->ns) > 1){
array_pop
(
$this
->ns);
unset(
$this
->cur_ns);
$this
->cur_ns =
array_pop
(
$this
->ns);
$this
->ns[] = &
$this
->cur_ns;
}
break
;
case
self::TEXT:
$o
+= 8;
$props
=
array
(
'tag'
=>
$this
->getString(
$this
->get32(
$o
))
);
$o
+= 8;
break
;
default
:
throw
new
Exception(
'Block Type Error'
, 3);
break
;
}
$this
->skip(
$o
);
$child
=
array
();
while
(
$this
->length >
$left
){
$c
=
$this
->parseBlock();
if
(
$props
&&
$c
)
$child
[] =
$c
;
if
(
$left
== -1 &&
$c
[
'type'
] == self::END_TAG){
$left
=
$this
->length;
break
;
}
}
if
(
$this
->length !=
$left
)
throw
new
Exception(
'Block Overflow Error'
, 4);
if
(
$props
){
$props
[
'type'
] =
$type
;
$props
[
'size'
] =
$size
;
$props
[
'child'
] =
$child
;
return
$props
;
}
else
{
return
false;
}
}
private
function
getAttributeValue(
$a
){
$type
= &
$a
[
'val_type'
];
$data
= &
$a
[
'val_data'
];
switch
(
$type
){
case
self::TYPE_STRING:
return
$this
->getString(
$a
[
'val_str'
]);
case
self::TYPE_ATTRIBUTE:
return
sprintf(
'?%s%08X'
, self::_getPackage(
$data
),
$data
);
case
self::TYPE_REFERENCE:
return
sprintf(
'@%s%08X'
, self::_getPackage(
$data
),
$data
);
case
self::TYPE_INT_HEX:
return
sprintf(
'0x%08X'
,
$data
);
case
self::TYPE_INT_BOOLEAN:
return
(
$data
!= 0 ?
'true'
:
'false'
);
case
self::TYPE_INT_COLOR_ARGB8:
case
self::TYPE_INT_COLOR_RGB8:
case
self::TYPE_INT_COLOR_ARGB4:
case
self::TYPE_INT_COLOR_RGB4:
return
sprintf(
'#%08X'
,
$data
);
case
self::TYPE_DIMENSION:
return
$this
->_complexToFloat(
$data
).self::
$DIMENSION_UNITS
[
$data
& self::UNIT_MASK];
case
self::TYPE_FRACTION:
return
$this
->_complexToFloat(
$data
).self::
$FRACTION_UNITS
[
$data
& self::UNIT_MASK];
case
self::TYPE_FLOAT:
return
$this
->_int2float(
$data
);
}
if
(
$type
>=self::TYPE_INT_DEC &&
$type
< self::TYPE_INT_COLOR_ARGB8){
return
(string)
$data
;
}
return
sprintf(
'<0x%X, type 0x%02X>'
,
$data
,
$type
);
}
private
function
_complexToFloat(
$data
){
return
(float)(
$data
& 0xFFFFFF00) * self::
$RADIX_MULTS
[(
$data
>>4) & 3];
}
private
function
_int2float(
$v
) {
$x
= (
$v
& ((1 << 23) - 1)) + (1 << 23) * (
$v
>> 31 | 1);
$exp
= (
$v
>> 23 & 0xFF) - 127;
return
$x
* pow(2,
$exp
- 23);
}
private
static
function
_getPackage(
$data
){
return
(
$data
>> 24 == 1) ?
'android:'
:
''
;
}
private
function
getStringTab(
$base
,
$list
){
$tab
=
array
();
foreach
(
$list
as
$off
){
$off
+=
$base
;
$len
=
$this
->get16(
$off
);
$mask
= (
$len
>> 0x8) & 0xFF;
$len
=
$len
& 0xFF;
if
(
$len
==
$mask
){
if
(
$off
+
$len
>
$this
->length)
throw
new
Exception(
'String Table Overflow'
, 11);
$tab
[] =
substr
(
$this
->xml,
$off
,
$len
);
}
else
{
if
(
$off
+
$len
* 2 >
$this
->length)
throw
new
Exception(
'String Table Overflow'
, 11);
$str
=
substr
(
$this
->xml,
$off
,
$len
* 2);
$tab
[] = mb_convert_encoding(
$str
,
'UTF-8'
,
'UCS-2LE'
);
}
}
return
$tab
;
}
private
function
getString(
$id
){
if
(
$id
> -1 &&
$id
<
$this
->stringCount){
return
$this
->stringTab[
$id
];
}
else
{
return
''
;
}
}
private
function
getNameSpace(
$uri
){
for
(
$i
=
count
(
$this
->ns);
$i
> 0; ){
$ns
=
$this
->ns[--
$i
];
if
(isset(
$ns
[
$uri
])){
$ns
=
$this
->getString(
$ns
[
$uri
]);
if
(!
empty
(
$ns
))
$ns
.=
':'
;
return
$ns
;
}
}
return
''
;
}
private
function
get32(&
$off
){
$int
= unpack(
'V'
,
substr
(
$this
->xml,
$off
, 4));
$off
+= 4;
return
array_shift
(
$int
);
}
private
function
get32array(&
$off
,
$size
){
if
(
$size
<= 0)
return
NULL;
$arr
= unpack(
'V*'
,
substr
(
$this
->xml,
$off
, 4 *
$size
));
if
(
count
(
$arr
) !=
$size
)
throw
new
Exception(
'Array Size Error'
, 10);
$off
+= 4 *
$size
;
return
$arr
;
}
private
function
get16(&
$off
){
$int
= unpack(
'v'
,
substr
(
$this
->xml,
$off
, 2));
$off
+= 2;
return
array_shift
(
$int
);
}
private
function
skip(
$size
){
$this
->xml =
substr
(
$this
->xml,
$size
);
$this
->length -=
$size
;
}
}
?>