Compare commits

...

1 Commits

Author SHA1 Message Date
e851f381a0
Volume provider (unsecure!!!) 2023-08-28 00:35:12 +02:00
29 changed files with 862 additions and 256 deletions

View File

@ -5,7 +5,8 @@ project(DroidFS)
option(GOCRYPTFS "build libgocryptfs" ON) option(GOCRYPTFS "build libgocryptfs" ON)
option(CRYFS "build libcryfs" ON) option(CRYFS "build libcryfs" ON)
add_library(memfile SHARED src/main/native/memfile.cpp) add_library(memfile SHARED src/main/native/memfile.c)
target_link_libraries(memfile log)
if (GOCRYPTFS) if (GOCRYPTFS)
add_library(gocryptfs SHARED IMPORTED) add_library(gocryptfs SHARED IMPORTED)

View File

@ -3,10 +3,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"> android:installLocation="auto">
<permission
android:name="${applicationId}.WRITE_TEMPORARY_STORAGE"
android:protectionLevel="signature" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
@ -68,16 +64,21 @@
</receiver> </receiver>
<provider <provider
android:name=".content_providers.MemoryFileProvider" android:name=".content_providers.TemporaryFileProvider"
android:authorities="${applicationId}.memory_provider" android:authorities="${applicationId}.temporary_provider"
android:exported="true" android:exported="true"/>
android:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE" />
<provider <provider
android:name=".content_providers.DiskFileProvider" android:authorities="${applicationId}.volume_provider"
android:authorities="${applicationId}.disk_provider" android:name=".content_providers.VolumeProvider"
android:exported="true" android:exported="true"
android:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE" /> android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,225 @@
package sushi.hardcore.droidfs
import android.app.ActivityManager
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.ParcelFileDescriptor
import android.system.Os
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.Compat
import sushi.hardcore.droidfs.util.Wiper
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.util.UUID
class EncryptedFileProvider(context: Context) {
companion object {
private const val TAG = "EncryptedFileProvider"
fun getTmpFilesDir(context: Context) = File(context.cacheDir, "tmp")
}
private val memoryInfo = ActivityManager.MemoryInfo()
private val isMemFileSupported = Compat.isMemFileSupported()
private val tmpFilesDir by lazy { getTmpFilesDir(context) }
private val handler by lazy { Handler(context.mainLooper) }
init {
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(
memoryInfo
)
}
class ExportedDiskFile private constructor(
path: String,
private val file: File,
private val handler: Handler
) : ExportedFile(path) {
companion object {
fun create(path: String, tmpFilesDir: File, handler: Handler): ExportedDiskFile? {
val uuid = UUID.randomUUID().toString()
val file = File(tmpFilesDir, uuid)
return if (file.createNewFile()) {
ExportedDiskFile(path, file, handler)
} else {
null
}
}
}
override fun open(mode: Int, furtive: Boolean): ParcelFileDescriptor {
return if (furtive) {
ParcelFileDescriptor.open(file, mode, handler) {
free()
}
} else {
ParcelFileDescriptor.open(file, mode)
}
}
override fun free() {
Wiper.wipe(file)
}
}
class ExportedMemFile private constructor(path: String, private val file: MemFile) :
ExportedFile(path) {
companion object {
fun create(path: String, size: Long): ExportedMemFile? {
val uuid = UUID.randomUUID().toString()
MemFile.create(uuid, size)?.let {
return ExportedMemFile(path, it)
}
return null
}
}
override fun open(mode: Int, furtive: Boolean): ParcelFileDescriptor {
val fd = if (furtive) {
file.toParcelFileDescriptor()
} else {
file.dup()
}
if (mode and ParcelFileDescriptor.MODE_TRUNCATE != 0) {
Os.ftruncate(fd.fileDescriptor, 0)
} else {
FileInputStream(fd.fileDescriptor).apply {
channel.position(0)
close()
}
}
return fd
}
override fun free() = file.close()
}
abstract class ExportedFile(val path: String) {
var isValid = true
private set
fun invalidate() {
isValid = false
}
/**
* @param furtive If set to true, the file will be deleted when closed
*/
abstract fun open(mode: Int, furtive: Boolean): ParcelFileDescriptor
abstract fun free()
}
fun createFile(
path: String,
size: Long,
): ExportedFile? {
return if (size > memoryInfo.availMem * 0.8) {
ExportedDiskFile.create(
path,
tmpFilesDir,
handler,
)
} else if (isMemFileSupported) {
ExportedMemFile.create(path, size) as ExportedFile
} else {
null
}
}
fun exportFile(
exportedFile: ExportedFile,
encryptedVolume: EncryptedVolume,
): Boolean {
val fd = exportedFile.open(ParcelFileDescriptor.MODE_WRITE_ONLY, false).fileDescriptor
return encryptedVolume.exportFile(exportedFile.path, FileOutputStream(fd))
}
enum class Error {
SUCCESS,
INVALID_STATE,
WRITE_ACCESS_DENIED,
UNSUPPORTED_APPEND,
UNSUPPORTED_RW,
;
fun log() {
Log.e(
TAG, when (this) {
SUCCESS -> "No error"
INVALID_STATE -> "Read after write is not supported"
WRITE_ACCESS_DENIED -> "Write access unauthorized"
UNSUPPORTED_APPEND -> "Appending is not supported"
UNSUPPORTED_RW -> "Read-write access requires Android 11 or later"
}
)
}
}
/**
* @param furtive If set to true, the file will be deleted when closed
*/
fun openFile(
file: ExportedFile,
mode: String,
encryptedVolume: EncryptedVolume,
volumeScope: CoroutineScope,
furtive: Boolean,
allowWrites: Boolean,
): Pair<ParcelFileDescriptor?, Error> {
val mode = ParcelFileDescriptor.parseMode(mode)
return if (mode and ParcelFileDescriptor.MODE_READ_ONLY != 0) {
if (!file.isValid) return Pair(null, Error.INVALID_STATE)
Pair(file.open(mode, furtive), Error.SUCCESS)
} else {
if (!allowWrites) {
return Pair(null, Error.WRITE_ACCESS_DENIED)
}
fun import(input: InputStream): Boolean {
return if (encryptedVolume.importFile(input, file.path)) {
true
} else {
Log.e(TAG, "Failed to import file")
false
}
}
if (mode and ParcelFileDescriptor.MODE_WRITE_ONLY != 0) {
if (mode and ParcelFileDescriptor.MODE_APPEND != 0) {
return Pair(null, Error.UNSUPPORTED_APPEND)
}
if (mode and ParcelFileDescriptor.MODE_TRUNCATE == 0) {
Log.w(TAG, "Truncating file despite not being requested")
}
val pipe = ParcelFileDescriptor.createReliablePipe()
val input = FileInputStream(pipe[0].fileDescriptor)
volumeScope.launch {
if (import(input)) {
file.invalidate()
}
}
Pair(pipe[1], Error.SUCCESS)
} else { // read-write
if (!file.isValid) return Pair(null, Error.INVALID_STATE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val fd = file.open(mode, false)
Pair(ParcelFileDescriptor.wrap(fd, handler) { e ->
if (e == null) {
import(FileInputStream(fd.fileDescriptor))
if (furtive) {
file.free()
}
}
}, Error.SUCCESS)
} else {
Pair(null, Error.UNSUPPORTED_RW)
}
}
}
}
}

