Cleanup API surface

This commit is contained in:
topjohnwu 2022-04-23 15:58:13 -07:00
parent 3eaeea1403
commit 74d7d23e4b
13 changed files with 312 additions and 125 deletions

View File

@ -19,7 +19,6 @@ package com.topjohnwu.libsuexample;
import static com.topjohnwu.libsuexample.MainActivity.TAG;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
import android.util.Log;
@ -64,12 +63,11 @@ class AIDLService extends RootService {
@Override
public IBinder getFS() {
return fs;
return FileSystemApi.getService();
}
}
private final String uuid = UUID.randomUUID().toString();
private final Binder fs = FileSystemApi.createBinder();
@Override
public void onCreate() {

View File

@ -76,7 +76,7 @@ public class MainActivity extends Activity implements Handler.Callback {
private AIDLConnection aidlConn;
private AIDLConnection daemonConn;
private FileSystemApi.Remote remoteFs;
private FileSystemApi remoteFs;
class AIDLConnection implements ServiceConnection {
@ -101,7 +101,7 @@ public class MainActivity extends Activity implements Handler.Callback {
consoleList.add("AIDL UID : " + ipc.getUid());
consoleList.add("AIDL UUID: " + ipc.getUUID());
if (!isDaemon)
remoteFs = FileSystemApi.asRemote(ipc.getFS());
remoteFs = FileSystemApi.getRemote(ipc.getFS());
} catch (RemoteException e) {
Log.e(TAG, "Remote error", e);
}

View File

@ -27,31 +27,29 @@ import com.topjohnwu.superuser.internal.IOFactory;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.nio.ExtendedFile;
import com.topjohnwu.superuser.nio.FileSystemApi;
import com.topjohnwu.superuser.nio.RemoteFile;
import com.topjohnwu.superuser.nio.RemoteFileChannel;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Random;
public class StressTest {
private static final Random r = new Random();
private static FileSystemApi.Remote fs;
private static FileSystemApi fs;
private static FileCallback callback;
interface FileCallback {
void onFile(ExtendedFile file) throws Exception;
}
public static void perform(FileSystemApi.Remote s) {
public static void perform(FileSystemApi s) {
fs = s;
Shell.EXECUTOR.execute(() -> {
try {
//testShellIO();
testShellIO();
testRemoteIO();
} catch (Exception e){
Log.d(TAG, "", e);
@ -94,15 +92,13 @@ public class StressTest {
}
private static void testRemoteIO() throws Exception {
RemoteFile root = new RemoteFile(fs, new File("/system/app"));
ExtendedFile root = fs.getFile("/system/app");
// Stress test fifo IOStreams
RemoteFile n = new RemoteFile(fs, new File("/dev/null"));
RemoteFileChannel out = new RemoteFileChannel(n, MODE_WRITE_ONLY);
FileChannel out = fs.openChannel("/dev/null", MODE_WRITE_ONLY);
ByteBuffer buf = ByteBuffer.allocateDirect(512 * 1024);
callback = file -> {
Log.d(TAG, file.getPath());
try (RemoteFileChannel src = new RemoteFileChannel((RemoteFile) file, MODE_READ_ONLY)) {
try (FileChannel src = fs.openChannel(file, MODE_READ_ONLY)) {
for (;;) {
// Randomize read/write length
int len = r.nextInt(buf.capacity());

View File

@ -25,6 +25,7 @@ import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.internal.FileImpl;
import com.topjohnwu.superuser.internal.Utils;
import com.topjohnwu.superuser.nio.ExtendedFile;
import com.topjohnwu.superuser.nio.FileSystemApi;
import java.io.File;
import java.io.FilenameFilter;
@ -62,22 +63,23 @@ public class SuFile extends FileImpl<SuFile> {
private Shell mShell;
public static ExtendedFile open(String pathname) {
return Utils.isMainShellRoot() ? new SuFile(pathname) : ExtendedFile.get(pathname);
return Utils.isMainShellRoot() ?
new SuFile(pathname) : FileSystemApi.getLocal().getFile(pathname);
}
public static ExtendedFile open(String parent, String child) {
return Utils.isMainShellRoot() ?
new SuFile(parent, child) : ExtendedFile.get(parent, child);
new SuFile(parent, child) : FileSystemApi.getLocal().getFile(parent, child);
}
public static ExtendedFile open(File parent, String child) {
return Utils.isMainShellRoot() ?
new SuFile(parent, child) : ExtendedFile.get(parent.getPath(), child);
return Utils.isMainShellRoot() ? new SuFile(parent, child) :
FileSystemApi.getLocal().getFile(parent.getPath(), child);
}
public static ExtendedFile open(URI uri) {
return Utils.isMainShellRoot() ?
new SuFile(uri) : ExtendedFile.get(new File(uri).getPath());
new SuFile(uri) : FileSystemApi.getLocal().getFile(new File(uri).getPath());
}
private static final Creator<SuFile> CREATOR = new Creator<SuFile>() {

View File

@ -35,7 +35,6 @@ import android.util.MutableLong;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import java.io.Closeable;
import java.io.File;
@ -45,8 +44,7 @@ import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.atomic.AtomicInteger;
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class FileSystemImpl extends IFileSystemService.Stub {
class FileSystemService extends IFileSystemService.Stub {
// This is only for testing purpose
private static final boolean FORCE_NO_SPLICE = false;

View File

@ -34,6 +34,7 @@ import android.os.Build;
import android.system.ErrnoException;
import android.system.Int64Ref;
import android.system.Os;
import android.util.ArraySet;
import android.util.MutableLong;
import androidx.annotation.RequiresApi;
@ -41,6 +42,9 @@ import androidx.annotation.RequiresApi;
import java.io.FileDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.util.Set;
class FileUtils {
@ -48,6 +52,14 @@ class FileUtils {
private static Method splice;
private static Method sendfile;
static class Flag {
boolean read;
boolean write;
boolean create;
boolean truncate;
boolean append;
}
static int pfdModeToPosix(int mode) {
int res;
if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) {
@ -71,6 +83,64 @@ class FileUtils {
return res;
}
@RequiresApi(api = 26)
static Set<OpenOption> pfdModeToOptions(int mode) {
Set<OpenOption> set = new ArraySet<>();
if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) {
set.add(StandardOpenOption.READ);
set.add(StandardOpenOption.WRITE);
} else if ((mode & MODE_WRITE_ONLY) == MODE_WRITE_ONLY) {
set.add(StandardOpenOption.WRITE);
} else if ((mode & MODE_READ_ONLY) == MODE_READ_ONLY) {
set.add(StandardOpenOption.READ);
} else {
throw new IllegalArgumentException("Bad mode: " + mode);
}
if ((mode & MODE_CREATE) == MODE_CREATE) {
set.add(StandardOpenOption.CREATE);
}
if ((mode & MODE_TRUNCATE) == MODE_TRUNCATE) {
set.add(StandardOpenOption.TRUNCATE_EXISTING);
}
if ((mode & MODE_APPEND) == MODE_APPEND) {
set.add(StandardOpenOption.APPEND);
}
return set;
}
static Flag pfdModeToFlag(int mode) {
Flag f = new Flag();
if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) {
f.read = true;
f.write = true;
} else if ((mode & MODE_WRITE_ONLY) == MODE_WRITE_ONLY) {
f.write = true;
} else if ((mode & MODE_READ_ONLY) == MODE_READ_ONLY) {
f.read = true;
} else {
throw new IllegalArgumentException("Bad mode: " + mode);
}
if ((mode & MODE_CREATE) == MODE_CREATE) {
f.create = true;
}
if ((mode & MODE_TRUNCATE) == MODE_TRUNCATE) {
f.truncate = true;
}
if ((mode & MODE_APPEND) == MODE_APPEND) {
f.append = true;
}
// Validate flags
if (f.append && f.read) {
throw new IllegalArgumentException("READ + APPEND not allowed");
}
if (f.append && f.truncate) {
throw new IllegalArgumentException("APPEND + TRUNCATE not allowed");
}
return f;
}
@RequiresApi(api = 28)
static long splice(
FileDescriptor fdIn, Int64Ref offIn,

View File

@ -21,10 +21,7 @@ import android.system.Os;
import android.system.OsConstants;
import android.system.StructStat;
import androidx.annotation.RestrictTo;
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class LocalFile extends FileImpl<LocalFile> {
class LocalFile extends FileImpl<LocalFile> {
private static final Creator<LocalFile> CREATOR = new Creator<LocalFile>() {
@ -44,11 +41,11 @@ public class LocalFile extends FileImpl<LocalFile> {
}
};
public LocalFile(String pathname) {
LocalFile(String pathname) {
super(pathname, CREATOR);
}
public LocalFile(String parent, String child) {
LocalFile(String parent, String child) {
super(parent, child, CREATOR);
}

View File

@ -0,0 +1,121 @@
/*
* Copyright 2022 John "topjohnwu" Wu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.topjohnwu.superuser.internal;
import android.os.Build;
import android.os.IBinder;
import android.system.ErrnoException;
import android.system.OsConstants;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import com.topjohnwu.superuser.nio.ExtendedFile;
import com.topjohnwu.superuser.nio.FileSystemApi;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class NIOFactory {
private NIOFactory() {}
public static FileSystemApi createLocal() {
return new FileSystemApi() {
@NonNull
@Override
public ExtendedFile getFile(@NonNull String pathname) {
return new LocalFile(pathname);
}
@NonNull
@Override
public ExtendedFile getFile(@Nullable String parent, @NonNull String child) {
return new LocalFile(parent, child);
}
@NonNull
@Override
public FileChannel openChannel(@NonNull File file, int mode) throws IOException {
if (Build.VERSION.SDK_INT >= 26) {
return FileChannel.open(file.toPath(), FileUtils.pfdModeToOptions(mode));
} else {
FileUtils.Flag f = FileUtils.pfdModeToFlag(mode);
if (f.write) {
if (!f.create) {
if (!file.exists()) {
ErrnoException e = new ErrnoException("open", OsConstants.ENOENT);
throw new FileNotFoundException(file + ": " + e.getMessage());
}
}
if (f.append) {
return new FileOutputStream(file, true).getChannel();
}
if (!f.read && f.truncate) {
return new FileOutputStream(file, false).getChannel();
}
// Unfortunately, there is no way to create a write-only channel
// without truncating. Forced to open rw RAF in all cases.
FileChannel ch = new RandomAccessFile(file, "rw").getChannel();
if (f.truncate) {
ch.truncate(0);
}
return ch;
} else {
return new FileInputStream(file).getChannel();
}
}
}
};
}
public static FileSystemApi createRemote(IBinder b) {
return new FileSystemApi() {
final IFileSystemService fs = IFileSystemService.Stub.asInterface(b);
@NonNull
@Override
public ExtendedFile getFile(@NonNull String pathname) {
return new RemoteFile(fs, pathname);
}
@NonNull
@Override
public ExtendedFile getFile(@Nullable String parent, @NonNull String child) {
return new RemoteFile(fs, parent, child);
}
@NonNull
@Override
public FileChannel openChannel(@NonNull File file, int mode) throws IOException {
return new RemoteFileChannel(fs, file, mode);
}
};
}
public static FileSystemService createFsService() {
return new FileSystemService();
}
}

View File

@ -19,16 +19,13 @@ package com.topjohnwu.superuser.internal;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.RestrictTo;
import java.util.ArrayList;
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class ParcelValues extends ArrayList<Object> implements Parcelable {
class ParcelValues extends ArrayList<Object> implements Parcelable {
private static final ClassLoader cl = ParcelValues.class.getClassLoader();
public static final Creator<ParcelValues> CREATOR = new Creator<ParcelValues>() {
static final Creator<ParcelValues> CREATOR = new Creator<ParcelValues>() {
@Override
public ParcelValues createFromParcel(Parcel in) {
return new ParcelValues(in);
@ -40,7 +37,7 @@ public class ParcelValues extends ArrayList<Object> implements Parcelable {
}
};
public ParcelValues() {}
ParcelValues() {}
private ParcelValues(Parcel in) {
int size = in.readInt();

View File

@ -14,26 +14,19 @@
* limitations under the License.
*/
package com.topjohnwu.superuser.nio;
package com.topjohnwu.superuser.internal;
import android.os.RemoteException;
import android.system.OsConstants;
import androidx.annotation.NonNull;
import com.topjohnwu.superuser.internal.FileImpl;
import com.topjohnwu.superuser.internal.IFileSystemService;
import com.topjohnwu.superuser.internal.ParcelValues;
import java.io.File;
import java.io.IOException;
/**
* Represents a {@link File} instance on a remote process.
*/
public class RemoteFile extends FileImpl<RemoteFile> {
class RemoteFile extends FileImpl<RemoteFile> {
final IFileSystemService fs;
private final IFileSystemService fs;
private static final Creator<RemoteFile> CREATOR = new Creator<RemoteFile>() {
@ -49,22 +42,17 @@ public class RemoteFile extends FileImpl<RemoteFile> {
@Override
public RemoteFile createChild(RemoteFile parent, String name) {
return new RemoteFile(parent.fs, new File(parent, name).getAbsolutePath());
return new RemoteFile(parent.fs, parent.getPath(), name);
}
};
/**
* Create a new file instance on a remote process.
* @param remote the remote process's filesystem API
* @param file the file path
*/
public RemoteFile(FileSystemApi.Remote remote, File file) {
super(file.getAbsolutePath(), CREATOR);
fs = remote.fs;
RemoteFile(IFileSystemService f, String path) {
super(path, CREATOR);
fs = f;
}
private RemoteFile(IFileSystemService f, String path) {
super(path, CREATOR);
RemoteFile(IFileSystemService f, String parent, String child) {
super(parent, child, CREATOR);
fs = f;
}
@ -129,9 +117,6 @@ public class RemoteFile extends FileImpl<RemoteFile> {
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isBlock() {
try {
@ -141,9 +126,6 @@ public class RemoteFile extends FileImpl<RemoteFile> {
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isCharacter() {
try {
@ -153,9 +135,6 @@ public class RemoteFile extends FileImpl<RemoteFile> {
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isSymlink() {
try {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.topjohnwu.superuser.nio;
package com.topjohnwu.superuser.internal;
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
@ -28,9 +28,6 @@ import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import com.topjohnwu.superuser.internal.IFileSystemService;
import com.topjohnwu.superuser.internal.ParcelValues;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
@ -44,7 +41,7 @@ import java.nio.channels.NonWritableChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
public class RemoteFileChannel extends FileChannel {
class RemoteFileChannel extends FileChannel {
private static final int PIPE_CAPACITY = 16 * 4096;
@ -57,8 +54,8 @@ public class RemoteFileChannel extends FileChannel {
private final int handle;
@SuppressWarnings("OctalInteger")
public RemoteFileChannel(RemoteFile file, int mode) throws IOException {
fs = file.fs;
RemoteFileChannel(IFileSystemService fs, File file, int mode) throws IOException {
this.fs = fs;
this.mode = mode;
try {
// We use a FIFO created on the client side instead of opening a pipe and
@ -68,7 +65,7 @@ public class RemoteFileChannel extends FileChannel {
Os.mkfifo(fifo.getPath(), 0644);
// Open the file on the remote process
handle = checkAndGet(fs.open(file.getPath(), mode, fifo.getPath()));
handle = checkAndGet(fs.open(file.getAbsolutePath(), mode, fifo.getPath()));
// Since we do not have the machinery to interrupt native pthreads, we
// have to make sure none of our I/O can block in all operations.

View File

@ -19,8 +19,6 @@ package com.topjohnwu.superuser.nio;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.topjohnwu.superuser.internal.LocalFile;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
@ -29,22 +27,6 @@ import java.net.URI;
public abstract class ExtendedFile extends File {
/**
* @see File#File(String)
*/
@NonNull
public static ExtendedFile get(@NonNull String pathname) {
return new LocalFile(pathname);
}
/**
* @see File#File(String, String)
*/
@NonNull
public static ExtendedFile get(@Nullable String parent, @NonNull String child) {
return new LocalFile(parent, child);
}
/**
* {@inheritDoc}
*/

View File

@ -17,61 +17,111 @@
package com.topjohnwu.superuser.nio;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.topjohnwu.superuser.internal.FileSystemImpl;
import com.topjohnwu.superuser.internal.IFileSystemService;
import com.topjohnwu.superuser.internal.NIOFactory;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.channels.FileChannel;
/**
* Expose filesystem APIs over Binder.
*
* In the remote process, create a new {@link Binder} object that exposes filesystem APIs by
* calling {@link #createBinder()}. You can then pass this {@link Binder} object in many different
* ways, such as adding it to an Intent, sending it with {@link Message}es, returning it in the
* {@code onBind()} of bind / root services, or returning it in an AIDL interface.
*
* In the client process, create a {@link Remote} instance by passing the remote {@link IBinder}
* proxy you received to {@link #asRemote(IBinder)}. This {@link Remote} instance can then be used
* to construct remote I/O classes, such as {@link RemoteFile}
* Access file system APIs.
*/
public final class FileSystemApi {
public abstract class FileSystemApi {
// The actual reason why this class exists is because we do not want to expose
// IFileSystemService in any API surface to keep it an internal implementation detail.
private static Binder fsService;
private FileSystemApi() {}
private static final FileSystemApi LOCAL = NIOFactory.createLocal();
/**
* Create a new {@link Binder} instance that exposes filesystem APIs.
* An example use case is to return this value in {@code onBind()} of (root) services.
* Get the service that exposes file system APIs over Binder IPC.
* <p>
* Sending the {@link Binder} obtained from this method to a client process enables
* the calling process to perform file system operations on behalf of the client.
* This allows a client process to access files normally denied by its permissions.
* <p>
* You can pass this {@link Binder} object in many different ways, such as returning it in the
* {@code onBind()} method of (root) services, passing it around with a {@link Bundle},
* or returning it in an AIDL interface method. The receiving end will get an {@link IBinder},
* which should be passed to {@link #getRemote(IBinder)} for usage.
*/
@NonNull
public static Binder createBinder() {
return new FileSystemImpl();
public synchronized static Binder getService() {
if (fsService == null)
fsService = NIOFactory.createFsService();
return fsService;
}
@NonNull
public static FileSystemApi getLocal() {
return LOCAL;
}
/**
* Create a {@link Remote} instance that exposes filesystem APIs of a remote process.
* @param service an instance or a remote proxy of the return value of {@link #createBinder()}
* @return this return value is for constructing {@link RemoteFile}
* Create a {@link FileSystemApi} to access file system APIs of a remote process.
* @param binder a remote proxy of the {@link Binder} obtained from {@link #getService()}
*/
@NonNull
public static Remote asRemote(@NonNull IBinder service) {
return new Remote(service);
public static FileSystemApi getRemote(@NonNull IBinder binder) {
return NIOFactory.createRemote(binder);
}
/**
* Represents the filesystem API of a remote process.
* @see #asRemote(IBinder)
* @see File#File(String)
*/
public static class Remote {
final IFileSystemService fs;
Remote(IBinder b) {
fs = IFileSystemService.Stub.asInterface(b);
}
@NonNull
public abstract ExtendedFile getFile(@NonNull String pathname);
/**
* @see File#File(String, String)
*/
@NonNull
public abstract ExtendedFile getFile(@Nullable String parent, @NonNull String child);
/**
* @see File#File(File, String)
*/
@NonNull
public final ExtendedFile getFile(@Nullable File parent, @NonNull String child) {
return getFile(parent == null ? null : parent.getPath(), child);
}
/**
* @see File#File(URI)
*/
@NonNull
public final ExtendedFile getFile(@NonNull URI uri) {
return getFile(new File(uri).getPath());
}
/**
* Opens a file channel to access the file.
* @param pathname the file to be opened.
* @param mode same {@code mode} argument in {@link ParcelFileDescriptor#open(File, int)}
* @return a new FileChannel pointing to the given file.
* @throws IOException if the given file can not be opened with the requested mode.
* @see ParcelFileDescriptor#open(File, int)
*/
@NonNull
public final FileChannel openChannel(@NonNull String pathname, int mode) throws IOException {
return openChannel(new File(pathname), mode);
}
/**
* Opens a file channel to access the file.
* @param file the file to be opened.
* @param mode same {@code mode} argument in {@link ParcelFileDescriptor#open(File, int)}
* @return a new FileChannel pointing to the given file.
* @throws IOException if the given file can not be opened with the requested mode.
* @see ParcelFileDescriptor#open(File, int)
*/
@NonNull
public abstract FileChannel openChannel(@NonNull File file, int mode) throws IOException;
}