Skip to content

Commit 9b49b3a

Browse files
committed
Add Android filesystem helpers
1 parent 38e552a commit 9b49b3a

File tree

5 files changed

+450
-6
lines changed

5 files changed

+450
-6
lines changed

lglpy/android/filesystem.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# SPDX-License-Identifier: MIT
2+
# -----------------------------------------------------------------------------
3+
# Copyright (c) 2024-2025 Arm Limited
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the 'Software'), to
7+
# deal in the Software without restriction, including without limitation the
8+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9+
# sell copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
# -----------------------------------------------------------------------------
23+
24+
'''
25+
This module implements higher level Android queries and utilities, built on top
26+
of the low level Android Debug Bridge wrapper.
27+
'''
28+
29+
import os
30+
import posixpath
31+
import subprocess as sp
32+
33+
from .adb import ADBConnect
34+
35+
36+
class AndroidFilesystem:
37+
'''
38+
A library of utility methods for transferring files to/from a device.
39+
'''
40+
41+
TEMP_DIR = '/data/local/tmp'
42+
DATA_PERM = '0666'
43+
EXEC_PERM = '0777'
44+
45+
@classmethod
46+
def push_file_to_tmp(
47+
cls, conn: ADBConnect, host_path: str,
48+
executable: bool = False) -> bool:
49+
'''
50+
Push a file to the device temp directory.
51+
52+
File will be copied to: /data/local/tmp/<file>.
53+
54+
Args:
55+
conn: The adb connection.
56+
host_path: The path of the file on the host file system.
57+
executable: True if the file should be configured as executable.
58+
59+
Returns:
60+
True if the file was copied, False otherwise.
61+
'''
62+
file_name = os.path.basename(host_path)
63+
64+
device_path = posixpath.join(cls.TEMP_DIR, file_name)
65+
66+
try:
67+
# Remove old file to prevent false success
68+
conn.adb('shell', 'rm', '-f', device_path)
69+
70+
# Push new file
71+
conn.adb('push', host_path, device_path)
72+
73+
# Check it actually copied
74+
conn.adb('shell', 'ls', device_path)
75+
76+
permission = cls.EXEC_PERM if executable else cls.DATA_PERM
77+
conn.adb('shell', 'chmod', permission, device_path)
78+
except sp.CalledProcessError:
79+
return False
80+
81+
return True
82+
83+
@classmethod
84+
def pull_file_from_tmp(
85+
cls, conn: ADBConnect, file_name: str,
86+
host_dir: str, delete: bool = False) -> bool:
87+
'''
88+
Pull a file from the device temp directory to a host directory.
89+
90+
File will be copied to: <host_dir>/<file>.
91+
92+
Args:
93+
conn: The adb connection.
94+
file_name: The name of the file in the tmp directory.
95+
host_path: The destination directory on the host file system.
96+
Host directory will be created if it doesn't exist.
97+
delete: Delete the file on the device after copying it.
98+
99+
Returns:
100+
True if the file was copied, False otherwise.
101+
'''
102+
host_dir = os.path.abspath(host_dir)
103+
os.makedirs(host_dir, exist_ok=True)
104+
105+
device_path = posixpath.join(cls.TEMP_DIR, file_name)
106+
107+
try:
108+
conn.adb('pull', device_path, host_dir)
109+
110+
if delete:
111+
cls.delete_file_in_tmp(conn, file_name)
112+
except sp.CalledProcessError:
113+
return False
114+
115+
return True
116+
117+
@classmethod
118+
def delete_file_in_tmp(cls, conn: ADBConnect, file_name: str) -> bool:
119+
'''
120+
Delete a file from the device temp directory.
121+
122+
File will be deleted from: /data/local/tmp/<file>.
123+
124+
Args:
125+
conn: The adb connection.
126+
file_name: The name of the file to delete.
127+
128+
Returns:
129+
True if the file was deleted, False otherwise.
130+
'''
131+
device_path = posixpath.join(cls.TEMP_DIR, file_name)
132+
133+
try:
134+
# Remove old file to prevent false success
135+
conn.adb('shell', 'rm', '-f', device_path)
136+
except sp.CalledProcessError:
137+
return False
138+
139+
return True
140+
141+
@staticmethod
142+
def push_file_to_package(
143+
conn: ADBConnect, package: str, host_path: str,
144+
executable: bool = False) -> bool:
145+
'''
146+
Push a file to the package data directory.
147+
148+
File will be copied to, e.g.: /data/user/0/<package>/<file>
149+
150+
Args:
151+
conn: The adb connection.
152+
package: The name of the package.
153+
host_path: The path of the file on the host file system.
154+
executable: True if the file should be configured as executable.
155+
156+
Returns:
157+
True if the file was copied, False otherwise.
158+
'''
159+
# TODO
160+
return False
161+
162+
@staticmethod
163+
def pull_file_from_package(
164+
conn: ADBConnect, package: str,
165+
src_file: str, host_dir: str) -> bool:
166+
'''
167+
Pull a file from the package data directory to a host directory.
168+
169+
File will be copied to: <host_dir>/<file>.
170+
171+
Args:
172+
conn: The adb connection.
173+
package: The name of the package.
174+
src_file: The name of the file in the tmp directory.
175+
host_path: The destination directory on the host file system.
176+
Host directory will be created if it doesn't exist.
177+
178+
Returns:
179+
True if the file was copied, False otherwise.
180+
'''
181+
# TODO
182+
return False