View File

@ -4,66 +4,48 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import sushi.hardcore.droidfs.content_providers.DiskFileProvider import androidx.preference.PreferenceManager
import sushi.hardcore.droidfs.content_providers.MemoryFileProvider
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.Version
import java.io.File import java.io.File
class FileShare(private val encryptedVolume: EncryptedVolume, private val context: Context) { class FileShare(context: Context) {
companion object { companion object {
private const val content_type_all = "*/*" private const val CONTENT_TYPE_ANY = "*/*"
fun getContentType(filename: String, previousContentType: String?): String { private fun getContentType(filename: String, previousContentType: String?): String {
if (content_type_all != previousContentType) { if (CONTENT_TYPE_ANY != previousContentType) {
var contentType = MimeTypeMap.getSingleton() var contentType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(File(filename).extension) .getMimeTypeFromExtension(File(filename).extension)
if (contentType == null) { if (contentType == null) {
contentType = content_type_all contentType = CONTENT_TYPE_ANY
} }
if (previousContentType == null) { if (previousContentType == null) {
return contentType return contentType
} else if (previousContentType != contentType) { } else if (previousContentType != contentType) {
return content_type_all return CONTENT_TYPE_ANY
} }
} }
return previousContentType return previousContentType
} }
} }
private val fileProvider: TemporaryFileProvider<*> private val usfSafWrite = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("usf_saf_write", false)
init { private fun exportFile(exportedFile: EncryptedFileProvider.ExportedFile, size: Long, volumeId: Int, previousContentType: String? = null): Pair<Uri, String>? {
var provider: MemoryFileProvider? = null val uri = TemporaryFileProvider.instance.exportFile(exportedFile, size, volumeId) ?: return null
System.getProperty("os.version")?.let { return Pair(uri, getContentType(File(exportedFile.path).name, previousContentType))
if (Version(it) >= Version("3.17")) {
provider = MemoryFileProvider()
}
}
fileProvider = provider ?: DiskFileProvider()
} }
private fun exportFile(path: String, size: Long, previousContentType: String? = null): Pair<Uri?, String?> { fun share(files: List<Pair<String, Long>>, volumeId: Int): Pair<Intent?, Int?> {
val fileName = File(path).name
val uri = fileProvider.newFile(fileName, size)
if (uri != null) {
if (encryptedVolume.exportFile(context, path, uri)) {
return Pair(uri, getContentType(fileName, previousContentType))
}
}
return Pair(null, null)
}
fun share(files: List<Pair<String, Long>>): Pair<Intent?, String?> {
var contentType: String? = null var contentType: String? = null
val uris = ArrayList<Uri>(files.size) val uris = ArrayList<Uri>(files.size)
for ((path, size) in files) { for ((path, size) in files) {
val result = exportFile(path, size, contentType) val exportedFile = TemporaryFileProvider.instance.encryptedFileProvider.createFile(path, size)
contentType = if (result.first == null) { ?: return Pair(null, R.string.export_failed_create)
return Pair(null, path) val result = exportFile(exportedFile, size, volumeId, contentType)
contentType = if (result == null) {
return Pair(null, R.string.export_failed_export)
} else { } else {
uris.add(result.first!!) uris.add(result.first)
result.second result.second
} }
} }
@ -79,15 +61,18 @@ class FileShare(private val encryptedVolume: EncryptedVolume, private val contex
}, null) }, null)
} }
fun openWith(path: String, size: Long): Intent? { fun openWith(exportedFile: EncryptedFileProvider.ExportedFile, size: Long, volumeId: Int): Pair<Intent?, Int?> {
val result = exportFile(path, size) val result = exportFile(exportedFile, size, volumeId)
return if (result.first != null) { return if (result == null) {
Intent(Intent.ACTION_VIEW).apply { Pair(null, R.string.export_failed_export)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
setDataAndType(result.first, result.second)
}
} else { } else {
null Pair(Intent(Intent.ACTION_VIEW).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (usfSafWrite) {
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
setDataAndType(result.first, result.second)
}, null)
} }
} }
} }

View File

@ -1,6 +1,7 @@
package sushi.hardcore.droidfs package sushi.hardcore.droidfs
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.system.Os
class MemFile private constructor(private val fd: Int) { class MemFile private constructor(private val fd: Int) {
companion object { companion object {
@ -15,8 +16,7 @@ class MemFile private constructor(private val fd: Int) {
} }
} }
private external fun close(fd: Int) fun dup(): ParcelFileDescriptor = ParcelFileDescriptor.fromFd(fd)
fun toParcelFileDescriptor(): ParcelFileDescriptor = ParcelFileDescriptor.adoptFd(fd)
fun getParcelFileDescriptor(): ParcelFileDescriptor = ParcelFileDescriptor.fromFd(fd) fun close() = Os.close(ParcelFileDescriptor.adoptFd(fd).fileDescriptor)
fun close() = close(fd)
} }

