/
TermuxFileUtils.java
377 lines (339 loc) · 19.7 KB
/
TermuxFileUtils.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
package com.termux.shared.file;
import android.content.Context;
import android.os.Environment;
import androidx.annotation.NonNull;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.models.errors.Error;
import com.termux.shared.models.errors.FileUtilsErrno;
import com.termux.shared.shell.TermuxShellEnvironmentClient;
import com.termux.shared.shell.TermuxTask;
import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class TermuxFileUtils {
private static final String LOG_TAG = "TermuxFileUtils";
/**
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
*
* @param paths The {@code paths} to expand.
* @return Returns the {@code expand paths}.
*/
public static List<String> getExpandedTermuxPaths(List<String> paths) {
if (paths == null) return null;
List<String> expandedPaths = new ArrayList<>();
for (int i = 0; i < paths.size(); i++) {
expandedPaths.add(getExpandedTermuxPath(paths.get(i)));
}
return expandedPaths;
}
/**
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
*
* @param path The {@code path} to expand.
* @return Returns the {@code expand path}.
*/
public static String getExpandedTermuxPath(String path) {
if (path != null && !path.isEmpty()) {
path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH);
path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/");
path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH);
path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/");
}
return path;
}
/**
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
*
* @param paths The {@code paths} to unexpand.
* @return Returns the {@code unexpand paths}.
*/
public static List<String> getUnExpandedTermuxPaths(List<String> paths) {
if (paths == null) return null;
List<String> unExpandedPaths = new ArrayList<>();
for (int i = 0; i < paths.size(); i++) {
unExpandedPaths.add(getUnExpandedTermuxPath(paths.get(i)));
}
return unExpandedPaths;
}
/**
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
*
* @param path The {@code path} to unexpand.
* @return Returns the {@code unexpand path}.
*/
public static String getUnExpandedTermuxPath(String path) {
if (path != null && !path.isEmpty()) {
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_PREFIX_DIR_PATH) + "/", "\\$PREFIX/");
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_HOME_DIR_PATH) + "/", "~/");
}
return path;
}
/**
* Get canonical path.
*
* @param path The {@code path} to convert.
* @param prefixForNonAbsolutePath Optional prefix path to prefix before non-absolute paths. This
* can be set to {@code null} if non-absolute paths should
* be prefixed with "/". The call to {@link File#getCanonicalPath()}
* will automatically do this anyways.
* @param expandPath The {@code boolean} that decides if input path is first attempted to be expanded by calling
* {@link TermuxFileUtils#getExpandedTermuxPath(String)} before its passed to
* {@link FileUtils#getCanonicalPath(String, String)}.
* @return Returns the {@code canonical path}.
*/
public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath, final boolean expandPath) {
if (path == null) path = "";
if (expandPath)
path = getExpandedTermuxPath(path);
return FileUtils.getCanonicalPath(path, prefixForNonAbsolutePath);
}
/**
* Check if {@code path} is under the allowed termux working directory paths. If it is, then
* allowed parent path is returned.
*
* @param path The {@code path} to check.
* @return Returns the allowed path if it {@code path} is under it, otherwise {@link TermuxConstants#TERMUX_FILES_DIR_PATH}.
*/
public static String getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String path) {
if (path == null || path.isEmpty()) return TermuxConstants.TERMUX_FILES_DIR_PATH;
if (path.startsWith(TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH + "/")) {
return TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH;
} if (path.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath() + "/")) {
return Environment.getExternalStorageDirectory().getAbsolutePath();
} else if (path.startsWith("/sdcard/")) {
return "/sdcard";
} else {
return TermuxConstants.TERMUX_FILES_DIR_PATH;
}
}
/**
* Validate the existence and permissions of directory file at path as a working directory for
* termux app.
*
* The creation of missing directory and setting of missing permissions will only be done if
* {@code path} is under paths returned by {@link #getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String)}.
*
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
*
* @param label The optional label for the directory file. This can optionally be {@code null}.
* @param filePath The {@code path} for file to validate or create. Symlinks will not be followed.
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setPermissions The {@code boolean} that decides if permissions are to be
* automatically set defined by {@code permissionsToCheck}.
* @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
* are to be set or if they should be overridden.
* @param ignoreErrorsIfPathIsInParentDirPath The {@code boolean} that decides if existence
* and permission errors are to be ignored if path is
* in {@code parentDirPath}.
* @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
* error is to be ignored. This allows making an attempt to set
* executable permissions, but ignoring if it fails.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error validateDirectoryFileExistenceAndPermissions(String label, final String filePath, final boolean createDirectoryIfMissing,
final boolean setPermissions, final boolean setMissingPermissionsOnly,
final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) {
return FileUtils.validateDirectoryFileExistenceAndPermissions(label, filePath,
TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(filePath), createDirectoryIfMissing,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setPermissions, setMissingPermissionsOnly,
ignoreErrorsIfPathIsInParentDirPath, ignoreIfNotExecutable);
}
/**
* Validate if {@link TermuxConstants#TERMUX_FILES_DIR_PATH} exists and has
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
*
* This is required because binaries compiled for termux are hard coded with
* {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and the path must be accessible.
*
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
*
* This function does not create the directory manually but by calling {@link Context#getFilesDir()}
* so that android itself creates it. However, the call will not create its parent package
* data directory `/data/user/0/[package_name]` if it does not already exist and a `logcat`
* error will be logged by android.
* {@code Failed to ensure /data/user/0/<package_name>/files: mkdir failed: ENOENT (No such file or directory)}
* An android app normally can't create the package data directory since its parent `/data/user/0`
* is owned by `system` user and is normally created at app install or update time and not at app startup.
*
* Note that the path returned by {@link Context#getFilesDir()} may
* be under `/data/user/[id]/[package_name]` instead of `/data/data/[package_name]`
* defined by default by {@link TermuxConstants#TERMUX_FILES_DIR_PATH} where id will be 0 for
* primary user and a higher number for other users/profiles. If app is running under work profile
* or secondary user, then {@link TermuxConstants#TERMUX_FILES_DIR_PATH} will not be accessible
* and will not be automatically created, unless there is a bind mount from `/data/data` to
* `/data/user/[id]`, ideally in the right namespace.
* https://source.android.com/devices/tech/admin/multi-user
*
*
* On Android version `<=10`, the `/data/user/0` is a symlink to `/data/data` directory.
* https://cs.android.com/android/platform/superproject/+/android-10.0.0_r47:system/core/rootdir/init.rc;l=589
* {@code
* symlink /data/data /data/user/0
* }
*
* {@code
* /system/bin/ls -lhd /data/data /data/user/0
* drwxrwx--x 179 system system 8.0K 2021-xx-xx xx:xx /data/data
* lrwxrwxrwx 1 root root 10 2021-xx-xx xx:xx /data/user/0 -> /data/data
* }
*
* On Android version `>=11`, the `/data/data` directory is bind mounted at `/data/user/0`.
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:system/core/rootdir/init.rc;l=705
* https://cs.android.com/android/_/android/platform/system/core/+/3cca270e95ca8d8bc8b800e2b5d7da1825fd7100
* {@code
* # Unlink /data/user/0 if we previously symlink it to /data/data
* rm /data/user/0
*
* # Bind mount /data/user/0 to /data/data
* mkdir /data/user/0 0700 system system encryption=None
* mount none /data/data /data/user/0 bind rec
* }
*
* {@code
* /system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1
* 87 32 253:5 / /data rw,nosuid,nodev,noatime shared:27 - ext4 /dev/block/dm-5 rw,seclabel,resgid=1065,errors=panic
* 91 87 253:5 /data /data/user/0 rw,nosuid,nodev,noatime shared:27 - ext4 /dev/block/dm-5 rw,seclabel,resgid=1065,errors=panic
* }
*
* The column 4 defines the root of the mount within the filesystem.
* Basically, `/dev/block/dm-5/` is mounted at `/data` and `/dev/block/dm-5/data` is mounted at
* `/data/user/0`.
* https://www.kernel.org/doc/Documentation/filesystems/proc.txt (section 3.5)
* https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
* https://unix.stackexchange.com/a/571959
*
*
* Also note that running `/system/bin/ls -lhd /data/user/0/com.termux` as secondary user will result
* in `ls: /data/user/0/com.termux: Permission denied` where `0` is primary user id but running
* `/system/bin/ls -lhd /data/user/10/com.termux` will result in
* `drwx------ 6 u10_a149 u10_a149 4.0K 2021-xx-xx xx:xx /data/user/10/com.termux` where `10` is
* secondary user id. So can't stat directory (not contents) of primary user from secondary user
* but can the other way around. However, this is happening on android 10 avd, but not on android
* 11 avd.
*
* @param context The {@link Context} for operations.
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
* automatically set.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error isTermuxFilesDirectoryAccessible(@NonNull final Context context, boolean createDirectoryIfMissing, boolean setMissingPermissions) {
if (createDirectoryIfMissing)
context.getFilesDir();
if (!FileUtils.directoryFileExists(TermuxConstants.TERMUX_FILES_DIR_PATH, true))
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH);
if (setMissingPermissions)
FileUtils.setMissingFilePermissions("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS);
return FileUtils.checkMissingFilePermissions("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, false);
}
/**
* Validate if {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} exists and has
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
* .
*
* The {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} directory would not exist if termux has
* not been installed or the bootstrap setup has not been run or if it was deleted by the user.
*
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
* automatically set.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error isTermuxPrefixDirectoryAccessible(boolean createDirectoryIfMissing, boolean setMissingPermissions) {
return FileUtils.validateDirectoryFileExistenceAndPermissions("termux prefix directory", TermuxConstants.TERMUX_PREFIX_DIR_PATH,
null, createDirectoryIfMissing,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setMissingPermissions, true,
false, false);
}
/**
* Validate if {@link TermuxConstants#TERMUX_STAGING_PREFIX_DIR_PATH} exists and has
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
*
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
* automatically set.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error isTermuxPrefixStagingDirectoryAccessible(boolean createDirectoryIfMissing, boolean setMissingPermissions) {
return FileUtils.validateDirectoryFileExistenceAndPermissions("termux prefix staging directory", TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH,
null, createDirectoryIfMissing,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setMissingPermissions, true,
false, false);
}
/**
* Get a markdown {@link String} for stat output for various Termux app files paths.
*
* @param context The context for operations.
* @return Returns the markdown {@link String}.
*/
public static String getTermuxFilesStatMarkdownString(@NonNull final Context context) {
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(context);
if (termuxPackageContext == null) return null;
// Also ensures that termux files directory is created if it does not already exist
String filesDir = termuxPackageContext.getFilesDir().getAbsolutePath();
// Build script
StringBuilder statScript = new StringBuilder();
statScript
.append("echo 'ls info:'\n")
.append("/system/bin/ls -lhdZ")
.append(" '/data/data'")
.append(" '/data/user/0'")
.append(" '" + TermuxConstants.TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "'")
.append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "'")
.append(" '" + TermuxConstants.TERMUX_FILES_DIR_PATH + "'")
.append(" '" + filesDir + "'")
.append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'")
.append(" '/data/user/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'")
.append(" '" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_PREFIX_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_HOME_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/login'")
.append(" 2>&1")
.append("\necho; echo 'mount info:'\n")
.append("/system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1");
// Run script
ExecutionCommand executionCommand = new ExecutionCommand(1, "/system/bin/sh", null, statScript.toString() + "\n", "/", true, true);
executionCommand.commandLabel = TermuxConstants.TERMUX_APP_NAME + " Files Stat Command";
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF;
TermuxTask termuxTask = TermuxTask.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true);
if (termuxTask == null || !executionCommand.isSuccessful()) {
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
return null;
}
// Build script output
StringBuilder statOutput = new StringBuilder();
statOutput.append("$ ").append(statScript.toString());
statOutput.append("\n\n").append(executionCommand.resultData.stdout.toString());
boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
if (executionCommand.resultData.exitCode != 0 || stderrSet) {
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
if (stderrSet)
statOutput.append("\n").append(executionCommand.resultData.stderr.toString());
statOutput.append("\n").append("exit code: ").append(executionCommand.resultData.exitCode.toString());
}
// Build markdown output
StringBuilder markdownString = new StringBuilder();
markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" Files Info\n\n");
AndroidUtils.appendPropertyToMarkdown(markdownString,"TERMUX_REQUIRED_FILES_DIR_PATH ($PREFIX)", TermuxConstants.TERMUX_FILES_DIR_PATH);
AndroidUtils.appendPropertyToMarkdown(markdownString,"ANDROID_ASSIGNED_FILES_DIR_PATH", filesDir);
markdownString.append("\n\n").append(MarkdownUtils.getMarkdownCodeForString(statOutput.toString(), true));
markdownString.append("\n##\n");
return markdownString.toString();
}
}