lglpy/android/test.py

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,33 @@
3030
import contextlib
3131
import os
3232
import re
33+
import shutil
34+
import subprocess as sp
3335
import sys
3436
import tempfile
3537
import unittest
3638

3739
from .adb import ADBConnect
3840
from .utils import AndroidUtils
41+
from .filesystem import AndroidFilesystem
3942

4043
SLOW_TESTS = False # Set to True to enable slot tests, False to skip them
4144

4245

46+
def get_script_relative_path(file_name: str) -> str:
47+
'''
48+
Get the host path of a script relative file.
49+
50+
Args:
51+
file_name: The path of the file relative to this script.
52+
53+
Returns:
54+
The path of the file on disk.
55+
'''
56+
dir_name = os.path.dirname(__file__)
57+
return os.path.join(dir_name, file_name)
58+
59+
4360
@contextlib.contextmanager
4461
def NamedTempFile(): # pylint: disable=invalid-name
4562
'''
@@ -196,7 +213,7 @@ def test_util_device_list(self):
196213
self.assertGreaterEqual(len(devices[1]), 0)
197214

198215

199-
class AndroidTestDefaultDevice(unittest.TestCase):
216+
class AndroidTestDeviceUtil(unittest.TestCase):
200217
'''
201218
This set of tests validates execution of device-level commands that
202219
require adb to have a valid implicit default device connected.
@@ -274,6 +291,167 @@ def test_util_device_model(self):
274291
self.assertTrue(version[0])
275292
self.assertTrue(version[1])
276293

