diff options
author | Harsh Shandilya <me@msfjarvis.dev> | 2020-03-09 19:06:11 +0530 |
---|---|---|
committer | Harsh Shandilya <me@msfjarvis.dev> | 2020-03-09 19:24:27 +0530 |
commit | 7d48bef70a56d4370856eedab619b1f83ac3d0d0 (patch) | |
tree | 76fd859578e499cd3a8fd2f402652530ea36a72d /ui/src/main/java/com/wireguard/android/util | |
parent | Enable nonnull generation for tunnel module (diff) | |
download | wireguard-android-7d48bef70a56d4370856eedab619b1f83ac3d0d0.tar.xz wireguard-android-7d48bef70a56d4370856eedab619b1f83ac3d0d0.zip |
Rename app module to ui
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'ui/src/main/java/com/wireguard/android/util')
11 files changed, 903 insertions, 0 deletions
diff --git a/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.java b/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.java new file mode 100644 index 00000000..0df5e96a --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import com.google.android.material.snackbar.Snackbar; +import android.view.View; +import android.widget.TextView; + +/** + * Standalone utilities for interacting with the system clipboard. + */ + +public final class ClipboardUtils { + private ClipboardUtils() { + // Prevent instantiation + } + + public static void copyTextView(final View view) { + if (!(view instanceof TextView)) + return; + final CharSequence text = ((TextView) view).getText(); + if (text == null || text.length() == 0) + return; + final Object service = view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + if (!(service instanceof ClipboardManager)) + return; + final CharSequence description = view.getContentDescription(); + ((ClipboardManager) service).setPrimaryClip(ClipData.newPlainText(description, text)); + Snackbar.make(view, description + " copied to clipboard", Snackbar.LENGTH_LONG).show(); + } +} diff --git a/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java b/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java new file mode 100644 index 00000000..7db46fa9 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java @@ -0,0 +1,98 @@ +/* + * Copyright © 2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore; +import android.provider.MediaStore.MediaColumns; + +import com.wireguard.android.R; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class DownloadsFileSaver { + + public static class DownloadsFile { + private Context context; + private OutputStream outputStream; + private String fileName; + private Uri uri; + + private DownloadsFile(final Context context, final OutputStream outputStream, final String fileName, final Uri uri) { + this.context = context; + this.outputStream = outputStream; + this.fileName = fileName; + this.uri = uri; + } + + public OutputStream getOutputStream() { return outputStream; } + public String getFileName() { return fileName; } + + public void delete() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + context.getContentResolver().delete(uri, null, null); + else + new File(fileName).delete(); + } + } + + public static DownloadsFile save(final Context context, final String name, final String mimeType, final boolean overwriteExisting) throws Exception { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + final ContentResolver contentResolver = context.getContentResolver(); + if (overwriteExisting) + contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), new String[]{name}); + final ContentValues contentValues = new ContentValues(); + contentValues.put(MediaColumns.DISPLAY_NAME, name); + contentValues.put(MediaColumns.MIME_TYPE, mimeType); + final Uri contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues); + if (contentUri == null) + throw new IOException(context.getString(R.string.create_downloads_file_error)); + final OutputStream contentStream = contentResolver.openOutputStream(contentUri); + if (contentStream == null) + throw new IOException(context.getString(R.string.create_downloads_file_error)); + @SuppressWarnings("deprecation") + Cursor cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DATA}, null, null, null); + String path = null; + if (cursor != null) { + try { + if (cursor.moveToFirst()) + path = cursor.getString(0); + } finally { + cursor.close(); + } + } + if (path == null) { + path = "Download/"; + cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DISPLAY_NAME}, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) + path += cursor.getString(0); + } finally { + cursor.close(); + } + } + } + return new DownloadsFile(context, contentStream, path, contentUri); + } else { + @SuppressWarnings("deprecation") + final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + final File file = new File(path, name); + if (!path.isDirectory() && !path.mkdirs()) + throw new IOException(context.getString(R.string.create_output_dir_error)); + return new DownloadsFile(context, new FileOutputStream(file), file.getAbsolutePath(), null); + } + } +} diff --git a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.java b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.java new file mode 100644 index 00000000..481a6ffb --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.java @@ -0,0 +1,160 @@ +/* + * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util; + +import android.content.res.Resources; +import android.os.RemoteException; + +import androidx.annotation.Nullable; + +import com.wireguard.android.Application; +import com.wireguard.android.R; +import com.wireguard.android.backend.BackendException; +import com.wireguard.android.util.RootShell.RootShellException; +import com.wireguard.config.BadConfigException; +import com.wireguard.config.BadConfigException.Location; +import com.wireguard.config.InetEndpoint; +import com.wireguard.config.InetNetwork; +import com.wireguard.config.ParseException; +import com.wireguard.crypto.Key.Format; +import com.wireguard.crypto.KeyFormatException; +import com.wireguard.crypto.KeyFormatException.Type; + +import java.net.InetAddress; +import java.util.EnumMap; +import java.util.Map; + +import java9.util.Maps; + +public final class ErrorMessages { + private static final Map<BadConfigException.Reason, Integer> BCE_REASON_MAP = new EnumMap<>(Maps.of( + BadConfigException.Reason.INVALID_KEY, R.string.bad_config_reason_invalid_key, + BadConfigException.Reason.INVALID_NUMBER, R.string.bad_config_reason_invalid_number, + BadConfigException.Reason.INVALID_VALUE, R.string.bad_config_reason_invalid_value, + BadConfigException.Reason.MISSING_ATTRIBUTE, R.string.bad_config_reason_missing_attribute, + BadConfigException.Reason.MISSING_SECTION, R.string.bad_config_reason_missing_section, + BadConfigException.Reason.MISSING_VALUE, R.string.bad_config_reason_missing_value, + BadConfigException.Reason.SYNTAX_ERROR, R.string.bad_config_reason_syntax_error, + BadConfigException.Reason.UNKNOWN_ATTRIBUTE, R.string.bad_config_reason_unknown_attribute, + BadConfigException.Reason.UNKNOWN_SECTION, R.string.bad_config_reason_unknown_section + )); + private static final Map<BackendException.Reason, Integer> BE_REASON_MAP = new EnumMap<>(Maps.of( + BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME, R.string.module_version_error, + BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE, R.string.tunnel_config_error, + BackendException.Reason.TUNNEL_MISSING_CONFIG, R.string.no_config_error, + BackendException.Reason.VPN_NOT_AUTHORIZED, R.string.vpn_not_authorized_error, + BackendException.Reason.UNABLE_TO_START_VPN, R.string.vpn_start_error, + BackendException.Reason.TUN_CREATION_ERROR, R.string.tun_create_error, + BackendException.Reason.GO_ACTIVATION_ERROR_CODE, R.string.tunnel_on_error + )); + private static final Map<RootShellException.Reason, Integer> RSE_REASON_MAP = new EnumMap<>(Maps.of( + RootShellException.Reason.NO_ROOT_ACCESS, R.string.error_root, + RootShellException.Reason.SHELL_MARKER_COUNT_ERROR, R.string.shell_marker_count_error, + RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR, R.string.shell_exit_status_read_error, + RootShellException.Reason.SHELL_START_ERROR, R.string.shell_start_error, + RootShellException.Reason.CREATE_BIN_DIR_ERROR, R.string.create_bin_dir_error, + RootShellException.Reason.CREATE_TEMP_DIR_ERROR, R.string.create_temp_dir_error + )); + private static final Map<Format, Integer> KFE_FORMAT_MAP = new EnumMap<>(Maps.of( + Format.BASE64, R.string.key_length_explanation_base64, + Format.BINARY, R.string.key_length_explanation_binary, + Format.HEX, R.string.key_length_explanation_hex + )); + private static final Map<Type, Integer> KFE_TYPE_MAP = new EnumMap<>(Maps.of( + Type.CONTENTS, R.string.key_contents_error, + Type.LENGTH, R.string.key_length_error + )); + private static final Map<Class, Integer> PE_CLASS_MAP = Maps.of( + InetAddress.class, R.string.parse_error_inet_address, + InetEndpoint.class, R.string.parse_error_inet_endpoint, + InetNetwork.class, R.string.parse_error_inet_network, + Integer.class, R.string.parse_error_integer + ); + + private ErrorMessages() { + // Prevent instantiation + } + + public static String get(@Nullable final Throwable throwable) { + final Resources resources = Application.get().getResources(); + if (throwable == null) + return resources.getString(R.string.unknown_error); + final Throwable rootCause = rootCause(throwable); + final String message; + if (rootCause instanceof BadConfigException) { + final BadConfigException bce = (BadConfigException) rootCause; + final String reason = getBadConfigExceptionReason(resources, bce); + final String context = bce.getLocation() == Location.TOP_LEVEL ? + resources.getString(R.string.bad_config_context_top_level, + bce.getSection().getName()) : + resources.getString(R.string.bad_config_context, + bce.getSection().getName(), + bce.getLocation().getName()); + final String explanation = getBadConfigExceptionExplanation(resources, bce); + message = resources.getString(R.string.bad_config_error, reason, context) + explanation; + } else if (rootCause instanceof BackendException) { + final BackendException be = (BackendException) rootCause; + message = resources.getString(BE_REASON_MAP.get(be.getReason()), be.getFormat()); + } else if (rootCause instanceof RootShellException) { + final RootShellException rse = (RootShellException) rootCause; + message = resources.getString(RSE_REASON_MAP.get(rse.getReason()), rse.getFormat()); + } else if (rootCause.getMessage() != null) { + message = rootCause.getMessage(); + } else { + final String errorType = rootCause.getClass().getSimpleName(); + message = resources.getString(R.string.generic_error, errorType); + } + return message; + } + + private static String getBadConfigExceptionExplanation(final Resources resources, + final BadConfigException bce) { + if (bce.getCause() instanceof KeyFormatException) { + final KeyFormatException kfe = (KeyFormatException) bce.getCause(); + if (kfe.getType() == Type.LENGTH) + return resources.getString(KFE_FORMAT_MAP.get(kfe.getFormat())); + } else if (bce.getCause() instanceof ParseException) { + final ParseException pe = (ParseException) bce.getCause(); + if (pe.getMessage() != null) + return ": " + pe.getMessage(); + } else if (bce.getLocation() == Location.LISTEN_PORT) { + return resources.getString(R.string.bad_config_explanation_udp_port); + } else if (bce.getLocation() == Location.MTU) { + return resources.getString(R.string.bad_config_explanation_positive_number); + } else if (bce.getLocation() == Location.PERSISTENT_KEEPALIVE) { + return resources.getString(R.string.bad_config_explanation_pka); + } + return ""; + } + + private static String getBadConfigExceptionReason(final Resources resources, + final BadConfigException bce) { + if (bce.getCause() instanceof KeyFormatException) { + final KeyFormatException kfe = (KeyFormatException) bce.getCause(); + return resources.getString(KFE_TYPE_MAP.get(kfe.getType())); + } else if (bce.getCause() instanceof ParseException) { + final ParseException pe = (ParseException) bce.getCause(); + final String type = resources.getString(PE_CLASS_MAP.containsKey(pe.getParsingClass()) ? + PE_CLASS_MAP.get(pe.getParsingClass()) : R.string.parse_error_generic); + return resources.getString(R.string.parse_error_reason, type, pe.getText()); + } + return resources.getString(BCE_REASON_MAP.get(bce.getReason()), bce.getText()); + } + + private static Throwable rootCause(final Throwable throwable) { + Throwable cause = throwable; + while (cause.getCause() != null) { + if (cause instanceof BadConfigException || cause instanceof BackendException || + cause instanceof RootShellException) + break; + final Throwable nextCause = cause.getCause(); + if (nextCause instanceof RemoteException) + break; + cause = nextCause; + } + return cause; + } +} diff --git a/ui/src/main/java/com/wireguard/android/util/ExceptionLoggers.java b/ui/src/main/java/com/wireguard/android/util/ExceptionLoggers.java new file mode 100644 index 00000000..5c7a38c0 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/ExceptionLoggers.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util; + +import androidx.annotation.Nullable; +import android.util.Log; + +import java9.util.function.BiConsumer; + +/** + * Helpers for logging exceptions from asynchronous tasks. These can be passed to + * {@code CompletionStage.whenComplete()} at the end of an asynchronous future chain. + */ + +public enum ExceptionLoggers implements BiConsumer<Object, Throwable> { + D(Log.DEBUG), + E(Log.ERROR); + + private static final String TAG = "WireGuard/" + ExceptionLoggers.class.getSimpleName(); + private final int priority; + + ExceptionLoggers(final int priority) { + this.priority = priority; + } + + @Override + public void accept(final Object result, @Nullable final Throwable throwable) { + if (throwable != null) + Log.println(Log.ERROR, TAG, Log.getStackTraceString(throwable)); + else if (priority <= Log.DEBUG) + Log.println(priority, TAG, "Future completed successfully"); + } +} diff --git a/ui/src/main/java/com/wireguard/android/util/Extensions.kt b/ui/src/main/java/com/wireguard/android/util/Extensions.kt new file mode 100644 index 00000000..6b528a85 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/Extensions.kt @@ -0,0 +1,16 @@ +/* + * Copyright © 2020 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util + +import android.content.Context +import android.util.TypedValue +import androidx.annotation.AttrRes + +fun Context.resolveAttribute(@AttrRes attrRes: Int): Int { + val typedValue = TypedValue() + theme.resolveAttribute(attrRes, typedValue, true) + return typedValue.data +} diff --git a/ui/src/main/java/com/wireguard/android/util/FragmentUtils.java b/ui/src/main/java/com/wireguard/android/util/FragmentUtils.java new file mode 100644 index 00000000..5fb9a3bc --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/FragmentUtils.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.wireguard.android.util; + +import android.content.Context; +import androidx.preference.Preference; +import android.view.ContextThemeWrapper; + +import com.wireguard.android.activity.SettingsActivity; + +public final class FragmentUtils { + private FragmentUtils() { + // Prevent instantiation + } + + public static SettingsActivity getPrefActivity(final Preference preference) { + final Context context = preference.getContext(); + if (context instanceof ContextThemeWrapper) { + if (context instanceof SettingsActivity) { + return ((SettingsActivity) context); + } + } + return null; + } +} diff --git a/ui/src/main/java/com/wireguard/android/util/ModuleLoader.java b/ui/src/main/java/com/wireguard/android/util/ModuleLoader.java new file mode 100644 index 00000000..bf094a5e --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/ModuleLoader.java @@ -0,0 +1,186 @@ +/* + * Copyright © 2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util; + +import android.content.Context; +import android.system.OsConstants; +import android.util.Base64; + +import com.wireguard.android.util.RootShell.RootShellException; + +import net.i2p.crypto.eddsa.EdDSAEngine; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.InvalidParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +public class ModuleLoader { + private static final String MODULE_PUBLIC_KEY_BASE64 = "RWRmHuT9PSqtwfsLtEx+QS06BJtLgFYteL9WCNjH7yuyu5Y1DieSN7If"; + private static final String MODULE_LIST_URL = "https://download.wireguard.com/android-module/modules.txt.sig"; + private static final String MODULE_URL = "https://download.wireguard.com/android-module/%s"; + private static final String MODULE_NAME = "wireguard-%s.ko"; + + private final RootShell rootShell; + private final String userAgent; + private final File moduleDir; + private final File tmpDir; + + public ModuleLoader(final Context context, final RootShell rootShell, final String userAgent) { + moduleDir = new File(context.getCacheDir(), "kmod"); + tmpDir = new File(context.getCacheDir(), "tmp"); + this.rootShell = rootShell; + this.userAgent = userAgent; + } + + public boolean moduleMightExist() { + return moduleDir.exists() && moduleDir.isDirectory(); + } + + public void loadModule() throws IOException, RootShellException { + rootShell.run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath())); + } + + public static boolean isModuleLoaded() { + return new File("/sys/module/wireguard").exists(); + } + + private static final class Sha256Digest { + private byte[] bytes; + private Sha256Digest(final String hex) { + if (hex.length() != 64) + throw new InvalidParameterException("SHA256 hashes must be 32 bytes long"); + bytes = new byte[32]; + for (int i = 0; i < 32; ++i) + bytes[i] = (byte)Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + } + + @Nullable + private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) { + final byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT); + + if (publicKeyBytes == null || publicKeyBytes.length != 32 + 10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd') + return null; + + final String[] lines = signifyDigest.split("\n", 3); + if (lines.length != 3) + return null; + if (!lines[0].startsWith("untrusted comment: ")) + return null; + + final byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT); + if (signatureBytes == null || signatureBytes.length != 64 + 10) + return null; + for (int i = 0; i < 10; ++i) { + if (signatureBytes[i] != publicKeyBytes[i]) + return null; + } + + try { + EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519); + Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm())); + byte[] rawPublicKeyBytes = new byte[32]; + System.arraycopy(publicKeyBytes, 10, rawPublicKeyBytes, 0, 32); + signature.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(rawPublicKeyBytes, parameterSpec))); + signature.update(lines[2].getBytes(StandardCharsets.UTF_8)); + if (!signature.verify(signatureBytes, 10, 64)) + return null; + } catch (final Exception ignored) { + return null; + } + + Map<String, Sha256Digest> hashes = new HashMap<>(); + for (final String line : lines[2].split("\n")) { + final String[] components = line.split(" ", 2); + if (components.length != 2) + return null; + try { + hashes.put(components[1], new Sha256Digest(components[0])); + } catch (final Exception ignored) { + return null; + } + } + return hashes; + } + + public Integer download() throws IOException, RootShellException, NoSuchAlgorithmException { + final List<String> output = new ArrayList<>(); + rootShell.run(output, "sha256sum /proc/version|cut -d ' ' -f 1"); + if (output.size() != 1 || output.get(0).length() != 64) + throw new InvalidParameterException("Invalid sha256 of /proc/version"); + final String moduleName = String.format(MODULE_NAME, output.get(0)); + HttpURLConnection connection = (HttpURLConnection)new URL(MODULE_LIST_URL).openConnection(); + connection.setRequestProperty("User-Agent", userAgent); + connection.connect(); + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) + throw new IOException("Hash list could not be found"); + byte[] input = new byte[1024 * 1024 * 3 /* 3MiB */]; + int len; + try (final InputStream inputStream = connection.getInputStream()) { + len = inputStream.read(input); + } + if (len <= 0) + throw new IOException("Hash list was empty"); + final Map<String, Sha256Digest> modules = verifySignedHashes(new String(input, 0, len, StandardCharsets.UTF_8)); + if (modules == null) + throw new InvalidParameterException("The signature did not verify or invalid hash list format"); + if (!modules.containsKey(moduleName)) + return OsConstants.ENOENT; + connection = (HttpURLConnection)new URL(String.format(MODULE_URL, moduleName)).openConnection(); + connection.setRequestProperty("User-Agent", userAgent); + connection.connect(); + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) + throw new IOException("Module file could not be found, despite being on hash list"); + + tmpDir.mkdirs(); + moduleDir.mkdir(); + File tempFile = null; + try { + tempFile = File.createTempFile("UNVERIFIED-", null, tmpDir); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (final InputStream inputStream = connection.getInputStream(); + final FileOutputStream outputStream = new FileOutputStream(tempFile)) { + int total = 0; + while ((len = inputStream.read(input)) > 0) { + total += len; + if (total > 1024 * 1024 * 15 /* 15 MiB */) + throw new IOException("File too big"); + outputStream.write(input, 0, len); + digest.update(input, 0, len); + } + outputStream.getFD().sync(); + } + if (!Arrays.equals(digest.digest(), modules.get(moduleName).bytes)) + throw new IOException("Incorrect file hash"); + + if (!tempFile.renameTo(new File(moduleDir, moduleName))) + throw new IOException("Unable to rename to final destination"); + } finally { + if (tempFile != null) + tempFile.delete(); + } + return OsConstants.EXIT_SUCCESS; + } +} diff --git a/ui/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java b/ui/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java new file mode 100644 index 00000000..0ba02184 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java @@ -0,0 +1,109 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util; + +import androidx.databinding.ObservableArrayList; +import androidx.annotation.Nullable; + +import com.wireguard.util.Keyed; + +import java.util.Collection; +import java.util.ListIterator; +import java.util.Objects; + +/** + * ArrayList that allows looking up elements by some key property. As the key property must always + * be retrievable, this list cannot hold {@code null} elements. Because this class places no + * restrictions on the order or duplication of keys, lookup by key, as well as all list modification + * operations, require O(n) time. + */ + +public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>> + extends ObservableArrayList<E> implements ObservableKeyedList<K, E> { + @Override + public boolean add(@Nullable final E e) { + if (e == null) + throw new NullPointerException("Trying to add a null element"); + return super.add(e); + } + + @Override + public void add(final int index, @Nullable final E e) { + if (e == null) + throw new NullPointerException("Trying to add a null element"); + super.add(index, e); + } + + @Override + public boolean addAll(final Collection<? extends E> c) { + if (c.contains(null)) + throw new NullPointerException("Trying to add a collection with null element(s)"); + return super.addAll(c); + } + + @Override + public boolean addAll(final int index, final Collection<? extends E> c) { + if (c.contains(null)) + throw new NullPointerException("Trying to add a collection with null element(s)"); + return super.addAll(index, c); + } + + @Override + public boolean containsAllKeys(final Collection<K> keys) { + for (final K key : keys) + if (!containsKey(key)) + return false; + return true; + } + + @Override + public boolean containsKey(final K key) { + return indexOfKey(key) >= 0; + } + + @Nullable + @Override + public E get(final K key) { + final int index = indexOfKey(key); + return index >= 0 ? get(index) : null; + } + + @Nullable + @Override + public E getLast(final K key) { + final int index = lastIndexOfKey(key); + return index >= 0 ? get(index) : null; + } + + @Override + public int indexOfKey(final K key) { + final ListIterator<E> iterator = listIterator(); + while (iterator.hasNext()) { + final int index = iterator.nextIndex(); + if (Objects.equals(iterator.next().getKey(), key)) + return index; + } + return -1; + } + + @Override + public int lastIndexOfKey(final K key) { + final ListIterator<E> iterator = listIterator(size()); + while (iterator.hasPrevious()) { + final int index = iterator.previousIndex(); + if (Objects.equals(iterator.previous().getKey(), key)) + return index; + } + return -1; + } + + @Override + public E set(final int index, @Nullable final E e) { + if (e == null) + throw new NullPointerException("Trying to set a null key"); + return super.set(index, e); + } +} diff --git a/ui/src/main/java/com/wireguard/android/util/ObservableKeyedList.java b/ui/src/main/java/com/wireguard/android/util/ObservableKeyedList.java new file mode 100644 index 00000000..be8ceb9b --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/ObservableKeyedList.java @@ -0,0 +1,19 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util; + +import androidx.databinding.ObservableList; + +import com.wireguard.util.Keyed; +import com.wireguard.util.KeyedList; + +/** + * A list that is both keyed and observable. + */ + +public interface ObservableKeyedList<K, E extends Keyed<? extends K>> + extends KeyedList<K, E>, ObservableList<E> { +} diff --git a/ui/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java b/ui/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java new file mode 100644 index 00000000..1d585856 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java @@ -0,0 +1,198 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util; + +import androidx.annotation.Nullable; + +import com.wireguard.util.Keyed; +import com.wireguard.util.SortedKeyedList; + +import java.util.AbstractList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.Spliterator; + +/** + * KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses + * binary search to improve lookup and replacement times to O(log(n)). However, due to the + * array-based nature of this class, insertion and removal of elements with anything but the largest + * key still require O(n) time. + */ + +public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>> + extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> { + @Nullable private final Comparator<? super K> comparator; + private final transient KeyList<K, E> keyList = new KeyList<>(this); + + @SuppressWarnings("WeakerAccess") + public ObservableSortedKeyedArrayList() { + comparator = null; + } + + public ObservableSortedKeyedArrayList(final Comparator<? super K> comparator) { + this.comparator = comparator; + } + + public ObservableSortedKeyedArrayList(final Collection<? extends E> c) { + this(); + addAll(c); + } + + public ObservableSortedKeyedArrayList(final SortedKeyedList<K, E> other) { + this(other.comparator()); + addAll(other); + } + + @Override + public boolean add(final E e) { + final int insertionPoint = getInsertionPoint(e); + if (insertionPoint < 0) { + // Skipping insertion is non-destructive if the new and existing objects are the same. + if (e == get(-insertionPoint - 1)) + return false; + throw new IllegalArgumentException("Element with same key already exists in list"); + } + super.add(insertionPoint, e); + return true; + } + + @Override + public void add(final int index, final E e) { + final int insertionPoint = getInsertionPoint(e); + if (insertionPoint < 0) + throw new IllegalArgumentException("Element with same key already exists in list"); + if (insertionPoint != index) + throw new IndexOutOfBoundsException("Wrong index given for element"); + super.add(index, e); + } + + @Override + public boolean addAll(final Collection<? extends E> c) { + boolean didChange = false; + for (final E e : c) + if (add(e)) + didChange = true; + return didChange; + } + + @Override + public boolean addAll(int index, final Collection<? extends E> c) { + for (final E e : c) + add(index++, e); + return true; + } + + @Nullable + @Override + public Comparator<? super K> comparator() { + return comparator; + } + + @Override + public K firstKey() { + if (isEmpty()) + // The parameter in the exception is only to shut + // lint up, we never care for the exception message. + throw new NoSuchElementException("Empty set"); + return get(0).getKey(); + } + + private int getInsertionPoint(final E e) { + if (comparator != null) { + return -Collections.binarySearch(keyList, e.getKey(), comparator) - 1; + } else { + @SuppressWarnings("unchecked") final List<Comparable<? super K>> list = + (List<Comparable<? super K>>) keyList; + return -Collections.binarySearch(list, e.getKey()) - 1; + } + } + + @Override + public int indexOfKey(final K key) { + final int index; + if (comparator != null) { + index = Collections.binarySearch(keyList, key, comparator); + } else { + @SuppressWarnings("unchecked") final List<Comparable<? super K>> list = + (List<Comparable<? super K>>) keyList; + index = Collections.binarySearch(list, key); + } + return index >= 0 ? index : -1; + } + + @Override + public Set<K> keySet() { + return keyList; + } + + @Override + public int lastIndexOfKey(final K key) { + // There can never be more than one element with the same key in the list. + return indexOfKey(key); + } + + @Override + public K lastKey() { + if (isEmpty()) + // The parameter in the exception is only to shut + // lint up, we never care for the exception message. + throw new NoSuchElementException("Empty set"); + return get(size() - 1).getKey(); + } + + @Override + public E set(final int index, final E e) { + final int order; + if (comparator != null) { + order = comparator.compare(e.getKey(), get(index).getKey()); + } else { + @SuppressWarnings("unchecked") final Comparable<? super K> key = + (Comparable<? super K>) e.getKey(); + order = key.compareTo(get(index).getKey()); + } + if (order != 0) { + // Allow replacement if the new key would be inserted adjacent to the replaced element. + final int insertionPoint = getInsertionPoint(e); + if (insertionPoint < index || insertionPoint > index + 1) + throw new IndexOutOfBoundsException("Wrong index given for element"); + } + return super.set(index, e); + } + + @Override + public Collection<E> values() { + return this; + } + + private static final class KeyList<K, E extends Keyed<? extends K>> + extends AbstractList<K> implements Set<K> { + private final ObservableSortedKeyedArrayList<K, E> list; + + private KeyList(final ObservableSortedKeyedArrayList<K, E> list) { + this.list = list; + } + + @Override + public K get(final int index) { + return list.get(index).getKey(); + } + + @Override + public int size() { + return list.size(); + } + + @Override + @SuppressWarnings("EmptyMethod") + public Spliterator<K> spliterator() { + return super.spliterator(); + } + } +} diff --git a/ui/src/main/java/com/wireguard/android/util/ObservableSortedKeyedList.java b/ui/src/main/java/com/wireguard/android/util/ObservableSortedKeyedList.java new file mode 100644 index 00000000..d796704e --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/util/ObservableSortedKeyedList.java @@ -0,0 +1,17 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util; + +import com.wireguard.util.Keyed; +import com.wireguard.util.SortedKeyedList; + +/** + * A list that is both sorted/keyed and observable. + */ + +public interface ObservableSortedKeyedList<K, E extends Keyed<? extends K>> + extends ObservableKeyedList<K, E>, SortedKeyedList<K, E> { +} |