View File

@ -7,8 +7,16 @@ import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.preference.* import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreference
import androidx.preference.SwitchPreferenceCompat
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
import sushi.hardcore.droidfs.content_providers.VolumeProvider
import sushi.hardcore.droidfs.databinding.ActivitySettingsBinding import sushi.hardcore.droidfs.databinding.ActivitySettingsBinding
import sushi.hardcore.droidfs.util.Compat
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog import sushi.hardcore.droidfs.widgets.EditTextDialog
@ -150,6 +158,47 @@ class SettingsActivity : BaseActivity() {
true true
} }
} }
val switchKeepOpen = findPreference<SwitchPreference>("usf_keep_open")!!
val switchExternalOpen = findPreference<SwitchPreference>("usf_open")!!
val switchExpose = findPreference<SwitchPreference>("usf_expose")!!
val switchSafWrite = findPreference<SwitchPreference>("usf_saf_write")!!
fun updateView(usfOpen: Boolean? = null, usfKeepOpen: Boolean? = null, usfExpose: Boolean? = null) {
val usfKeepOpen = usfKeepOpen ?: switchKeepOpen.isChecked
switchExpose.isEnabled = usfKeepOpen
switchSafWrite.isEnabled = usfOpen ?: switchExternalOpen.isChecked || (usfKeepOpen && usfExpose ?: switchExpose.isChecked)
}
updateView()
switchKeepOpen.setOnPreferenceChangeListener { _, checked ->
updateView(usfKeepOpen = checked as Boolean)
true
}
switchExternalOpen.setOnPreferenceChangeListener { _, checked ->
updateView(usfOpen = checked as Boolean)
true
}
switchExpose.setOnPreferenceChangeListener { _, checked ->
if (checked as Boolean) {
if (!Compat.isMemFileSupported()) {
CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme)
.setTitle(R.string.error)
.setMessage("Your current kernel does not support memfd_create(). This feature requires a minimum kernel version of ${Compat.MEMFD_CREATE_MINIMUM_KERNEL_VERSION}.")
.setPositiveButton(R.string.ok, null)
.show()
return@setOnPreferenceChangeListener false
}
}
VolumeProvider.usfExpose = checked
updateView(usfExpose = checked)
VolumeProvider.notifyRootChanged(requireContext())
true
}
switchSafWrite.setOnPreferenceChangeListener { _, checked ->
VolumeProvider.usfSafWrite = checked as Boolean
TemporaryFileProvider.usfSafWrite = checked
true
}
} }
} }
} }

View File

@ -1,12 +1,15 @@
package sushi.hardcore.droidfs package sushi.hardcore.droidfs
import android.content.Context
import android.provider.DocumentsContract
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import sushi.hardcore.droidfs.content_providers.VolumeProvider
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
class VolumeManager { class VolumeManager(private val context: Context) {
private var id = 0 private var id = 0
private val volumes = HashMap<Int, EncryptedVolume>() private val volumes = HashMap<Int, EncryptedVolume>()
private val volumesData = HashMap<VolumeData, Int>() private val volumesData = HashMap<VolumeData, Int>()
@ -15,6 +18,7 @@ class VolumeManager {
fun insert(volume: EncryptedVolume, data: VolumeData): Int { fun insert(volume: EncryptedVolume, data: VolumeData): Int {
volumes[id] = volume volumes[id] = volume
volumesData[data] = id volumesData[data] = id
VolumeProvider.notifyRootChanged(context)
return id++ return id++
} }
@ -30,6 +34,10 @@ class VolumeManager {
return volumes[id] return volumes[id]
} }
fun listVolumes(): List<Pair<Int, VolumeData>> {
return volumesData.map { (data, id) -> Pair(id, data) }
}
fun getCoroutineScope(volumeId: Int): CoroutineScope { fun getCoroutineScope(volumeId: Int): CoroutineScope {
return scopes[volumeId] ?: CoroutineScope(SupervisorJob() + Dispatchers.IO).also { scopes[volumeId] = it } return scopes[volumeId] ?: CoroutineScope(SupervisorJob() + Dispatchers.IO).also { scopes[volumeId] = it }
} }
@ -41,6 +49,7 @@ class VolumeManager {
volumesData.filter { it.value == id }.forEach { volumesData.filter { it.value == id }.forEach {
volumesData.remove(it.key) volumesData.remove(it.key)
} }
VolumeProvider.notifyRootChanged(context)
} }
} }
@ -51,5 +60,6 @@ class VolumeManager {
} }
volumes.clear() volumes.clear()
volumesData.clear() volumesData.clear()
VolumeProvider.notifyRootChanged(context)
} }
} }

View File

@ -6,8 +6,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import sushi.hardcore.droidfs.content_providers.MemoryFileProvider import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
import sushi.hardcore.droidfs.content_providers.DiskFileProvider
class VolumeManagerApp : Application(), DefaultLifecycleObserver { class VolumeManagerApp : Application(), DefaultLifecycleObserver {
companion object { companion object {
@ -21,8 +20,9 @@ class VolumeManagerApp : Application(), DefaultLifecycleObserver {
} }
} }
private var usfKeepOpen = false private var usfKeepOpen = false
var isExporting = false
var isStartingExternalApp = false var isStartingExternalApp = false
val volumeManager = VolumeManager() val volumeManager = VolumeManager(this)
override fun onCreate() { override fun onCreate() {
super<Application>.onCreate() super<Application>.onCreate()
@ -46,8 +46,9 @@ class VolumeManagerApp : Application(), DefaultLifecycleObserver {
if (!usfKeepOpen) { if (!usfKeepOpen) {
volumeManager.closeAll() volumeManager.closeAll()
} }
DiskFileProvider.wipe() if (!usfKeepOpen || !isExporting) {
MemoryFileProvider.wipe() TemporaryFileProvider.instance.wipe()
}
} }
} }
} }

