Skip to content

Commit 117894b

Browse files
Add Android App Bundle (.aab) file support.
1 parent db9dd08 commit 117894b

File tree

3 files changed

+252
-0
lines changed

3 files changed

+252
-0
lines changed

src/extractcode/androidappbundle.py

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# ScanCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/extractcode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import logging
11+
import os
12+
import zipfile
13+
import attr
14+
15+
from extractcode import ExtractErrorFailedToExtract
16+
17+
"""
18+
Support to extract Android App Bundle (.aab) files.
19+
"""
20+
21+
logger = logging.getLogger(__name__)
22+
23+
TRACE = False
24+
25+
if TRACE:
26+
import sys
27+
logging.basicConfig(stream=sys.stdout)
28+
logger.setLevel(logging.DEBUG)
29+
30+
31+
@attr.s
32+
class AndroidAppBundle:
33+
location = attr.ib()
34+
extracted_dir = attr.ib(default=None)
35+
36+
@classmethod
37+
def from_file(cls, location):
38+
"""
39+
Build a new AndroidAppBundle from the file at location.
40+
Raise exceptions on errors.
41+
"""
42+
assert location
43+
abs_location = os.path.abspath(os.path.expanduser(location))
44+
45+
if not os.path.exists(abs_location):
46+
raise ExtractErrorFailedToExtract(
47+
f'The system cannot find the path specified: {abs_location}')
48+
49+
if not is_aab(abs_location):
50+
raise ExtractErrorFailedToExtract(
51+
f'Unsupported file format: {abs_location}. Expected an Android App Bundle (.aab).')
52+
53+
return cls(location=abs_location)
54+
55+
def extract(self, target_dir):
56+
"""
57+
Extract the Android App Bundle (.aab) file to the target directory.
58+
Return a dictionary mapping file paths to their sizes.
59+
Raise exceptions on errors.
60+
"""
61+
assert target_dir
62+
abs_target_dir = os.path.abspath(os.path.expanduser(target_dir))
63+
64+
if not os.path.exists(abs_target_dir) or not os.path.isdir(abs_target_dir):
65+
raise ExtractErrorFailedToExtract(
66+
f'The system cannot find the target directory path specified: {target_dir}')
67+
68+
try:
69+
with zipfile.ZipFile(self.location, 'r') as zip_ref:
70+
zip_ref.extractall(abs_target_dir)
71+
self.extracted_dir = abs_target_dir
72+
73+
# Generate a file map of extracted files and their sizes
74+
file_map = {}
75+
for root, _, files in os.walk(abs_target_dir):
76+
for file in files:
77+
file_path = os.path.join(root, file)
78+
file_size = os.path.getsize(file_path)
79+
relative_path = os.path.relpath(file_path, abs_target_dir)
80+
file_map[relative_path] = file_size
81+
82+
return file_map
83+
except Exception as e:
84+
raise ExtractErrorFailedToExtract(f'Failed to extract {self.location}: {e}')
85+
86+
def show_file_map(self):
87+
"""
88+
Show the file map of extracted files and their sizes.
89+
"""
90+
if not self.extracted_dir:
91+
raise ExtractErrorFailedToExtract('No files have been extracted yet.')
92+
93+
# Generate the file map dynamically
94+
file_map = {}
95+
for root, _, files in os.walk(self.extracted_dir):
96+
for file in files:
97+
file_path = os.path.join(root, file)
98+
file_size = os.path.getsize(file_path)
99+
relative_path = os.path.relpath(file_path, self.extracted_dir)
100+
file_map[relative_path] = file_size
101+
102+
# Print the file map
103+
for file_path, file_size in file_map.items():
104+
print(f'{file_path} ({file_size} bytes)')
105+
106+
107+
def is_aab(file_path):
108+
"""
109+
Check if a file is an Android App Bundle (.aab) by checking its extension.
110+
"""
111+
return file_path.endswith('.aab')
112+
113+
114+
def extract(location, target_dir):
115+
"""
116+
Extract an Android App Bundle (.aab) file at ``location`` to the ``target_dir`` directory.
117+
Return a dictionary mapping file paths to their sizes.
118+
Raise Exception on errors.
119+
"""
120+
assert target_dir
121+
abs_target_dir = os.path.abspath(os.path.expanduser(target_dir))
122+
123+
if not os.path.exists(abs_target_dir) or not os.path.isdir(abs_target_dir):
124+
raise ExtractErrorFailedToExtract(
125+
f'The system cannot find the target directory path specified: {target_dir}')
126+
127+
aab = AndroidAppBundle.from_file(location)
128+
return aab.extract(abs_target_dir)
4.21 MB
Binary file not shown.