294+
def test_util_package_debuggable(self):
295+
'''
296+
Test helper to get package debug status
297+
'''
298+
conn = ADBConnect()
299+
300+
# Fetch some packages that we can use
301+
all_packages = AndroidUtils.get_packages(conn, False, False)
302+
self.assertGreater(len(all_packages), 0)
303+
304+
dbg_packages = AndroidUtils.get_packages(conn, True, False)
305+
self.assertGreater(len(dbg_packages), 0)
306+
307+
ndbg_packages = list(set(all_packages) ^ set(dbg_packages))
308+
self.assertGreater(len(ndbg_packages), 0)
309+
310+
# Test the package
311+
is_debug = AndroidUtils.is_package_debuggable(conn, ndbg_packages[0])
312+
self.assertFalse(is_debug)
313+
314+
is_debug = AndroidUtils.is_package_debuggable(conn, dbg_packages[0])
315+
self.assertTrue(is_debug)
316+
317+
def test_util_package_bitness(self):
318+
'''
319+
Test helper to get package ABI bitness.
320+
'''
321+
conn = ADBConnect()
322+
323+
# Fetch some packages that we can use
324+
packages = AndroidUtils.get_packages(conn, True, False)
325+
self.assertGreater(len(packages), 0)
326+
327+
# Test the package
328+
is_32bit = AndroidUtils.is_package_32bit(conn, packages[0])
329+
self.assertTrue(isinstance(is_32bit, bool))
330+
331+
def test_util_package_data_dir(self):
332+
'''
333+
Test helper to get package data directory on the device filesystem.
334+
'''
335+
conn = ADBConnect()
336+
337+
# Fetch some packages that we can use
338+
packages = AndroidUtils.get_packages(conn, True, False)
339+
self.assertGreater(len(packages), 0)
340+
341+
# Test the package
342+
data_dir = AndroidUtils.get_package_data_dir(conn, packages[0])
343+
self.assertTrue(data_dir)
344+
345+
346+
class AndroidTestDeviceFilesystem(unittest.TestCase):
347+
'''
348+
This set of tests validates execution of device-level filesystem operations
349+
that require adb to have a valid implicit default device connected.
350+
'''
351+
352+
HOST_DEST_DIR = 'x_test_tmp'
353+
354+
def tearDown(self):
355+
'''
356+
Post-test cleanup.
357+
'''
358+
shutil.rmtree(self.HOST_DEST_DIR, True)
359+
360+
def test_util_copy_to_device_tmp(self):
361+
'''
362+
Test filesystem copy to device temp directory.
363+
'''
364+
conn = ADBConnect()
365+
366+
test_file = 'test_data.txt'
367+
test_path = get_script_relative_path(test_file)
368+
device_file = f'/data/local/tmp/{test_file}'
369+
370+
# Push the file
371+
success = AndroidFilesystem.push_file_to_tmp(conn, test_path, False)
372+
self.assertTrue(success)
373+
374+
# Validate it pushed OK
375+
data = conn.adb('shell', 'cat', device_file)
376+
self.assertEqual(data.strip(), 'test payload')
377+
378+
# Cleanup
379+
success = AndroidFilesystem.delete_file_in_tmp(conn, test_file)
380+
self.assertTrue(success)
381+
382+
def test_util_copy_to_device_tmp_exec(self):
383+
'''
384+
Test filesystem copy executable payload to device temp directory.
385+
'''
386+
conn = ADBConnect()
387+
388+
test_file = 'test_data.sh'
389+
test_path = get_script_relative_path(test_file)
390+
device_file = f'/data/local/tmp/{test_file}'
391+
392+
# Push the file with executable permissions
393+
success = AndroidFilesystem.push_file_to_tmp(conn, test_path, True)
394+
self.assertTrue(success)
395+
396+
# Validate it pushed OK
397+
data = conn.adb('shell', device_file)
398+
self.assertEqual(data.strip(), 'test payload exec')
399+
400+
# Cleanup
401+
success = AndroidFilesystem.delete_file_in_tmp(conn, test_file)
402+
self.assertTrue(success)
403+
404+
def test_util_copy_from_device_keep(self):
405+
'''
406+
Test filesystem copy executable payload from device temp directory.
407+
'''
408+
conn = ADBConnect()
409+
410+
test_file = 'test_data.txt'
411+
test_path = get_script_relative_path(test_file)
412+
413+
# Push the file
414+
success = AndroidFilesystem.push_file_to_tmp(conn, test_path, False)
415+
self.assertTrue(success)
416+
417+
# Copy the file without deletion
418+
success = AndroidFilesystem.pull_file_from_tmp(
419+
conn, test_file, self.HOST_DEST_DIR, False)
420+
self.assertTrue(success)
421+
422+
# Cleanup
423+
success = AndroidFilesystem.delete_file_in_tmp(conn, test_file)
424+
self.assertTrue(success)
425+
426+
def test_util_copy_from_device_delete(self):
427+
'''
428+
Test filesystem copy executable payload from device temp directory.
429+
'''
430+
conn = ADBConnect()
431+
432+
test_file = 'test_data.txt'
433+
test_path = get_script_relative_path(test_file)
434+
435+
device_path = f'/data/local/tmp/{test_file}'
436+
host_path = f'{self.HOST_DEST_DIR}/{test_file}'
437+
438+
# Push the file
439+
success = AndroidFilesystem.push_file_to_tmp(conn, test_path, False)
440+
self.assertTrue(success)
441+
442+
# Copy the file with deletion
443+
success = AndroidFilesystem.pull_file_from_tmp(
444+
conn, test_file, self.HOST_DEST_DIR, True)
445+
self.assertTrue(success)
446+
447+
with open(host_path, 'r', encoding='utf-8') as handle:
448+
data = handle.read()
449+
self.assertEqual(data, 'test payload')
450+
451+
# Check the file is deleted - this should fail
452+
with self.assertRaises(sp.CalledProcessError):
453+
conn.adb('shell', 'ls', device_path)
454+
277455

278456
def main():
279457
'''

lglpy/android/test_data.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
echo test payload exec

lglpy/android/test_data.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test payload

0 commit comments

Comments
 (0)