View File

@ -1,68 +0,0 @@
package sushi.hardcore.droidfs.content_providers
import android.net.Uri
import android.os.ParcelFileDescriptor
import sushi.hardcore.droidfs.BuildConfig
import sushi.hardcore.droidfs.util.Wiper
import java.io.File
import java.util.UUID
class DiskFileProvider: TemporaryFileProvider<File>() {
companion object {
private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".disk_provider"
private val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY")
const val TEMPORARY_FILES_DIR_NAME = "temp"
private lateinit var tempFilesDir: File
private var files = HashMap<Uri, TemporaryFileProvider<File>.SharedFile>()
fun wipe() {
for (i in files.values) {
Wiper.wipe(i.file)
}
files.clear()
tempFilesDir.listFiles()?.let {
for (file in it) {
Wiper.wipe(file)
}
}
}
}
override fun onCreate(): Boolean {
context?.let {
tempFilesDir = File(it.cacheDir, TEMPORARY_FILES_DIR_NAME)
return tempFilesDir.mkdirs()
}
return false
}
override fun getFile(uri: Uri): SharedFile? = files[uri]
override fun newFile(name: String, size: Long): Uri? {
val uuid = UUID.randomUUID().toString()
val file = File(tempFilesDir, uuid)
return if (file.createNewFile()) {
Uri.withAppendedPath(CONTENT_URI, uuid).also {
files[it] = SharedFile(name, size, file)
}
} else {
null
}
}
override fun delete(uri: Uri, givenSelection: String?, givenSelectionArgs: Array<String>?): Int {
return if (files.remove(uri)?.file?.also { Wiper.wipe(it) } == null) 0 else 1
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
return if (("w" in mode && callingPackage == BuildConfig.APPLICATION_ID) || "w" !in mode) {
files[uri]?.file?.let {
return ParcelFileDescriptor.open(it, ParcelFileDescriptor.parseMode(mode))
}
} else {
throw SecurityException("Read-only access")
}
}
}

View File

@ -1,48 +0,0 @@
package sushi.hardcore.droidfs.content_providers
import android.net.Uri
import android.os.ParcelFileDescriptor
import sushi.hardcore.droidfs.BuildConfig
import sushi.hardcore.droidfs.MemFile
import java.io.FileInputStream
import java.util.UUID
class MemoryFileProvider: TemporaryFileProvider<MemFile>() {
companion object {
private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".memory_provider"
private val BASE_URI: Uri = Uri.parse("content://$AUTHORITY")
private var files = HashMap<Uri, TemporaryFileProvider<MemFile>.SharedFile>()
fun wipe() {
for (i in files.values) {
i.file.close()
}
files.clear()
}
}
override fun onCreate(): Boolean = true
override fun getFile(uri: Uri): SharedFile? = files[uri]
override fun newFile(name: String, size: Long): Uri? {
val uuid = UUID.randomUUID().toString()
return Uri.withAppendedPath(BASE_URI, uuid).also {
files[it] = SharedFile(name, size, MemFile.create(uuid, size) ?: return null)
}
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return if (files.remove(uri)?.file?.close() == null) 0 else 1
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
return files[uri]?.file?.getParcelFileDescriptor()?.also {
FileInputStream(it.fileDescriptor).apply {
channel.position(0)
close()
}
}
}
}

View File

@ -5,20 +5,83 @@ import android.content.ContentValues
import android.database.Cursor import android.database.Cursor
import android.database.MatrixCursor import android.database.MatrixCursor
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.preference.PreferenceManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.BuildConfig
import sushi.hardcore.droidfs.EncryptedFileProvider
import sushi.hardcore.droidfs.VolumeManager
import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.util.Wiper
import java.io.File import java.io.File
import java.util.UUID
abstract class TemporaryFileProvider<T>: ContentProvider() { class TemporaryFileProvider : ContentProvider() {
protected inner class SharedFile(val name: String, val size: Long, val file: T) private inner class ProvidedFile(
val file: EncryptedFileProvider.ExportedFile,
val size: Long,
val volumeId: Int
)
protected abstract fun getFile(uri: Uri): SharedFile? companion object {
abstract fun newFile(name: String, size: Long): Uri? private const val TAG = "TemporaryFileProvider"
private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".temporary_provider"
private val BASE_URI: Uri = Uri.parse("content://$AUTHORITY")
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? { lateinit var instance: TemporaryFileProvider
val file = getFile(uri) ?: return null private set
var usfSafWrite = false
}
private lateinit var volumeManager: VolumeManager
lateinit var encryptedFileProvider: EncryptedFileProvider
private val files = HashMap<Uri, ProvidedFile>()
override fun onCreate(): Boolean {
return context?.let {
volumeManager = (it.applicationContext as VolumeManagerApp).volumeManager
usfSafWrite =
PreferenceManager.getDefaultSharedPreferences(it).getBoolean("usf_saf_write", false)
encryptedFileProvider = EncryptedFileProvider(it)
instance = this
val tmpFilesDir = EncryptedFileProvider.getTmpFilesDir(it)
val success = tmpFilesDir.mkdirs()
// wipe any additional files not previously deleted
GlobalScope.launch(Dispatchers.IO) {
tmpFilesDir.listFiles()?.onEach { f -> Wiper.wipe(f) }
}
success
} ?: false
}
fun exportFile(
exportedFile: EncryptedFileProvider.ExportedFile,
size: Long,
volumeId: Int
): Uri? {
if (!encryptedFileProvider.exportFile(exportedFile, volumeManager.getVolume(volumeId)!!)) {
return null
}
return Uri.withAppendedPath(BASE_URI, UUID.randomUUID().toString()).also {
files[it] = ProvidedFile(exportedFile, size, volumeId)
}
}
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
val file = files[uri] ?: return null
return MatrixCursor(arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), 1).apply { return MatrixCursor(arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), 1).apply {
addRow(arrayOf(file.name, file.size)) addRow(arrayOf(File(file.file.path).name, file.size))
} }
} }
@ -26,11 +89,57 @@ abstract class TemporaryFileProvider<T>: ContentProvider() {
throw UnsupportedOperationException("Operation not supported") throw UnsupportedOperationException("Operation not supported")
} }
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int { override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?
): Int {
throw UnsupportedOperationException("Operation not supported") throw UnsupportedOperationException("Operation not supported")
} }
override fun getType(uri: Uri): String = getFile(uri)?.name?.let { override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return if (files.remove(uri)?.file?.also { it.free() } == null) 0 else 1
}
override fun getType(uri: Uri): String = files[uri]?.file?.path?.let {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(it).extension) MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(it).extension)
} ?: "application/octet-stream" } ?: "application/octet-stream"
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
files[uri]?.let { file ->
val encryptedVolume = volumeManager.getVolume(file.volumeId) ?: run {
Log.e(TAG, "Volume closed for $uri")
return null
}
val result = encryptedFileProvider.openFile(
file.file,
mode,
encryptedVolume,
volumeManager.getCoroutineScope(file.volumeId),
false,
usfSafWrite,
)
when (result.second) {
EncryptedFileProvider.Error.SUCCESS -> return result.first!!
EncryptedFileProvider.Error.WRITE_ACCESS_DENIED -> Log.e(
TAG,
"Unauthorized write access requested from $callingPackage to $uri"
)
else -> result.second.log()
}
}
return null
}
// this must not be cancelled
fun wipe() = GlobalScope.launch(Dispatchers.IO) {
synchronized(this@TemporaryFileProvider) {
for (i in files.values) {
i.file.free()
}
files.clear()
}
}
} }