tests/test_androidappbundle.py

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# ScanCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/extractcode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import os
11+
#from pathlib import Path
12+
13+
import pytest
14+
import io
15+
from contextlib import redirect_stdout
16+
17+
from extractcode_assert_utils import BaseArchiveTestCase
18+
from extractcode_assert_utils import check_files
19+
20+
from extractcode import androidappbundle as aab_extractor
21+
22+
23+
class TestExtractAAB(BaseArchiveTestCase):
24+
test_data_dir = os.path.join(os.path.dirname(__file__), 'data')
25+
26+
def test_can_extract_aab_file(self):
27+
# Path to the test .aab file
28+
test_file = self.get_test_loc('androidappbundle/app-release.aab')
29+
target_dir = self.get_temp_dir('aab_extraction')
30+
31+
# Extract the .aab file and get the file map
32+
file_map = aab_extractor.extract(test_file, target_dir)
33+
34+
# Check if expected files are extracted
35+
expected_files = [
36+
'BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb',
37+
'base/manifest/AndroidManifest.xml',
38+
'base/resources.pb',
39+
'BundleConfig.pb',
40+
]
41+
# Verify that all expected files are in the file map
42+
for expected_file in expected_files:
43+
assert expected_file in file_map, f"Expected file {expected_file} not found in the file map"
44+
45+
# Verify that the directories and files are physically created
46+
for expected_file in expected_files:
47+
# Construct the full path to the expected file
48+
full_path = os.path.join(target_dir, expected_file)
49+
# Check if the file exists
50+
assert os.path.exists(full_path), f"Expected file {full_path} does not exist"
51+
# Check if it is a file (not a directory)
52+
assert os.path.isfile(full_path), f"Expected file {full_path} is not a file"
53+
54+
# Verify that the directories are created
55+
expected_directories = [
56+
'BUNDLE-METADATA',
57+
'BUNDLE-METADATA/com.android.tools.build.libraries',
58+
'base',
59+
'base/manifest',
60+
]
61+
62+
for expected_dir in expected_directories:
63+
# Construct the full path to the expected directory
64+
full_path = os.path.join(target_dir, expected_dir)
65+
# Check if the directory exists
66+
assert os.path.exists(full_path), f"Expected directory {full_path} does not exist"
67+
# Check if it is a directory
68+
assert os.path.isdir(full_path), f"Expected directory {full_path} is not a directory"
69+
70+
def test_can_identify_aab_file(self):
71+
# Path to the test .aab file
72+
test_file = self.get_test_loc('androidappbundle/app-release.aab')
73+
74+
# Check if the file is identified as an .aab file
75+
assert aab_extractor.is_aab(test_file) == True
76+
77+
def test_extract_aab_invalid_file(self):
78+
# Create an invalid .aab file (not a zip file)
79+
invalid_file = os.path.join(self.get_temp_dir(), 'invalid.aab')
80+
with open(invalid_file, 'w') as f:
81+
f.write('This is not a valid .aab file')
82+
83+
target_dir = self.get_temp_dir('aab_extraction_invalid')
84+
85+
# Attempt to extract the invalid .aab file
86+
with pytest.raises(Exception):
87+
aab_extractor.extract(invalid_file, target_dir)
88+
89+
def test_extract_aab_nonexistent_file(self):
90+
# Define a non-existent .aab file
91+
nonexistent_file = "nonexistent.aab"
92+
target_dir = self.get_temp_dir('aab_extraction_nonexistent')
93+
94+
# Attempt to extract the non-existent .aab file
95+
with pytest.raises(aab_extractor.ExtractErrorFailedToExtract):
96+
aab_extractor.extract(nonexistent_file, target_dir)
97+
98+
def test_show_file_map(self):
99+
# Path to the test .aab file
100+
test_file = self.get_test_loc('androidappbundle/app-release.aab')
101+
target_dir = self.get_temp_dir('aab_extraction')
102+
103+
# Create an AndroidAppBundle instance and extract the .aab file
104+
aab = aab_extractor.AndroidAppBundle.from_file(test_file)
105+
file_map = aab.extract(target_dir)
106+
107+
# Verify that the file map is not empty
108+
assert file_map, "File map should not be empty after extraction"
109+
110+
# Call show_file_map() and capture the output
111+
output = io.StringIO()
112+
with redirect_stdout(output):
113+
aab.show_file_map()
114+
115+
# Verify that the output contains expected files
116+
output_str = output.getvalue()
117+
expected_files = [
118+
'BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb',
119+
'base/manifest/AndroidManifest.xml',
120+
'base/resources.pb',
121+
'BundleConfig.pb',
122+
]
123+
for expected_file in expected_files:
124+
assert expected_file in output_str, f"Expected file {expected_file} not found in the output"

0 commit comments

Comments
 (0)