diff --git a/app/build.gradle b/app/build.gradle index 339466e99..83cdd996a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -187,18 +187,11 @@ dependencies { implementation 'me.zhanghai.android.appiconloader:appiconloader:1.5.0' implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'me.zhanghai.android.foregroundcompat:library:1.0.2' + implementation 'me.zhanghai.android.libarchive:library:1.0.0' implementation 'me.zhanghai.android.libselinux:library:2.1.0' implementation 'me.zhanghai.android.retrofile:library:1.1.1' implementation 'me.zhanghai.android.systemuihelper:library:1.0.0' implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.4' - // Commons Compress 1.21 requires Android 8.0. - // See also https://issues.apache.org/jira/browse/COMPRESS-611 - // Run `git revert fb1628e` if we ever created a fork that supports Android 5.0, or we raised - // minimum SDK version to Android 8.0. - //noinspection GradleDependency - implementation 'org.apache.commons:commons-compress:1.20' - // Optional dependency of Commons Compress for 7Z LZMA. - implementation 'org.tukaani:xz:1.9' implementation 'org.apache.ftpserver:ftpserver-core:1.2.0' // This is a dependency of org.apache.ftpserver:ftpserver-core but org.apache.mina:mina-core // 2.1.3+ became incompatible before API 24 due to dependency on StandardSocketOptions @@ -208,11 +201,10 @@ dependencies { strictly '2.1.3' } } - // Also a dependency of jCIFS-NG and Junrar. + // Also a dependency of jCIFS-NG. implementation 'org.slf4j:slf4j-android:1.7.36' //#ifdef NONFREE - implementation 'com.github.junrar:junrar:7.5.4' implementation platform('com.google.firebase:firebase-bom:32.2.3') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-crashlytics-ndk' diff --git a/app/src/main/java/me/zhanghai/android/files/file/MimeTypeTypeExtensions.kt b/app/src/main/java/me/zhanghai/android/files/file/MimeTypeTypeExtensions.kt index 1a89c462b..184a8b866 100644 --- a/app/src/main/java/me/zhanghai/android/files/file/MimeTypeTypeExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/file/MimeTypeTypeExtensions.kt @@ -5,8 +5,6 @@ package me.zhanghai.android.files.file -import android.os.Build - val MimeType.isApk: Boolean get() = this == MimeType.APK @@ -20,24 +18,24 @@ private val supportedArchiveMimeTypes = mutableListOf( "application/zip", "application/vnd.android.package-archive", "application/vnd.debian.binary-package", + "application/x-7z-compressed", "application/x-bzip2", + "application/x-cab", "application/x-compress", "application/x-cpio", "application/x-deb", "application/x-debian-package", "application/x-gtar", "application/x-gtar-compressed", + "application/x-iso9660-image", "application/x-java-archive", + "application/x-lha", "application/x-lzma", + "application/x-redhat-package-manager", "application/x-tar", + "application/x-ustar", "application/x-xz" -) - .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - this += "application/x-7z-compressed" - } - } - .map { it.asMimeType() }.toSet() +).map { it.asMimeType() }.toSet() val MimeType.isImage: Boolean get() = icon == MimeTypeIcon.IMAGE diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt index c5b7b2e7e..ef34013e1 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt @@ -98,11 +98,12 @@ class FileJobService : Service() { fun archive( sources: List, archiveFile: Path, - archiveType: String, - compressorType: String?, + format: Int, + filter: Int, + password: String?, context: Context ) { - startJob(ArchiveFileJob(sources, archiveFile, archiveType, compressorType), context) + startJob(ArchiveFileJob(sources, archiveFile, format, filter, password), context) } fun copy(sources: List, targetDirectory: Path, context: Context) { diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt index 72435bacb..7fb6e2344 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt @@ -582,8 +582,9 @@ private class ActionAllInfo( class ArchiveFileJob( private val sources: List, private val archiveFile: Path, - private val archiveType: String, - private val compressorType: String? + private val format: Int, + private val filter: Int, + private val password: String? ) : FileJob() { @Throws(IOException::class) override fun run() { @@ -594,16 +595,16 @@ class ArchiveFileJob( var successful = false try { channel.use { - ArchiveWriter(archiveType, compressorType, channel).use { writer -> + ArchiveWriter(channel, format, filter, password).use { writer -> val transferInfo = TransferInfo(scanInfo, archiveFile) for (source in sources) { val target = getTargetFileName(source) archiveRecursively(source, writer, target, transferInfo) throwIfInterrupted() } - successful = true } } + successful = true } finally { if (!successful) { try { diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt index a3a0f6224..15263dc32 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt @@ -23,8 +23,7 @@ import me.zhanghai.android.files.util.args import me.zhanghai.android.files.util.putArgs import me.zhanghai.android.files.util.setTextWithSelection import me.zhanghai.android.files.util.show -import org.apache.commons.compress.archivers.ArchiveStreamFactory -import org.apache.commons.compress.compressors.CompressorStreamFactory +import me.zhanghai.android.libarchive.Archive class CreateArchiveDialogFragment : FileNameDialogFragment() { private val args by args() @@ -72,24 +71,13 @@ class CreateArchiveDialogFragment : FileNameDialogFragment() { } override fun onOk(name: String) { - val archiveType: String - val compressorType: String? - when (val typeId = binding.typeGroup.checkedRadioButtonId) { - R.id.zipRadio -> { - archiveType = ArchiveStreamFactory.ZIP - compressorType = null - } - R.id.tarXzRadio -> { - archiveType = ArchiveStreamFactory.TAR - compressorType = CompressorStreamFactory.XZ - } - R.id.sevenZRadio -> { - archiveType = ArchiveStreamFactory.SEVEN_Z - compressorType = null - } + val (format, filter) = when (val typeId = binding.typeGroup.checkedRadioButtonId) { + R.id.zipRadio -> Archive.FORMAT_ZIP to Archive.FILTER_NONE + R.id.tarXzRadio -> Archive.FORMAT_TAR to Archive.FILTER_XZ + R.id.sevenZRadio -> Archive.FORMAT_7ZIP to Archive.FILTER_NONE else -> throw AssertionError(typeId) } - listener.archive(args.files, name, archiveType, compressorType) + listener.archive(args.files, name, format, filter, null) } companion object { @@ -120,6 +108,6 @@ class CreateArchiveDialogFragment : FileNameDialogFragment() { } interface Listener : FileNameDialogFragment.Listener { - fun archive(files: FileItemSet, name: String, archiveType: String, compressorType: String?) + fun archive(files: FileItemSet, name: String, format: Int, filter: Int, password: String?) } } diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index e2b29468b..2443724e4 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -911,12 +911,13 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. override fun archive( files: FileItemSet, name: String, - archiveType: String, - compressorType: String? + format: Int, + filter: Int, + password: String? ) { val archiveFile = viewModel.currentPath.resolve(name) FileJobService.archive( - makePathListForJob(files), archiveFile, archiveType, compressorType, requireContext() + makePathListForJob(files), archiveFile, format, filter, password, requireContext() ) viewModel.selectFiles(files, false) } diff --git a/app/src/main/java/me/zhanghai/android/files/nonfree/RarArchiveEntry.kt b/app/src/main/java/me/zhanghai/android/files/nonfree/RarArchiveEntry.kt deleted file mode 100644 index c2ffd4fa2..000000000 --- a/app/src/main/java/me/zhanghai/android/files/nonfree/RarArchiveEntry.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2019 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.nonfree - -import com.github.junrar.rarfile.FileHeader -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipEncoding -import java.util.Date - -class RarArchiveEntry(val header: FileHeader, zipEncoding: ZipEncoding) : ArchiveEntry { - private val name: String - - init { - @Suppress("DEPRECATION") - var name = header.fileNameW - if (name.isNullOrEmpty()) { - name = zipEncoding.decode(header.fileNameByteArray) - } - name = name.replace('\\', '/') - this.name = name - } - - override fun getName(): String = name - - override fun getSize(): Long = header.fullUnpackSize - - override fun isDirectory(): Boolean = header.isDirectory - - override fun getLastModifiedDate(): Date = header.mTime -} diff --git a/app/src/main/java/me/zhanghai/android/files/nonfree/RarChannelVolumeManager.kt b/app/src/main/java/me/zhanghai/android/files/nonfree/RarChannelVolumeManager.kt deleted file mode 100644 index e8596371f..000000000 --- a/app/src/main/java/me/zhanghai/android/files/nonfree/RarChannelVolumeManager.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.nonfree - -import com.github.junrar.Archive -import com.github.junrar.io.SeekableReadOnlyByteChannel -import com.github.junrar.volume.Volume -import com.github.junrar.volume.VolumeManager -import java8.nio.channels.SeekableByteChannel -import me.zhanghai.android.files.compat.withInitial -import java.io.EOFException -import java.io.IOException -import java.nio.ByteBuffer - -internal class RarChannelVolumeManager( - private val channel: SeekableByteChannel -) : VolumeManager { - override fun nextVolume(archive: Archive, lastVolume: Volume?): Volume? = - if (lastVolume == null) RarChannelVolume(archive, channel) else null -} - -private class RarChannelVolume( - private val archive: Archive, - private val channel: SeekableByteChannel -) : Volume { - private val delegateChannel = DelegateSeekableReadOnlyByteChannel(channel) - - @Throws(IOException::class) - override fun getChannel(): SeekableReadOnlyByteChannel = delegateChannel - - override fun getLength(): Long = - try { - channel.size() - } catch (e: IOException) { - e.printStackTrace() - 0 - } - - override fun getArchive(): Archive = archive -} - -private class DelegateSeekableReadOnlyByteChannel( - private val channel: SeekableByteChannel -) : SeekableReadOnlyByteChannel { - private val SINGLE_BYTE_BUFFER = ThreadLocal::class.withInitial { ByteBuffer.allocate(1) } - - @Throws(IOException::class) - override fun getPosition(): Long = channel.position() - - @Throws(IOException::class) - override fun setPosition(pos: Long) { - channel.position(pos) - } - - @Throws(IOException::class) - override fun read(): Int { - val buffer = SINGLE_BYTE_BUFFER.get()!! - buffer.clear() - while (true) { - when (channel.read(buffer)) { - -1 -> return -1 - 0 -> continue - else -> return buffer[0].toInt() and 0xFF - } - } - } - - @Throws(IOException::class) - override fun read(buffer: ByteArray, off: Int, count: Int): Int { - if (buffer.isEmpty()) { - return 0 - } - val byteBuffer = ByteBuffer.wrap(buffer, off, count) - while (true) { - val bytesRead = channel.read(byteBuffer) - if (bytesRead == 0) { - continue - } - return bytesRead - } - } - - @Throws(IOException::class) - override fun readFully(buffer: ByteArray, count: Int): Int { - require(count <= buffer.size) { - "count > buffer.size: count = $count, buffer.size = ${buffer.size}" - } - if (count == 0) { - return 0 - } - val byteBuffer = ByteBuffer.wrap(buffer, 0, count) - while (byteBuffer.hasRemaining()) { - if (channel.read(byteBuffer) == -1) { - throw EOFException() - } - } - return count - } - - @Throws(IOException::class) - override fun close() { - channel.close() - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/nonfree/RarFile.kt b/app/src/main/java/me/zhanghai/android/files/nonfree/RarFile.kt deleted file mode 100644 index d278c0b16..000000000 --- a/app/src/main/java/me/zhanghai/android/files/nonfree/RarFile.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2019 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.nonfree - -import com.github.junrar.Archive -import com.github.junrar.exception.RarException -import java8.nio.channels.SeekableByteChannel -import java8.nio.file.Path -import me.zhanghai.android.files.provider.archive.archiver.ArchiveException -import me.zhanghai.android.files.provider.common.newByteChannel -import org.apache.commons.compress.archivers.zip.ZipEncodingHelper -import org.apache.commons.compress.utils.IOUtils -import java.io.Closeable -import java.io.IOException -import java.io.InputStream -import java.io.PipedInputStream -import java.io.PipedOutputStream -import kotlin.math.max - -class RarFile(channel: SeekableByteChannel, encoding: String?) : Closeable { - private var archive = - try { - Archive(RarChannelVolumeManager(channel), null, null) - } catch (e: RarException) { - throw ArchiveException(e) - } - - private val zipEncoding = ZipEncodingHelper.getZipEncoding(encoding) - - @get:Throws(IOException::class) - val nextEntry: RarArchiveEntry? - get() = archive.nextFileHeader()?.let { RarArchiveEntry(it, zipEncoding) } - - @get:Throws(IOException::class) - val entries: Iterable - get() { - val entries = mutableListOf() - for (header in archive.fileHeaders) { - entries += RarArchiveEntry(header, zipEncoding) - } - return entries - } - - @Throws(IOException::class) - fun getInputStream(entry: RarArchiveEntry): InputStream { - val inputStream = PipedInputStream() - val outputStream = PipedOutputStream(inputStream) - Thread { - try { - outputStream.use { archive.extractFile(entry.header, it) } - } catch (e: IOException) { - e.printStackTrace() - } catch (e: RarException) { - e.printStackTrace() - } - }.start() - return inputStream - } - - @Throws(IOException::class) - override fun close() { - archive.close() - } - - companion object { - const val RAR = "rar" - - private val SIGNATURE_OLD = byteArrayOf(0x52, 0x45, 0x7e, 0x5e) - private val SIGNATURE_V4 = byteArrayOf(0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00) - private val SIGNATURE_V5 = byteArrayOf(0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01) - - @Throws(IOException::class) - fun detect(inputStream: InputStream): String? { - require(inputStream.markSupported()) { "InputStream.markSupported() returned false" } - val signature = ByteArray(max(SIGNATURE_OLD.size, SIGNATURE_V4.size)) - inputStream.mark(signature.size) - val signatureLength = try { - IOUtils.readFully(inputStream, signature) - } finally { - inputStream.reset() - } - return if (matches(signature, signatureLength)) RAR else null - } - - private fun matches(signature: ByteArray, length: Int): Boolean = - matches(signature, length, SIGNATURE_OLD) - || matches(signature, length, SIGNATURE_V4) - || matches(signature, length, SIGNATURE_V5) - - private fun matches(actual: ByteArray, actualLength: Int, expected: ByteArray): Boolean { - if (actualLength < expected.size) { - return false - } - for (index in expected.indices) { - if (actual[index] != expected[index]) { - return false - } - } - return true - } - - fun create(file: Path, encoding: String?): RarFile = - RarFile(file.newByteChannel(), encoding) - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt index b462a8b62..dfb161433 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt @@ -10,22 +10,14 @@ import java8.nio.file.Path import java8.nio.file.attribute.FileTime import kotlinx.parcelize.Parcelize import kotlinx.parcelize.WriteWith +import me.zhanghai.android.files.provider.archive.archiver.ReadArchive import me.zhanghai.android.files.provider.common.AbstractPosixFileAttributes import me.zhanghai.android.files.provider.common.ByteString import me.zhanghai.android.files.provider.common.FileTimeParceler -import me.zhanghai.android.files.provider.common.PosixFileMode import me.zhanghai.android.files.provider.common.PosixFileModeBit import me.zhanghai.android.files.provider.common.PosixFileType import me.zhanghai.android.files.provider.common.PosixGroup import me.zhanghai.android.files.provider.common.PosixUser -import me.zhanghai.android.files.provider.common.posixFileType -import me.zhanghai.android.files.provider.common.toByteString -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.dump.DumpArchiveEntry -import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry -import org.apache.commons.compress.archivers.tar.TarArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.threeten.bp.Instant @Parcelize internal class ArchiveFileAttributes( @@ -44,72 +36,16 @@ internal class ArchiveFileAttributes( fun entryName(): String = entryName companion object { - fun from(archiveFile: Path, entry: ArchiveEntry): ArchiveFileAttributes { - val lastModifiedTime = FileTime.from(Instant.ofEpochMilli(entry.lastModifiedDate.time)) - val lastAccessTime = when (entry) { - is DumpArchiveEntry -> FileTime.from(Instant.ofEpochMilli(entry.accessTime.time)) - is SevenZArchiveEntry -> - if (entry.hasAccessDate) { - FileTime.from(Instant.ofEpochMilli(entry.accessDate.time)) - } else { - lastModifiedTime - } - is TarArchiveEntry -> { - val atimeMillis = entry.getExtraPaxHeaderTimeMillis("atime") - if (atimeMillis != null) { - FileTime.from(Instant.ofEpochMilli(atimeMillis)) - } else { - lastModifiedTime - } - } - else -> lastModifiedTime - } - val creationTime = when (entry) { - is DumpArchiveEntry -> FileTime.from(Instant.ofEpochMilli(entry.creationTime.time)) - is SevenZArchiveEntry -> - if (entry.hasCreationDate) { - FileTime.from(Instant.ofEpochMilli(entry.creationDate.time)) - } else { - lastModifiedTime - } - is TarArchiveEntry -> { - val ctimeMillis = entry.getExtraPaxHeaderTimeMillis("ctime") - if (ctimeMillis != null) { - FileTime.from(Instant.ofEpochMilli(ctimeMillis)) - } else { - lastModifiedTime - } - } - else -> lastModifiedTime - } - val type = entry.posixFileType + fun from(archiveFile: Path, entry: ReadArchive.Entry): ArchiveFileAttributes { + val lastModifiedTime = entry.lastModifiedTime ?: FileTime.fromMillis(0) + val lastAccessTime = entry.lastAccessTime ?: lastModifiedTime + val creationTime = entry.creationTime ?: lastModifiedTime + val type = entry.type val size = entry.size val fileKey = ArchiveFileKey(archiveFile, entry.name) - val owner = when (entry) { - is DumpArchiveEntry -> PosixUser(entry.userId, null) - is TarArchiveEntry -> - @Suppress("DEPRECATION") - PosixUser(entry.userId, entry.userName?.toByteString()) - else -> null - } - val group = when (entry) { - is DumpArchiveEntry -> PosixGroup(entry.groupId, null) - is TarArchiveEntry -> - @Suppress("DEPRECATION") - PosixGroup(entry.groupId, entry.groupName?.toByteString()) - else -> null - } - val mode = when (entry) { - is DumpArchiveEntry -> PosixFileMode.fromInt(entry.mode) - is TarArchiveEntry -> PosixFileMode.fromInt(entry.mode) - is ZipArchiveEntry -> - if (entry.platform == ZipArchiveEntry.PLATFORM_UNIX) { - PosixFileMode.fromInt(entry.unixMode) - } else { - null - } - else -> null - } + val owner = entry.owner + val group = entry.group + val mode = entry.mode val seLinuxContext = null val entryName = entry.name return ArchiveFileAttributes( @@ -117,16 +53,5 @@ internal class ArchiveFileAttributes( mode, seLinuxContext, entryName ) } - - private fun TarArchiveEntry.getExtraPaxHeaderTimeMillis(name: String): Long? { - val timeString = getExtraPaxHeader(name) ?: return null - val timeSeconds = try { - timeString.toDouble() - } catch (e: NumberFormatException) { - e.printStackTrace() - return null - } - return (timeSeconds * 1000).toLong() - } } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt index b906dc50f..46252ddd1 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt @@ -8,11 +8,11 @@ package me.zhanghai.android.files.provider.archive import android.os.Parcel import android.os.Parcelable import java8.nio.file.Path +import me.zhanghai.android.files.provider.archive.archiver.ReadArchive import me.zhanghai.android.files.provider.common.ByteString import me.zhanghai.android.files.provider.common.ByteStringListPathCreator import me.zhanghai.android.files.provider.remote.RemoteFileSystemException import me.zhanghai.android.files.provider.root.RootableFileSystem -import org.apache.commons.compress.archivers.ArchiveEntry import java.io.IOException import java.io.InputStream @@ -39,7 +39,7 @@ internal class ArchiveFileSystem( get() = localFileSystem.archiveFile @Throws(IOException::class) - fun getEntryAsLocal(path: Path): ArchiveEntry = localFileSystem.getEntry(path) + fun getEntryAsLocal(path: Path): ReadArchive.Entry = localFileSystem.getEntry(path) @Throws(IOException::class) fun newInputStreamAsLocal(file: Path): InputStream = localFileSystem.newInputStream(file) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt index 0cca8a316..10180388a 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt @@ -16,11 +16,11 @@ import java8.nio.file.WatchService import java8.nio.file.attribute.UserPrincipalLookupService import java8.nio.file.spi.FileSystemProvider import me.zhanghai.android.files.provider.archive.archiver.ArchiveReader +import me.zhanghai.android.files.provider.archive.archiver.ReadArchive import me.zhanghai.android.files.provider.common.ByteString import me.zhanghai.android.files.provider.common.ByteStringBuilder import me.zhanghai.android.files.provider.common.ByteStringListPathCreator import me.zhanghai.android.files.provider.common.toByteString -import org.apache.commons.compress.archivers.ArchiveEntry import java.io.IOException import java.io.InputStream @@ -49,19 +49,19 @@ internal class LocalArchiveFileSystem( private var isRefreshNeeded = true - private var entries: Map? = null + private var entries: Map? = null private var tree: Map>? = null @Throws(IOException::class) - fun getEntry(path: Path): ArchiveEntry = + fun getEntry(path: Path): ReadArchive.Entry = synchronized(lock) { ensureEntriesLocked() getEntryLocked(path) } @Throws(IOException::class) - private fun getEntryLocked(path: Path): ArchiveEntry = + private fun getEntryLocked(path: Path): ReadArchive.Entry = synchronized(lock) { entries!![path] ?: throw NoSuchFileException(path.toString()) } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveException.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveException.kt deleted file mode 100644 index 9b5c2ca2b..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveException.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2018 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.archive.archiver - -//#ifdef NONFREE -import com.github.junrar.exception.RarException -//#endif -import org.apache.commons.compress.compressors.CompressorException -import java.io.IOException -import org.apache.commons.compress.archivers.ArchiveException as ApacheArchiveException - -class ArchiveException : IOException { - constructor(cause: ApacheArchiveException) : super(cause) - - constructor(cause: CompressorException) : super(cause) - -//#ifdef NONFREE - constructor(cause: RarException) : super(cause) -//#endif -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt index da0d63a8e..96b46e560 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt @@ -5,64 +5,38 @@ package me.zhanghai.android.files.provider.archive.archiver -import android.os.Build -import androidx.annotation.RequiresApi import androidx.preference.PreferenceManager import java8.nio.channels.SeekableByteChannel import java8.nio.charset.StandardCharsets -import java8.nio.file.AccessMode import java8.nio.file.NoSuchFileException import java8.nio.file.NotLinkException import java8.nio.file.Path import me.zhanghai.android.files.R -import me.zhanghai.android.files.compat.toJavaSeekableByteChannel -//#ifdef NONFREE -import me.zhanghai.android.files.nonfree.RarArchiveEntry -import me.zhanghai.android.files.nonfree.RarFile -//#endif import me.zhanghai.android.files.provider.common.DelegateForceableSeekableByteChannel import me.zhanghai.android.files.provider.common.DelegateInputStream import me.zhanghai.android.files.provider.common.DelegateNonForceableSeekableByteChannel import me.zhanghai.android.files.provider.common.ForceableChannel import me.zhanghai.android.files.provider.common.IsDirectoryException +import me.zhanghai.android.files.provider.common.PosixFileMode import me.zhanghai.android.files.provider.common.PosixFileType -import me.zhanghai.android.files.provider.common.checkAccess import me.zhanghai.android.files.provider.common.newByteChannel import me.zhanghai.android.files.provider.common.newInputStream -import me.zhanghai.android.files.provider.common.posixFileType -import me.zhanghai.android.files.provider.linux.isLinuxPath import me.zhanghai.android.files.provider.root.isRunningAsRoot import me.zhanghai.android.files.provider.root.rootContext import me.zhanghai.android.files.settings.Settings import me.zhanghai.android.files.util.valueCompat -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.ArchiveInputStream -import org.apache.commons.compress.archivers.ArchiveStreamFactory -import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry -import org.apache.commons.compress.archivers.sevenz.SevenZFile -import org.apache.commons.compress.archivers.tar.TarArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.compressors.CompressorException -import org.apache.commons.compress.compressors.CompressorStreamFactory -import java.io.BufferedInputStream import java.io.Closeable -import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream -import java.util.Date -import kotlin.reflect.KClass -import org.apache.commons.compress.archivers.ArchiveException as ApacheArchiveException +import java.nio.charset.Charset object ArchiveReader { - private val compressorStreamFactory = CompressorStreamFactory() - private val archiveStreamFactory = ArchiveStreamFactory() - @Throws(IOException::class) fun readEntries( file: Path, rootPath: Path - ): Pair, Map>> { - val entries = mutableMapOf() + ): Pair, Map>> { + val entries = mutableMapOf() val rawEntries = readEntries(file) for (entry in rawEntries) { var path = rootPath.resolve(entry.name) @@ -80,7 +54,7 @@ object ArchiveReader { } entries.getOrPut(path) { entry } } - entries.getOrPut(rootPath) { DirectoryArchiveEntry("") } + entries.getOrPut(rootPath) { createDirectoryEntry("") } val tree = mutableMapOf>() tree[rootPath] = mutableListOf() val paths = entries.keys.toList() @@ -96,235 +70,46 @@ object ArchiveReader { if (entries.containsKey(parentPath)) { break } - entries[parentPath] = DirectoryArchiveEntry(parentPath.toString()) + entries[parentPath] = createDirectoryEntry(parentPath.toString()) path = parentPath } } return entries to tree } + private fun createDirectoryEntry(name: String): ReadArchive.Entry { + require(!name.endsWith("/")) { "name $name should not end with a slash" } + return ReadArchive.Entry( + name, null, null, null, PosixFileType.DIRECTORY, 0, null, null, + PosixFileMode.DIRECTORY_DEFAULT, null + ) + } + @Throws(IOException::class) - private fun readEntries(file: Path): List { - val compressorType: String? - val archiveType = try { - file.newInputStream().buffered().use { inputStream -> - compressorType = try { - // inputStream must be buffered for markSupported(). - CompressorStreamFactory.detect(inputStream) - } catch (e: CompressorException) { - // Ignored. - null - } - val compressorInputStream = if (compressorType != null) { - compressorStreamFactory.createCompressorInputStream(compressorType, inputStream) - .buffered() - } else { - inputStream - } - try { - // compressorInputStream must be buffered for markSupported(). - compressorInputStream.use { detectArchiveType(it) } - } catch (e: ApacheArchiveException) { - throw ArchiveException(e) - } catch (e: CompressorException) { - throw ArchiveException(e) + private fun readEntries(file: Path): List { + val charset = archiveFileNameCharset + val (archive, closeable) = openArchive(file) + return closeable.use { + buildList { + while (true) { + this += archive.readEntry(charset) ?: break } } - } catch (e: FileNotFoundException) { - file.checkAccess(AccessMode.READ) - throw NoSuchFileException(file.toString()).apply { initCause(e) } - } - val encoding = archiveFileNameEncoding - if (compressorType == null) { - when { - archiveType == ArchiveStreamFactory.ZIP && ZipFileCompat::class.isSupported(file) -> - return ZipFileCompat::class.create(file, encoding).use { it.entries.toList() } - archiveType == ArchiveStreamFactory.SEVEN_Z -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - throw IOException(UnsupportedOperationException("SevenZFile")) - } - return SevenZFile::class.create(file).use { it.entries.toList() } - } - //#ifdef NONFREE - archiveType == RarFile.RAR -> - return RarFile.create(file, encoding).use { it.entries.toList() } - //#endif - // Unnecessary, but teaches lint that compressorType != null below might be false. - else -> {} - } - } - return try { - file.newInputStream().buffered().use { inputStream -> - val compressorInputStream = if (compressorType != null) { - compressorStreamFactory.createCompressorInputStream(compressorType, inputStream) - } else { - inputStream - } - compressorInputStream.use { - archiveStreamFactory.createArchiveInputStream( - archiveType, compressorInputStream, encoding - ).use { archiveInputStream -> - val entries = mutableListOf() - while (true) { - val entry = archiveInputStream.nextEntry ?: break - entries += entry - } - entries - } - } - } - } catch (e: FileNotFoundException) { - throw NoSuchFileException(file.toString()).apply { initCause(e) } - } catch (e: ApacheArchiveException) { - throw ArchiveException(e) - } catch (e: CompressorException) { - throw ArchiveException(e) } } - private val archiveFileNameEncoding: String - get() = - if (isRunningAsRoot) { - try { - val sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(rootContext) - val key = rootContext.getString(R.string.pref_key_archive_file_name_encoding) - val defaultValue = rootContext.getString( - R.string.pref_default_value_archive_file_name_encoding - ) - sharedPreferences.getString(key, defaultValue)!! - } catch (e: Exception) { - e.printStackTrace() - StandardCharsets.UTF_8.name() - } - } else { - Settings.ARCHIVE_FILE_NAME_ENCODING.valueCompat - } - @Throws(IOException::class) - fun newInputStream(file: Path, entry: ArchiveEntry): InputStream { + fun newInputStream(file: Path, entry: ReadArchive.Entry): InputStream { if (entry.isDirectory) { throw IsDirectoryException(file.toString()) } - val compressorType: String? - val archiveType = try { - file.newInputStream().buffered().use { inputStream -> - compressorType = try { - // inputStream must be buffered for markSupported(). - CompressorStreamFactory.detect(inputStream) - } catch (e: CompressorException) { - // Ignored. - null - } - val compressorInputStream = if (compressorType != null) { - compressorStreamFactory.createCompressorInputStream(compressorType, inputStream) - .buffered() - } else { - inputStream - } - try { - // compressorInputStream must be buffered for markSupported(). - compressorInputStream.use { detectArchiveType(it) } - } catch (e: ApacheArchiveException) { - throw ArchiveException(e) - } catch (e: CompressorException) { - throw ArchiveException(e) - } - } - } catch (e: FileNotFoundException) { - file.checkAccess(AccessMode.READ) - throw NoSuchFileException(file.toString()).apply { initCause(e) } - } - val encoding = archiveFileNameEncoding - if (compressorType == null) { - when { - entry is ZipArchiveEntry && ZipFileCompat::class.isSupported(file) -> { - var successful = false - var zipFile: ZipFileCompat? = null - var zipEntryInputStream: InputStream? = null - return try { - zipFile = ZipFileCompat::class.create(file, encoding) - zipEntryInputStream = zipFile.getInputStream(entry) - ?: throw NoSuchFileException(file.toString()) - val inputStream = CloseableInputStream(zipEntryInputStream, zipFile) - successful = true - inputStream - } finally { - if (!successful) { - zipEntryInputStream?.close() - zipFile?.close() - } - } - } - entry is SevenZArchiveEntry -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - throw IOException(UnsupportedOperationException("SevenZFile")) - } - var successful = false - var sevenZFile: SevenZFile? = null - return try { - sevenZFile = SevenZFile::class.create(file) - var inputStream: InputStream? = null - while (true) { - val currentEntry = sevenZFile.nextEntry ?: break - if (currentEntry.name != entry.name) { - continue - } - inputStream = SevenZArchiveEntryInputStream(sevenZFile, currentEntry) - successful = true - break - } - inputStream ?: throw NoSuchFileException(file.toString()) - } finally { - if (!successful) { - sevenZFile?.close() - } - } - } -//#ifdef NONFREE - entry is RarArchiveEntry -> { - var successful = false - var rarFile: RarFile? = null - return try { - rarFile = RarFile.create(file, encoding) - var inputStream: InputStream? = null - while (true) { - val currentEntry = rarFile.nextEntry ?: break - if (currentEntry.name != entry.name) { - continue - } - inputStream = rarFile.getInputStream(currentEntry) - successful = true - break - } - inputStream ?: throw NoSuchFileException(file.toString()) - } finally { - if (!successful) { - rarFile?.close() - } - } - } -//#endif - // Unnecessary, but teaches lint that compressorType != null below might be false. - else -> {} - } - } + val charset = archiveFileNameCharset + val (archive, closeable) = openArchive(file) var successful = false - var inputStream: BufferedInputStream? = null - var compressorInputStream: InputStream? = null - var archiveInputStream: ArchiveInputStream? = null return try { - inputStream = file.newInputStream().buffered() - compressorInputStream = if (compressorType != null) { - compressorStreamFactory.createCompressorInputStream(compressorType, inputStream) - } else { - inputStream - } - archiveInputStream = archiveStreamFactory.createArchiveInputStream( - archiveType, compressorInputStream, encoding - ) + var currentEntry: ReadArchive.Entry? = null while (true) { - val currentEntry = archiveInputStream.nextEntry ?: break + currentEntry = archive.readEntry(charset) ?: break if (currentEntry.name != entry.name) { continue } @@ -332,55 +117,50 @@ object ArchiveReader { break } if (successful) { - archiveInputStream + CloseableInputStream(archive.newDataInputStream(), closeable) } else { throw NoSuchFileException(file.toString()) } - } catch (e: FileNotFoundException) { - throw NoSuchFileException(file.toString()).apply { initCause(e) } - } catch (e: ApacheArchiveException) { - throw ArchiveException(e) - } catch (e: CompressorException) { - throw ArchiveException(e) } finally { if (!successful) { - archiveInputStream?.close() - compressorInputStream?.close() - inputStream?.close() + closeable.close() } } } - @Throws(ApacheArchiveException::class) - private fun detectArchiveType(inputStream: InputStream): String = -//#ifdef NONFREE + private fun openArchive(file: Path): Pair { + val channel = try { + CacheSizeSeekableByteChannel(file.newByteChannel()) + } catch (e: Exception) { + e.printStackTrace() + null + } + if (channel != null) { + var successful = false + try { + val archive = ReadArchive(channel) + successful = true + return archive to ArchiveCloseable(archive, channel) + } finally { + if (!successful) { + channel.close() + } + } + } + val inputStream = file.newInputStream() + var successful = false try { - RarFile.detect(inputStream) - } catch (e: IOException) { - throw ApacheArchiveException("RarFile.detect()", e) - } ?: -//#endif - ArchiveStreamFactory.detect(inputStream) - - private fun KClass.isSupported(file: Path): Boolean = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || file.isLinuxPath - - private fun KClass.create(file: Path, encoding: String?): ZipFileCompat = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ZipFileCompat( - CacheSizeSeekableByteChannel(file.newByteChannel()).toJavaSeekableByteChannel(), - encoding - ) - } else { - ZipFileCompat(file.toFile()) + val archive = ReadArchive(inputStream) + successful = true + return archive to ArchiveCloseable(archive, inputStream) + } finally { + if (!successful) { + inputStream.close() + } } + } - @RequiresApi(Build.VERSION_CODES.N) - private fun KClass.create(file: Path): SevenZFile = - SevenZFile(CacheSizeSeekableByteChannel(file.newByteChannel()).toJavaSeekableByteChannel()) - - // ZipFileCompat and SevenZFile call size() repeatedly, especially ZipFile.skipBytes(), so make - // it cached to improve performance. + // size() may be called repeatedly for ZIP and 7Z, so make it cached to improve performance. private fun CacheSizeSeekableByteChannel(channel: SeekableByteChannel): SeekableByteChannel = if (channel is ForceableChannel) { CacheSizeForceableSeekableByteChannel(channel) @@ -404,48 +184,37 @@ object ArchiveReader { override fun size(): Long = size } - @Throws(IOException::class) - fun readSymbolicLink(file: Path, entry: ArchiveEntry): String { - if (!isSymbolicLink(entry)) { - throw NotLinkException(file.toString()) - } - return if (entry is TarArchiveEntry) { - entry.linkName - } else { - newInputStream(file, entry).use { it.reader(StandardCharsets.UTF_8).readText() } - } - } - - private fun isSymbolicLink(entry: ArchiveEntry): Boolean = - entry.posixFileType == PosixFileType.SYMBOLIC_LINK - - private class DirectoryArchiveEntry(name: String) : ArchiveEntry { - init { - require(!name.endsWith("/")) { "name $name should not end with a slash" } - } - - private val name = "$name/" - - override fun getName(): String = name - - override fun getSize(): Long = 0 - - override fun isDirectory(): Boolean = true - - override fun getLastModifiedDate(): Date = Date(-1) - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true + private val archiveFileNameCharset: Charset + get() = + if (isRunningAsRoot) { + try { + val sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(rootContext) + val key = rootContext.getString(R.string.pref_key_archive_file_name_encoding) + val defaultValue = rootContext.getString( + R.string.pref_default_value_archive_file_name_encoding + ) + Charset.forName(sharedPreferences.getString(key, defaultValue)!!) + } catch (e: Exception) { + e.printStackTrace() + StandardCharsets.UTF_8 + } + } else { + Charset.forName(Settings.ARCHIVE_FILE_NAME_ENCODING.valueCompat) } - if (javaClass != other?.javaClass) { - return false + + private class ArchiveCloseable( + private val archive: ReadArchive, + private val closeable: Closeable + ) : Closeable { + override fun close() { + @Suppress("ConvertTryFinallyToUseCall") + try { + archive.close() + } finally { + closeable.close() } - other as DirectoryArchiveEntry - return name == other.name } - - override fun hashCode(): Int = name.hashCode() } private class CloseableInputStream( @@ -460,30 +229,11 @@ object ArchiveReader { } } - private class SevenZArchiveEntryInputStream( - private val file: SevenZFile, - private val entry: SevenZArchiveEntry - ) : InputStream() { - override fun available(): Int { - val size = entry.size - val read = file.statisticsForCurrentEntry - .uncompressedCount - val available = size - read - return available.coerceAtMost(Int.MAX_VALUE.toLong()).toInt() - } - - @Throws(IOException::class) - override fun read(): Int = file.read() - - @Throws(IOException::class) - override fun read(b: ByteArray): Int = file.read(b) - - @Throws(IOException::class) - override fun read(b: ByteArray, off: Int, len: Int): Int = file.read(b, off, len) - - @Throws(IOException::class) - override fun close() { - file.close() + @Throws(IOException::class) + fun readSymbolicLink(file: Path, entry: ReadArchive.Entry): String { + if (entry.type != PosixFileType.SYMBOLIC_LINK) { + throw NotLinkException(file.toString()) } + return entry.symbolicLinkTarget ?: "" } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveWriter.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveWriter.kt index f8a1e2958..3524f897b 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveWriter.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveWriter.kt @@ -5,224 +5,74 @@ package me.zhanghai.android.files.provider.archive.archiver -import android.os.Build import java8.nio.channels.SeekableByteChannel import java8.nio.file.LinkOption import java8.nio.file.Path import java8.nio.file.attribute.BasicFileAttributes -import me.zhanghai.android.files.compat.toJavaSeekableByteChannel import me.zhanghai.android.files.provider.common.PosixFileAttributes +import me.zhanghai.android.files.provider.common.PosixFileMode +import me.zhanghai.android.files.provider.common.PosixFileType import me.zhanghai.android.files.provider.common.copyTo import me.zhanghai.android.files.provider.common.getLastModifiedTime -import me.zhanghai.android.files.provider.common.isDirectory -import me.zhanghai.android.files.provider.common.isRegularFile import me.zhanghai.android.files.provider.common.newInputStream -import me.zhanghai.android.files.provider.common.newOutputStream import me.zhanghai.android.files.provider.common.readAttributes import me.zhanghai.android.files.provider.common.readSymbolicLinkByteString import me.zhanghai.android.files.provider.common.size -import me.zhanghai.android.files.provider.common.toInt -import me.zhanghai.android.files.util.lazyReflectedField -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.ArchiveOutputStream -import org.apache.commons.compress.archivers.ArchiveStreamFactory -import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile -import org.apache.commons.compress.archivers.tar.TarArchiveEntry -import org.apache.commons.compress.archivers.tar.TarConstants -import org.apache.commons.compress.archivers.zip.UnixStat -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.compressors.CompressorException -import org.apache.commons.compress.compressors.CompressorStreamFactory import java.io.Closeable -import java.io.File import java.io.IOException -import java.io.OutputStream -class ArchiveWriter( - archiveType: String, - compressorType: String?, - channel: SeekableByteChannel +class ArchiveWriter @Throws(IOException::class) constructor( + channel: SeekableByteChannel, + format: Int, + filter: Int, + password: String? ) : Closeable { - private val archiveOutputStream: ArchiveOutputStream - - init { - when (archiveType) { - ArchiveStreamFactory.SEVEN_Z -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - throw IOException(UnsupportedOperationException("SevenZOutputFile")) - } - archiveOutputStream = SevenZArchiveOutputStream( - SevenZOutputFile(channel.toJavaSeekableByteChannel()) - ) - } - else -> { - var successful = false - var outputStream: OutputStream? = null - var compressorOutputStream: OutputStream? = null - try { - outputStream = channel.newOutputStream().buffered() - compressorOutputStream = if (compressorType != null) { - compressorStreamFactory.createCompressorOutputStream( - compressorType, outputStream - ) - } else { - outputStream - } - // Use the platform default encoding (which is UTF-8) instead of the user-set - // one, because that one is for reading archives instead of creating. - archiveOutputStream = archiveStreamFactory.createArchiveOutputStream( - archiveType, compressorOutputStream - ) - successful = true - } catch (e: org.apache.commons.compress.archivers.ArchiveException) { - throw ArchiveException(e) - } catch (e: CompressorException) { - throw ArchiveException(e) - } finally { - if (!successful) { - compressorOutputStream?.close() - outputStream?.close() - } - } - } - } - } + private val archive = WriteArchive(channel, format, filter, password) @Throws(IOException::class) fun write(file: Path, entryName: Path, intervalMillis: Long, listener: ((Long) -> Unit)?) { - val entry = archiveOutputStream.createArchiveEntry(PathFile(file), entryName.toString()) + val name = entryName.toString() + val lastModifiedTime = file.getLastModifiedTime(LinkOption.NOFOLLOW_LINKS) + val lastAccessTime = null + val creationTime = null val attributes = file.readAttributes( BasicFileAttributes::class.java, LinkOption.NOFOLLOW_LINKS ) - val writeData = when { - attributes.isRegularFile -> true - attributes.isDirectory -> false - attributes.isSymbolicLink -> - when (entry) { - is ZipArchiveEntry -> { - entry.unixMode = UnixStat.LINK_FLAG or UnixStat.DEFAULT_LINK_PERM - true - } - is TarArchiveEntry -> { - tarArchiveEntryLinkFlagsField.setByte(entry, TarConstants.LF_SYMLINK) - entry.linkName = file.readSymbolicLinkByteString().toString() - false - } - else -> throw IOException(UnsupportedOperationException("symbolic link")) - } - else -> throw IOException(UnsupportedOperationException("type")) + val type = when { + attributes is PosixFileAttributes -> attributes.type() + attributes.isDirectory -> PosixFileType.DIRECTORY + attributes.isSymbolicLink -> PosixFileType.SYMBOLIC_LINK + else -> PosixFileType.REGULAR_FILE } - if (entry is TarArchiveEntry && attributes is PosixFileAttributes) { - attributes.mode()?.let { entry.mode = it.toInt() } - val owner = attributes.owner() - if (owner != null) { - entry.userId = owner.id - owner.name?.let { entry.userName = it } - } - val group = attributes.group() - if (group != null) { - entry.groupId = group.id - group.name?.let { entry.groupName = it } - } + val size = file.size(LinkOption.NOFOLLOW_LINKS) + val posixAttributes = attributes as? PosixFileAttributes + val owner = posixAttributes?.owner() + val group = posixAttributes?.group() + val mode = posixAttributes?.mode() ?: when { + attributes.isDirectory -> PosixFileMode.DIRECTORY_DEFAULT + attributes.isSymbolicLink -> PosixFileMode.SYMBOLIC_LINK_DEFAULT + else -> PosixFileMode.FILE_DEFAULT } - archiveOutputStream.putArchiveEntry(entry) - var isListenerNotified = false - if (writeData) { - if (attributes.isSymbolicLink) { - val target = file.readSymbolicLinkByteString().borrowBytes() - archiveOutputStream.write(target) - } else { - file.newInputStream(LinkOption.NOFOLLOW_LINKS).use { inputStream -> - inputStream.copyTo(archiveOutputStream, intervalMillis, listener) - } - isListenerNotified = true - } + val symbolicLinkTarget = if (attributes.isSymbolicLink) { + file.readSymbolicLinkByteString().toString() + } else { + null } - archiveOutputStream.closeArchiveEntry() - if (!isListenerNotified) { + archive.Entry( + name, lastModifiedTime, lastAccessTime, creationTime, type, size, owner, group, mode, + symbolicLinkTarget + ).use { archive.writeEntry(it) } + if (type == PosixFileType.REGULAR_FILE) { + file.newInputStream(LinkOption.NOFOLLOW_LINKS).use { inputStream -> + inputStream.copyTo(archive.newDataOutputStream(), intervalMillis, listener) + } + } else { listener?.invoke(attributes.size()) } } @Throws(IOException::class) override fun close() { - archiveOutputStream.finish() - archiveOutputStream.close() - } - - private class SevenZArchiveOutputStream( - private val file: SevenZOutputFile - ) : ArchiveOutputStream() { - @Throws(IOException::class) - override fun createArchiveEntry(file: File, entryName: String): ArchiveEntry = - this.file.createArchiveEntry(file, entryName) - - @Throws(IOException::class) - override fun putArchiveEntry(entry: ArchiveEntry) { - file.putArchiveEntry(entry) - } - - @Throws(IOException::class) - override fun write(b: Int) { - file.write(b) - } - - @Throws(IOException::class) - override fun write(b: ByteArray) { - file.write(b) - } - - @Throws(IOException::class) - override fun write(b: ByteArray, off: Int, len: Int) { - file.write(b, off, len) - } - - @Throws(IOException::class) - override fun closeArchiveEntry() { - file.closeArchiveEntry() - } - - @Throws(IOException::class) - override fun finish() { - file.finish() - } - - @Throws(IOException::class) - override fun close() { - file.close() - } - } - - // {@link ArchiveOutputStream#createArchiveEntry(File, String)} doesn't actually need a real - // file. - private class PathFile(private val path: Path) : File(path.toString()) { - override fun isDirectory(): Boolean = path.isDirectory(LinkOption.NOFOLLOW_LINKS) - - override fun isFile(): Boolean = path.isRegularFile(LinkOption.NOFOLLOW_LINKS) - - override fun lastModified(): Long = - try { - path.getLastModifiedTime(LinkOption.NOFOLLOW_LINKS).toMillis() - } catch (e: IOException) { - e.printStackTrace() - 0 - } - - override fun length(): Long = - try { - path.size(LinkOption.NOFOLLOW_LINKS) - } catch (e: IOException) { - e.printStackTrace() - 0 - } - } - - companion object { - private val compressorStreamFactory = CompressorStreamFactory() - private val archiveStreamFactory = ArchiveStreamFactory() - - private val tarArchiveEntryLinkFlagsField by lazyReflectedField( - TarArchiveEntry::class.java, "linkFlag" - ) + archive.close() } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt new file mode 100644 index 000000000..a6aa0d924 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.provider.archive.archiver + +import android.system.OsConstants +import java8.nio.channels.SeekableByteChannel +import java8.nio.charset.StandardCharsets +import java8.nio.file.attribute.FileTime +import me.zhanghai.android.files.provider.common.PosixFileMode +import me.zhanghai.android.files.provider.common.PosixFileModeBit +import me.zhanghai.android.files.provider.common.PosixFileType +import me.zhanghai.android.files.provider.common.PosixGroup +import me.zhanghai.android.files.provider.common.PosixUser +import me.zhanghai.android.files.provider.common.toByteString +import me.zhanghai.android.libarchive.Archive +import me.zhanghai.android.libarchive.ArchiveEntry +import me.zhanghai.android.libarchive.ArchiveException +import org.threeten.bp.Instant +import java.io.Closeable +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.charset.Charset + +class ReadArchive : Closeable { + private val archive = Archive.readNew() + + @Throws(ArchiveException::class) + constructor(inputStream: InputStream) { + var successful = false + try { + Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) + Archive.readSupportFilterAll(archive) + Archive.readSupportFormatAll(archive) + Archive.readSetCallbackData(archive, null) + val buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE) + Archive.readSetReadCallback(archive) { _, _ -> + buffer.clear() + val bytesRead = try { + inputStream.read(buffer.array()) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "InputStream.read", e) + } + if (bytesRead != -1) { + buffer.limit(bytesRead) + buffer + } else { + null + } + } + Archive.readSetSkipCallback(archive) { _, _, request -> + try { + inputStream.skip(request) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "InputStream.skip", e) + } + } + Archive.readOpen1(archive) + successful = true + } finally { + if (!successful) { + close() + } + } + } + + @Throws(ArchiveException::class) + constructor(channel: SeekableByteChannel) { + var successful = false + try { + Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) + Archive.readSupportFilterAll(archive) + Archive.readSupportFormatAll(archive) + Archive.readSetCallbackData(archive, null) + val buffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE) + Archive.readSetReadCallback(archive) { _, _ -> + buffer.clear() + val bytesRead = try { + channel.read(buffer) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.read", e) + } + if (bytesRead != -1) { + buffer.flip() + buffer + } else { + null + } + } + Archive.readSetSkipCallback(archive) { _, _, request -> + try { + channel.position(channel.position() + request) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.position", e) + } + request + } + Archive.readSetSeekCallback(archive) { _, _, offset, whence -> + val newPosition: Long + try { + newPosition = when (whence) { + OsConstants.SEEK_SET -> offset + OsConstants.SEEK_CUR -> channel.position() + offset + OsConstants.SEEK_END -> channel.size() + offset + else -> throw ArchiveException( + Archive.ERRNO_FATAL, + "Unknown whence $whence" + ) + } + channel.position(newPosition) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.position", e) + } + newPosition + } + Archive.readOpen1(archive) + successful = true + } finally { + if (!successful) { + close() + } + } + } + + @Throws(ArchiveException::class) + fun readEntry(charset: Charset): Entry? { + val entry = Archive.readNextHeader(archive) + if (entry == 0L) { + return null + } + val name = + getEntryString(ArchiveEntry.pathnameUtf8(entry), ArchiveEntry.pathname(entry), charset) + ?: throw ArchiveException( + Archive.ERRNO_FATAL, "pathname == null && pathnameUtf8 == null" + ) + val stat = ArchiveEntry.stat(entry) + val lastModifiedTime = if (ArchiveEntry.mtimeIsSet(entry)) { + FileTime.from( + Instant.ofEpochSecond(stat.stMtim.tvSec, stat.stMtim.tvNsec) + ) + } else { + null + } + val lastAccessTime = if (ArchiveEntry.atimeIsSet(entry)) { + FileTime.from( + Instant.ofEpochSecond(stat.stAtim.tvSec, stat.stAtim.tvNsec) + ) + } else { + null + } + val creationTime = if (ArchiveEntry.birthtimeIsSet(entry)) { + FileTime.from( + Instant.ofEpochSecond( + ArchiveEntry.birthtime(entry), ArchiveEntry.birthtimeNsec(entry) + ) + ) + } else { + null + } + val type = PosixFileType.fromMode(stat.stMode) + val size = stat.stSize + // TODO: There's no way to know if UID/GID is unset or root. + val owner = PosixUser( + stat.stUid, getEntryString( + ArchiveEntry.unameUtf8(entry), ArchiveEntry.uname(entry), charset + )?.toByteString() + ) + val group = PosixGroup( + stat.stGid, getEntryString( + ArchiveEntry.gnameUtf8(entry), ArchiveEntry.gname(entry), charset + )?.toByteString() + ) + val mode = PosixFileMode.fromInt(stat.stMode) + val symbolicLinkTarget = + getEntryString(ArchiveEntry.symlinkUtf8(entry), ArchiveEntry.symlink(entry), charset) + return Entry( + name, lastModifiedTime, lastAccessTime, creationTime, type, size, owner, group, mode, + symbolicLinkTarget + ) + } + + private fun getEntryString(stringUtf8: String?, string: ByteArray?, charset: Charset): String? = + stringUtf8 ?: string?.toString(charset) + + fun hasEncryptedEntries(): Boolean? { + val hasEncryptedEntries = Archive.readHasEncryptedEntries(archive) + return when { + hasEncryptedEntries > 0 -> true + hasEncryptedEntries == 0 -> false + else -> null + } + } + + @Throws(ArchiveException::class) + fun newDataInputStream(): InputStream = DataInputStream() + + @Throws(ArchiveException::class) + fun addPassword(password: String) { + Archive.readAddPassphrase(archive, password.toByteArray()) + } + + @Throws(ArchiveException::class) + override fun close() { + Archive.readFree(archive) + } + + class Entry( + val name: String, + val lastModifiedTime: FileTime?, + val lastAccessTime: FileTime?, + val creationTime: FileTime?, + val type: PosixFileType, + val size: Long, + val owner: PosixUser?, + val group: PosixGroup?, + val mode: Set, + val symbolicLinkTarget: String? + ) { + val isDirectory: Boolean + get() = type == PosixFileType.DIRECTORY + } + + private inner class DataInputStream : InputStream() { + private val oneByteBuffer = ByteBuffer.allocateDirect(1) + + @Throws(IOException::class) + override fun read(): Int { + read(oneByteBuffer) + return if (oneByteBuffer.hasRemaining()) oneByteBuffer.get().toUByte().toInt() else -1 + } + + @Throws(IOException::class) + override fun read(b: ByteArray, off: Int, len: Int): Int { + val buffer = ByteBuffer.wrap(b, off, len) + read(buffer) + return if (buffer.hasRemaining()) buffer.remaining() else -1 + } + + @Throws(IOException::class) + private fun read(buffer: ByteBuffer) { + buffer.clear() + Archive.readData(archive, buffer) + buffer.flip() + } + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt new file mode 100644 index 000000000..42781ef8a --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.provider.archive.archiver + +import java8.nio.channels.SeekableByteChannel +import java8.nio.charset.StandardCharsets +import java8.nio.file.attribute.FileTime +import me.zhanghai.android.files.provider.common.PosixFileModeBit +import me.zhanghai.android.files.provider.common.PosixFileType +import me.zhanghai.android.files.provider.common.PosixGroup +import me.zhanghai.android.files.provider.common.PosixUser +import me.zhanghai.android.files.provider.common.toInt +import me.zhanghai.android.libarchive.Archive +import me.zhanghai.android.libarchive.ArchiveEntry +import me.zhanghai.android.libarchive.ArchiveException +import java.io.Closeable +import java.io.IOException +import java.io.OutputStream +import java.nio.ByteBuffer + +class WriteArchive @Throws(ArchiveException::class) constructor( + channel: SeekableByteChannel, + format: Int, + filter: Int, + password: String? +) : Closeable { + private val archive = Archive.writeNew() + + init { + var successful = false + try { + Archive.writeSetBytesPerBlock(archive, DEFAULT_BUFFER_SIZE) + Archive.writeSetBytesInLastBlock(archive, 1) + Archive.writeSetFormat(archive, format) + Archive.writeAddFilter(archive, filter) + if (password != null) { + Archive.writeSetPassphrase(archive, password.toByteArray()) + } + Archive.writeOpen( + archive, null, null, { _, _, buffer -> channel.write(buffer) }, null + ) + successful = true + } finally { + if (!successful) { + close() + } + } + } + + @Throws(ArchiveException::class) + fun writeEntry(entry: Entry) { + Archive.writeHeader(archive, entry.entry) + } + + @Throws(ArchiveException::class) + fun newDataOutputStream(): OutputStream = DataOutputStream() + + @Throws(ArchiveException::class) + override fun close() { + Archive.writeFree(archive) + } + + inner class Entry( + name: String, + lastModifiedTime: FileTime?, + lastAccessTime: FileTime?, + creationTime: FileTime?, + type: PosixFileType, + size: Long, + owner: PosixUser?, + group: PosixGroup?, + mode: Set, + symbolicLinkTarget: String? + ) : Closeable { + internal val entry = ArchiveEntry.new2(archive) + + init { + Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) + ArchiveEntry.setPathname(entry, name.toByteArray()) + if (lastModifiedTime != null) { + val lastModifiedTimeInstant = lastModifiedTime.toInstant() + ArchiveEntry.setMtime( + entry, lastModifiedTimeInstant.epochSecond, + lastModifiedTimeInstant.nano.toLong() + ) + } + if (lastAccessTime != null) { + val lastAccessTimeInstant = lastAccessTime.toInstant() + ArchiveEntry.setAtime( + entry, lastAccessTimeInstant.epochSecond, lastAccessTimeInstant.nano.toLong() + ) + } + if (creationTime != null) { + val creationTimeInstant = creationTime.toInstant() + ArchiveEntry.setBirthtime( + entry, creationTimeInstant.epochSecond, creationTimeInstant.nano.toLong() + ) + } + ArchiveEntry.setFiletype(entry, type.mode) + ArchiveEntry.setSize(entry, size) + if (owner != null) { + ArchiveEntry.setUid(entry, owner.id.toLong()) + val ownerName = owner.name + if (ownerName != null) { + ArchiveEntry.setUname(entry, ownerName.toByteArray()) + } + } + if (group != null) { + ArchiveEntry.setGid(entry, group.id.toLong()) + val groupName = group.name + if (groupName != null) { + ArchiveEntry.setGname(entry, groupName.toByteArray()) + } + } + ArchiveEntry.setPerm(entry, mode.toInt()) + if (symbolicLinkTarget != null) { + ArchiveEntry.setSymlink(entry, symbolicLinkTarget.toByteArray()) + } + } + + override fun close() { + ArchiveEntry.free(entry) + } + } + + private inner class DataOutputStream : OutputStream() { + private val oneByteBuffer = ByteBuffer.allocateDirect(1) + + @Throws(IOException::class) + override fun write(b: Int) { + oneByteBuffer.clear() + oneByteBuffer.put(b.toByte()) + Archive.writeData(archive, oneByteBuffer) + } + + @Throws(IOException::class) + override fun write(b: ByteArray, off: Int, len: Int) { + val buffer = ByteBuffer.wrap(b, off, len) + while (buffer.hasRemaining()) { + Archive.writeData(archive, buffer) + } + } + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ZipFileCompat.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ZipFileCompat.kt deleted file mode 100644 index 45265ec7e..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ZipFileCompat.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2018 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.archive.archiver - -import android.annotation.SuppressLint -import android.os.Build -import androidx.annotation.RequiresApi -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipFile -import java.io.Closeable -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.nio.channels.SeekableByteChannel -import java.util.Enumeration -import java.util.zip.ZipEntry -import java.util.zip.ZipException -import java.util.zip.ZipFile as JavaZipFile - -internal class ZipFileCompat : Closeable { - @RequiresApi(Build.VERSION_CODES.N) - private val zipFile: ZipFile? - private val javaZipFile: JavaZipFile? - - @RequiresApi(Build.VERSION_CODES.N) - constructor(channel: SeekableByteChannel, encoding: String?) { - zipFile = ZipFile(channel, encoding) - javaZipFile = null - } - - constructor(file: File) { - @SuppressLint("NewApi") - zipFile = null - javaZipFile = JavaZipFile(file) - } - - val entries: Enumeration - get() = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - zipFile!!.entries - } else { - val entries = javaZipFile!!.entries() - object : Enumeration { - override fun hasMoreElements(): Boolean = entries.hasMoreElements() - - override fun nextElement(): ZipArchiveEntry { - val entry = entries.nextElement() - return try { - ZipArchiveEntry(entry) - } catch (e: ZipException) { - e.printStackTrace() - UnparseableExtraZipArchiveEntry(entry) - } - } - } - } - - @Throws(IOException::class) - fun getInputStream(entry: ZipArchiveEntry): InputStream? = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - zipFile!!.getInputStream(entry) - } else { - javaZipFile!!.getInputStream(entry) - } - - @Throws(IOException::class) - override fun close() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - zipFile!!.close() - } else { - javaZipFile!!.close() - } - } - - private class UnparseableExtraZipArchiveEntry(entry: ZipEntry) : ZipArchiveEntry(entry.name) { - init { - time = entry.time - setExtra() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - lastModifiedTime = entry.lastModifiedTime - lastAccessTime = entry.lastAccessTime - creationTime = entry.creationTime - } - val crc = entry.crc - if (crc in 0..0xFFFFFFFFL) { - setCrc(entry.crc) - } - val size = entry.size - if (size >= 0) { - setSize(size) - } - compressedSize = entry.compressedSize - method = entry.method - comment = entry.comment - } - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileMode.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileMode.kt index b4c4b8434..bbf3e2c1d 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileMode.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileMode.kt @@ -27,12 +27,25 @@ enum class PosixFileModeBit { } object PosixFileMode { + val CREATE_DIRECTORY_DEFAULT = fromInt( + OsConstants.S_IRWXU or OsConstants.S_IRWXG or OsConstants.S_IRWXO + ) + val CREATE_FILE_DEFAULT = fromInt( - OsConstants.S_IRUSR or OsConstants.S_IWUSR or OsConstants.S_IRGRP or OsConstants.S_IWGRP - or OsConstants.S_IROTH or OsConstants.S_IWOTH + OsConstants.S_IRUSR or OsConstants.S_IWUSR or OsConstants.S_IRGRP or OsConstants.S_IWGRP or + OsConstants.S_IROTH or OsConstants.S_IWOTH ) - val CREATE_DIRECTORY_DEFAULT = fromInt( + val DIRECTORY_DEFAULT = fromInt( + OsConstants.S_IRWXU or OsConstants.S_IRGRP or OsConstants.S_IXGRP or + OsConstants.S_IROTH or OsConstants.S_IXOTH + ) + + val FILE_DEFAULT = fromInt( + OsConstants.S_IRUSR or OsConstants.S_IWUSR or OsConstants.S_IRGRP or OsConstants.S_IROTH + ) + + val SYMBOLIC_LINK_DEFAULT = fromInt( OsConstants.S_IRWXU or OsConstants.S_IRWXG or OsConstants.S_IRWXO ) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileType.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileType.kt index eec607f96..faf405c27 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileType.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileType.kt @@ -7,21 +7,17 @@ package me.zhanghai.android.files.provider.common import android.system.OsConstants import java8.nio.file.attribute.BasicFileAttributes -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.dump.DumpArchiveEntry -import org.apache.commons.compress.archivers.tar.TarArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry // https://www.gnu.org/software/libc/manual/html_node/Testing-File-Type.html -enum class PosixFileType { - UNKNOWN, - DIRECTORY, - CHARACTER_DEVICE, - BLOCK_DEVICE, - REGULAR_FILE, - FIFO, - SYMBOLIC_LINK, - SOCKET; +enum class PosixFileType(val mode: Int) { + UNKNOWN(0), + DIRECTORY(OsConstants.S_IFDIR), + CHARACTER_DEVICE(OsConstants.S_IFCHR), + BLOCK_DEVICE(OsConstants.S_IFBLK), + REGULAR_FILE(OsConstants.S_IFREG), + FIFO(OsConstants.S_IFIFO), + SYMBOLIC_LINK(OsConstants.S_IFLNK), + SOCKET((OsConstants.S_IFSOCK)); companion object { fun fromMode(mode: Int): PosixFileType = @@ -38,49 +34,6 @@ enum class PosixFileType { } } -val ArchiveEntry.posixFileType: PosixFileType - get() = - when (this) { - is DumpArchiveEntry -> posixFileType - is TarArchiveEntry -> posixFileType - is ZipArchiveEntry -> posixFileType - else -> if (isDirectory) PosixFileType.DIRECTORY else PosixFileType.REGULAR_FILE - } - -private val DumpArchiveEntry.posixFileType: PosixFileType - get() = - when (type) { - DumpArchiveEntry.TYPE.SOCKET -> PosixFileType.SOCKET - DumpArchiveEntry.TYPE.LINK -> PosixFileType.SYMBOLIC_LINK - DumpArchiveEntry.TYPE.FILE -> PosixFileType.REGULAR_FILE - DumpArchiveEntry.TYPE.BLKDEV -> PosixFileType.BLOCK_DEVICE - DumpArchiveEntry.TYPE.DIRECTORY -> PosixFileType.DIRECTORY - DumpArchiveEntry.TYPE.CHRDEV -> PosixFileType.CHARACTER_DEVICE - DumpArchiveEntry.TYPE.FIFO -> PosixFileType.FIFO - DumpArchiveEntry.TYPE.WHITEOUT, DumpArchiveEntry.TYPE.UNKNOWN -> PosixFileType.UNKNOWN - else -> PosixFileType.UNKNOWN - } - -private val TarArchiveEntry.posixFileType: PosixFileType - get() = - when { - isDirectory -> PosixFileType.DIRECTORY - isFile -> PosixFileType.REGULAR_FILE - isSymbolicLink -> PosixFileType.SYMBOLIC_LINK - isCharacterDevice -> PosixFileType.CHARACTER_DEVICE - isBlockDevice -> PosixFileType.BLOCK_DEVICE - isFIFO -> PosixFileType.FIFO - else -> PosixFileType.UNKNOWN - } - -private val ZipArchiveEntry.posixFileType: PosixFileType - get() = - when { - isDirectory -> PosixFileType.DIRECTORY - isUnixSymlink -> PosixFileType.SYMBOLIC_LINK - else -> PosixFileType.REGULAR_FILE - } - val BasicFileAttributes.posixFileType: PosixFileType get() = when (this) { diff --git a/app/src/main/res/raw/licenses.xml b/app/src/main/res/raw/licenses.xml index fbbe26496..85d178dd0 100644 --- a/app/src/main/res/raw/licenses.xml +++ b/app/src/main/res/raw/licenses.xml @@ -154,6 +154,48 @@ Apache Software License 2.0 + + libarchive-android + https://github.com/zhanghai/libarchive-android + Copyright 2023 Google LLC + Apache Software License 2.0 + + + + libarchive + https://github.com/libarchive/libarchive + Copyright 2003 Tim Kientzle + BSD 2-Clause License + + + + bzip2 + https://gitlab.com/bzip2/bzip2 + Copyright 1996 Julian Seward + BSD 3-Clause License + + + + lz4 + https://github.com/lz4/lz4 + Copyright 2011 Yann Collet + BSD 2-Clause License + + + + zstd + https://github.com/facebook/zstd + Copyright Meta Platforms, Inc. and affiliates + BSD 3-Clause License + + + + mbedtls + https://github.com/Mbed-TLS/mbedtls + Copyright The Mbed TLS Contributors + Apache Software License 2.0 + + libselinux-android https://github.com/zhanghai/libselinux-android @@ -182,13 +224,6 @@ GNU General Public License 2.0 - - Apache Commons Compress - https://commons.apache.org/proper/commons-compress/ - Copyright 2002 The Apache Software Foundation - Apache Software License 2.0 - - Apache FtpServer https://mina.apache.org/ftpserver-project/index.html