View File

@ -0,0 +1,280 @@
package sushi.hardcore.droidfs.content_providers
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.database.MatrixCursor
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import android.provider.DocumentsProvider
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.preference.PreferenceManager
import sushi.hardcore.droidfs.BuildConfig
import sushi.hardcore.droidfs.EncryptedFileProvider
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.VolumeData
import sushi.hardcore.droidfs.VolumeManager
import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils
import java.io.File
class VolumeProvider: DocumentsProvider() {
companion object {
private const val TAG = "DocumentsProvider"
private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".volume_provider"
private val DEFAULT_ROOT_PROJECTION = arrayOf(
DocumentsContract.Root.COLUMN_ROOT_ID,
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.COLUMN_ICON,
DocumentsContract.Root.COLUMN_TITLE,
DocumentsContract.Root.COLUMN_DOCUMENT_ID,
)
private val DEFAULT_DOCUMENT_PROJECTION = arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_FLAGS,
DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
)
var usfExpose = false
var usfSafWrite = false
fun notifyRootChanged(context: Context) {
context.contentResolver.notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null)
}
}
private lateinit var volumeManager: VolumeManager
private val volumes = HashMap<String, Pair<Int, VolumeData>>()
private lateinit var encryptedFileProvider: EncryptedFileProvider
override fun onCreate(): Boolean {
val context = (context ?: return false)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
usfExpose = sharedPreferences.getBoolean("usf_expose", false)
usfSafWrite = sharedPreferences.getBoolean("usf_saf_write", false)
volumeManager = (context.applicationContext as VolumeManagerApp).volumeManager
encryptedFileProvider = EncryptedFileProvider(context)
return true
}
override fun queryRoots(projection: Array<out String>?): Cursor {
val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
if (!usfExpose) return cursor
val previousVolumeIds = volumes.keys.toSet()
volumes.clear()
for (volume in volumeManager.listVolumes()) {
var flags = DocumentsContract.Root.FLAG_LOCAL_ONLY or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
if (usfSafWrite && volume.second.canWrite(context!!.filesDir.path)) {
flags = flags or DocumentsContract.Root.FLAG_SUPPORTS_CREATE
}
cursor.newRow().apply {
add(DocumentsContract.Root.COLUMN_ROOT_ID, volume.second.name)
add(DocumentsContract.Root.COLUMN_FLAGS, flags)
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.icon_document_provider)
add(DocumentsContract.Root.COLUMN_TITLE, volume.second.name)
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, volume.first)
}
volumes[volume.first.toString()] = volume
}
for (i in previousVolumeIds) {
if (!volumes.containsKey(i)) {
val uri = DocumentsContract.buildRootUri(AUTHORITY, i)
context?.revokeUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
}
return cursor
}
internal data class DocumentData(
val rootId: String,
val volumeId: Int,
val volumeData: VolumeData,
val encryptedVolume: EncryptedVolume,
val path: String
) {
fun child(childPath: String) = DocumentData(rootId, volumeId, volumeData, encryptedVolume, childPath)
}
private fun parseDocumentId(documentId: String): DocumentData? {
val splits = documentId.split("/", limit = 2)
if (splits.size > 2) {
return null
} else {
volumes[splits[0]]?.let {
val encryptedVolume = volumeManager.getVolume(it.first) ?: return null
val path = "/"+if (splits.size == 2) {
splits[1]
} else {
""
}
return DocumentData(splits[0], it.first, it.second, encryptedVolume, path)
}
}
return null
}
override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean {
if (!usfExpose) return false
val parent = parseDocumentId(parentDocumentId) ?: return false
val child = parseDocumentId(documentId) ?: return false
return parent.rootId == child.rootId && PathUtils.isChildOf(child.path, parent.path)
}
private fun addDocumentRow(cursor: MatrixCursor, volumeData: VolumeData, documentId: String, name: String, stat: Stat) {
val isDirectory = stat.type == Stat.S_IFDIR
var flags = 0
if (usfSafWrite && volumeData.canWrite(context!!.filesDir.path)) {
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
if (isDirectory) {
flags = flags or DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
} else if (stat.type == Stat.S_IFREG) {
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_WRITE
}
}
val mimeType = if (isDirectory) {
DocumentsContract.Document.MIME_TYPE_DIR
} else {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(name).extension)
?: "application/octet-stream"
}
cursor.newRow().apply {
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId)
add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, name)
add(DocumentsContract.Document.COLUMN_MIME_TYPE, mimeType)
add(DocumentsContract.Document.COLUMN_FLAGS, flags)
add(DocumentsContract.Document.COLUMN_SIZE, stat.size)
add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, stat.mTime)
}
}
override fun queryDocument(documentId: String, projection: Array<out String>?): Cursor {
val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
if (!usfExpose) return cursor
val document = parseDocumentId(documentId) ?: return cursor
document.encryptedVolume.getAttr(document.path)?.let { stat ->
val name = if (document.path == "/") {
document.volumeData.shortName
} else {
File(document.path).name
}
addDocumentRow(cursor, document.volumeData, documentId, name, stat)
}
return cursor
}
override fun queryChildDocuments(
parentDocumentId: String,
projection: Array<out String>?,
sortOrder: String?
): Cursor {
val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
if (!usfExpose) return cursor
val document = parseDocumentId(parentDocumentId) ?: return cursor
document.encryptedVolume.readDir(document.path)?.let { content ->
for (i in content) {
if (i.isParentFolder) continue
addDocumentRow(cursor, document.volumeData, document.rootId+i.fullPath, i.name, i.stat)
}
}
return cursor
}
override fun openDocument(
documentId: String,
mode: String,
signal: CancellationSignal?
): ParcelFileDescriptor? {
if (!usfExpose) return null
val document = parseDocumentId(documentId) ?: return null
val size = document.encryptedVolume.getAttr(document.path)?.size ?: run {
Log.e(TAG, "stat() failed")
return null
}
val exportedFile = encryptedFileProvider.createFile(document.path, size) ?: run {
Log.e(TAG, "Can't create exported file")
return null
}
if (!encryptedFileProvider.exportFile(exportedFile, document.encryptedVolume)) {
Log.e(TAG, "File export failed")
return null
}
val result = encryptedFileProvider.openFile(
exportedFile,
mode,
document.encryptedVolume,
volumeManager.getCoroutineScope(document.volumeId),
true,
usfSafWrite,
)
when (result.second) {
EncryptedFileProvider.Error.SUCCESS -> return result.first!!
EncryptedFileProvider.Error.WRITE_ACCESS_DENIED -> Log.e(TAG, "Unauthorized write access requested from $callingPackage")
else -> result.second.log()
}
return null
}
override fun createDocument(
parentDocumentId: String,
mimeType: String?,
displayName: String
): String? {
if (!usfExpose || !usfSafWrite) return null
val document = parseDocumentId(parentDocumentId) ?: return null
val newFile = PathUtils.pathJoin(document.path, displayName)
val f = document.encryptedVolume.openFileWriteMode(newFile)
return if (f == -1L) {
Log.e(TAG, "Failed to create file: $document")
null
} else {
document.encryptedVolume.closeFile(f)
document.rootId+"/"+newFile
}
}
override fun deleteDocument(documentId: String) {
if (!usfExpose || !usfSafWrite) return
fun recursiveRemoveDirectory(document: DocumentData) {
document.encryptedVolume.readDir(document.path)?.forEach { e ->
val childPath = PathUtils.pathJoin(document.path, e.name)
if (e.isDirectory) {
recursiveRemoveDirectory(document.child(childPath))
} else {
document.encryptedVolume.deleteFile(childPath)
}
revokeDocumentPermission(document.rootId+childPath)
}
document.encryptedVolume.rmdir(document.path)
}
val document = parseDocumentId(documentId) ?: return
document.encryptedVolume.getAttr(document.path)?.let { stat ->
if (stat.type == Stat.S_IFDIR) {
recursiveRemoveDirectory(document)
} else {
document.encryptedVolume.deleteFile(document.path)
}
}
}
override fun renameDocument(documentId: String, displayName: String): String {
if (!usfExpose || !usfSafWrite) return documentId
val document = parseDocumentId(documentId) ?: return documentId
val newPath = PathUtils.pathJoin(PathUtils.getParentPath(document.path), displayName)
return if (document.encryptedVolume.rename(document.path, newPath)) {
document.rootId+newPath
} else {
documentId
}
}
}

View File

@ -29,6 +29,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.BaseActivity import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.EncryptedFileProvider
import sushi.hardcore.droidfs.FileShare import sushi.hardcore.droidfs.FileShare
import sushi.hardcore.droidfs.FileTypes import sushi.hardcore.droidfs.FileTypes
import sushi.hardcore.droidfs.LoadingTask import sushi.hardcore.droidfs.LoadingTask
@ -36,8 +37,7 @@ import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.VolumeManagerApp import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter
import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter
import sushi.hardcore.droidfs.content_providers.DiskFileProvider import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
import sushi.hardcore.droidfs.content_providers.MemoryFileProvider
import sushi.hardcore.droidfs.file_operations.FileOperationService import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.file_operations.OperationFile import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.file_operations.TaskResult import sushi.hardcore.droidfs.file_operations.TaskResult
@ -84,9 +84,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
private lateinit var numberOfFilesText: TextView private lateinit var numberOfFilesText: TextView
private lateinit var numberOfFoldersText: TextView private lateinit var numberOfFoldersText: TextView
private lateinit var totalSizeText: TextView private lateinit var totalSizeText: TextView
protected val fileShare by lazy { protected val fileShare by lazy { FileShare(this) }
FileShare(encryptedVolume, this)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -204,22 +202,38 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
startActivity(intent) startActivity(intent)
} }
protected fun onExportFailed(errorResId: Int) {
CustomAlertDialogBuilder(this, theme)
.setTitle(R.string.error)
.setMessage(getString(R.string.tmp_export_failed, getString(errorResId)))
.setPositiveButton(R.string.ok, null)
.show()
}
private fun openWithExternalApp(path: String, size: Long) { private fun openWithExternalApp(path: String, size: Long) {
app.isStartingExternalApp = true app.isExporting = true
object : LoadingTask<Intent?>(this, theme, R.string.loading_msg_export) { val exportedFile = TemporaryFileProvider.instance.encryptedFileProvider.createFile(path, size)
override suspend fun doTask(): Intent? { if (exportedFile == null) {
return fileShare.openWith(path, size) onExportFailed(R.string.export_failed_create)
return
}
val msg = when (exportedFile) {
is EncryptedFileProvider.ExportedMemFile -> R.string.export_mem
is EncryptedFileProvider.ExportedDiskFile -> R.string.export_disk
else -> R.string.loading_msg_export
}
object : LoadingTask<Pair<Intent?, Int?>>(this, theme, msg) {
override suspend fun doTask(): Pair<Intent?, Int?> {
return fileShare.openWith(exportedFile, size, volumeId)
} }
}.startTask(lifecycleScope) { openIntent -> }.startTask(lifecycleScope) { (intent, error) ->
if (openIntent == null) { if (intent == null) {
CustomAlertDialogBuilder(this, theme) onExportFailed(error!!)
.setTitle(R.string.error)
.setMessage(getString(R.string.export_failed, path))
.setPositiveButton(R.string.ok, null)
.show()
} else { } else {
startActivity(openIntent) app.isStartingExternalApp = true
startActivity(intent)
} }
app.isExporting = false
} }
} }
@ -644,8 +658,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (app.isStartingExternalApp) { if (app.isStartingExternalApp) {
MemoryFileProvider.wipe() TemporaryFileProvider.instance.wipe()
DiskFileProvider.wipe()
} }
if (encryptedVolume.isClosed()) { if (encryptedVolume.isClosed()) {
finish() finish()

View File

@ -164,7 +164,7 @@ class ExplorerActivity : BaseExplorerActivity() {
} else { } else {
val adapter = IconTextDialogAdapter(this) val adapter = IconTextDialogAdapter(this)
adapter.items = listOf( adapter.items = listOf(
listOf("importFromOtherVolumes", R.string.import_from_other_volume, R.drawable.icon_transfert), listOf("importFromOtherVolumes", R.string.import_from_other_volume, R.drawable.icon_transfer),
listOf("importFiles", R.string.import_files, R.drawable.icon_encrypt), listOf("importFiles", R.string.import_files, R.drawable.icon_encrypt),
listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder), listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder),
listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown), listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown),
@ -385,26 +385,24 @@ class ExplorerActivity : BaseExplorerActivity() {
true true
} }
R.id.share -> { R.id.share -> {
app.isStartingExternalApp = true
val files = explorerAdapter.selectedItems.map { i -> val files = explorerAdapter.selectedItems.map { i ->
explorerElements[i].let { explorerElements[i].let {
Pair(it.fullPath, it.stat.size) Pair(it.fullPath, it.stat.size)
} }
} }
object : LoadingTask<Pair<Intent?, String?>>(this, theme, R.string.loading_msg_export) { app.isExporting = true
override suspend fun doTask(): Pair<Intent?, String?> { object : LoadingTask<Pair<Intent?, Int?>>(this, theme, R.string.loading_msg_export) {
return fileShare.share(files) override suspend fun doTask(): Pair<Intent?, Int?> {
return fileShare.share(files, volumeId)
} }
}.startTask(lifecycleScope) { (intent, failedItem) -> }.startTask(lifecycleScope) { (intent, error) ->
if (intent == null) { if (intent == null) {
CustomAlertDialogBuilder(this, theme) onExportFailed(error!!)
.setTitle(R.string.error)
.setMessage(getString(R.string.export_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
} else { } else {
app.isStartingExternalApp = true
startActivity(Intent.createChooser(intent, getString(R.string.share_chooser))) startActivity(Intent.createChooser(intent, getString(R.string.share_chooser)))
} }
app.isExporting = false
} }
unselectAll() unselectAll()
true true

View File

@ -6,7 +6,7 @@ import sushi.hardcore.droidfs.util.PathUtils
import java.text.Collator import java.text.Collator
class ExplorerElement(val name: String, val stat: Stat, val parentPath: String) { class ExplorerElement(val name: String, val stat: Stat, val parentPath: String) {
val fullPath: String = PathUtils.pathJoin(parentPath, name) val fullPath: String = PathUtils.pathJoin(parentPath.ifEmpty { "/" }, name)
val collationKey = Collator.getInstance().getCollationKeyForFileName(fullPath) val collationKey = Collator.getInstance().getCollationKeyForFileName(fullPath)
val isDirectory: Boolean val isDirectory: Boolean

View File

@ -160,7 +160,6 @@ abstract class EncryptedVolume: Parcelable {
if (written == length) { if (written == length) {
offset += written offset += written
} else { } else {
inputStream.close()
success = false success = false
break break
} }

View File

@ -1,14 +1,17 @@
package sushi.hardcore.droidfs.filesystems package sushi.hardcore.droidfs.filesystems
class Stat(val type: Int, var size: Long, val mTime: Long) { class Stat(mode: Int, var size: Long, val mTime: Long) {
companion object { companion object {
private const val S_IFMT = 0xF000
const val S_IFDIR = 0x4000 const val S_IFDIR = 0x4000
const val S_IFREG = 0x8000 const val S_IFREG = 0x8000
const val S_IFLNK = 0xA000 const val S_IFLNK = 0xA000
const val PARENT_FOLDER_TYPE = -1 const val PARENT_FOLDER_TYPE = 0xE000
fun parentFolderStat(): Stat { fun parentFolderStat(): Stat {
return Stat(PARENT_FOLDER_TYPE, -1, -1) return Stat(PARENT_FOLDER_TYPE, -1, -1)
} }
} }
val type = mode and S_IFMT
} }

View File

@ -13,4 +13,11 @@ object Compat {
bundle.getParcelable(name) bundle.getParcelable(name)
} }
} }
val MEMFD_CREATE_MINIMUM_KERNEL_VERSION = Version("3.17")
fun isMemFileSupported(): Boolean {
val kernel = System.getProperty("os.version") ?: return false
return Version(kernel) >= MEMFD_CREATE_MINIMUM_KERNEL_VERSION
}
} }

View File

@ -3,7 +3,7 @@ package sushi.hardcore.droidfs.util
import java.lang.Integer.max import java.lang.Integer.max
class Version(inputVersion: String) : Comparable<Version> { class Version(inputVersion: String) : Comparable<Version> {
private var version: String val version: String
init { init {
val regex = "[0-9]+(\\.[0-9]+)*".toRegex() val regex = "[0-9]+(\\.[0-9]+)*".toRegex()

View File

@ -1,24 +1,30 @@
#include <errno.h>
#include <string.h>
#include <sys/mman.h> #include <sys/mman.h>
#include <sys/syscall.h> #include <sys/syscall.h>
#include <unistd.h> #include <unistd.h>
#include <jni.h> #include <jni.h>
#include <android/log.h>
const char* LOG_TAG = "MemFile";
void log_err(const char* function) {
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "%s(): %s", function, strerror(errno));
}
extern "C"
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_sushi_hardcore_droidfs_MemFile_00024Companion_createMemFile(JNIEnv *env, jobject thiz, jstring jname, Java_sushi_hardcore_droidfs_MemFile_00024Companion_createMemFile(JNIEnv *env, jobject thiz, jstring jname,
jlong size) { jlong size) {
const char* name = env->GetStringUTFChars(jname, nullptr); const char* name = (*env)->GetStringUTFChars(env, jname, NULL);
int fd = syscall(SYS_memfd_create, name, MFD_CLOEXEC); int fd = syscall(SYS_memfd_create, name, MFD_CLOEXEC);
if (fd < 0) return fd; if (fd < 0) {
log_err("memfd_create");
return fd;
}
if (ftruncate64(fd, size) == -1) { if (ftruncate64(fd, size) == -1) {
log_err("ftruncate64");
close(fd); close(fd);
return -1; return -1;
} }
return fd; return fd;
} }
extern "C"
JNIEXPORT void JNICALL
Java_sushi_hardcore_droidfs_MemFile_close(JNIEnv *env, jobject thiz, jint fd) {
close(fd);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/colorAccent" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@ -263,4 +263,14 @@
<string name="filesystem_id_changed">The filesystem id in the config file is different to the last time we opened this volume. This could mean an attacker replaced the filesystem with a different one.</string> <string name="filesystem_id_changed">The filesystem id in the config file is different to the last time we opened this volume. This could mean an attacker replaced the filesystem with a different one.</string>
<string name="inaccessible_base_dir">The volume doesn\'t exist or is inaccessible.</string> <string name="inaccessible_base_dir">The volume doesn\'t exist or is inaccessible.</string>
<string name="task_failed">The task failed: %s</string> <string name="task_failed">The task failed: %s</string>
<string name="usf_expose">Expose open volumes</string>
<string name="usf_expose_summary">Allow other applications to browse open volumes as documents providers</string>
<string name="usf_saf_write">Grant write access</string>
<string name="usf_saf_write_summary">Grant write access when opening files with other applications</string>
<string name="saf">Storage Access Framework</string>
<string name="tmp_export_failed">Export failed: %s</string>
<string name="export_failed_create">can\'t create exported file</string>
<string name="export_failed_export">failed to export file</string>
<string name="export_mem">Exporting to memory…</string>
<string name="export_disk">Exporting to disk…</string>
</resources> </resources>

View File

@ -1,6 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/about">
<Preference
android:icon="@drawable/icon_notes"
android:summary="@string/usf_read_doc"
android:title="@string/usf_doc">
<intent
android:action="android.intent.action.VIEW"
android:data="https://forge.chapril.org/hardcoresushi/DroidFS#unsafe-features" />
</Preference>
</PreferenceCategory>
<PreferenceCategory android:title="@string/ux"> <PreferenceCategory android:title="@string/ux">
<SwitchPreference <SwitchPreference
@ -18,23 +31,13 @@
android:icon="@drawable/icon_lock_open" android:icon="@drawable/icon_lock_open"
android:key="usf_decrypt" android:key="usf_decrypt"
android:title="@string/usf_decrypt" /> android:title="@string/usf_decrypt" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_open_in_new"
android:key="usf_open"
android:title="@string/usf_open" />
<SwitchPreference <SwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:icon="@drawable/icon_share" android:icon="@drawable/icon_share"
android:key="usf_share" android:key="usf_share"
android:title="@string/usf_share" /> android:title="@string/usf_share" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_lock_open"
android:key="usf_keep_open"
android:title="@string/usf_keep_open" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/usf_volume_management"> <PreferenceCategory android:title="@string/usf_volume_management">
@ -45,18 +48,35 @@
android:key="usf_fingerprint" android:key="usf_fingerprint"
android:title="@string/usf_fingerprint" /> android:title="@string/usf_fingerprint" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_lock_open"
android:key="usf_keep_open"
android:title="@string/usf_keep_open" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/about"> <PreferenceCategory android:title="@string/saf">
<Preference <SwitchPreference
android:icon="@drawable/icon_notes" android:defaultValue="false"
android:summary="@string/usf_read_doc" android:icon="@drawable/icon_open_in_new"
android:title="@string/usf_doc"> android:key="usf_open"
<intent android:title="@string/usf_open" />
android:action="android.intent.action.VIEW"
android:data="https://forge.chapril.org/hardcoresushi/DroidFS#unsafe-features" /> <SwitchPreference
</Preference> android:defaultValue="false"
android:icon="@drawable/icon_transfer"
android:key="usf_expose"
android:title="@string/usf_expose"
android:summary="@string/usf_expose_summary" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_edit"
android:key="usf_saf_write"
android:title="@string/usf_saf_write"
android:summary="@string/usf_saf_write_summary" />
</PreferenceCategory> </PreferenceCategory>