Java tutorial
/* * Copyright 2017 Alexander Rvachev * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.xfd.provider; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Process; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.LruCache; import android.support.v4.util.Pools; import android.text.TextUtils; import android.webkit.MimeTypeMap; import com.carrotsearch.hppc.ObjectHashSet; import com.carrotsearch.hppc.ObjectSet; import net.sf.xfd.DirFd; import net.sf.xfd.Fd; import net.sf.xfd.FsType; import net.sf.xfd.Interruption; import net.sf.xfd.LogUtil; import net.sf.xfd.MountInfo; import net.sf.xfd.NativeBits; import net.sf.xfd.OS; import net.sf.xfd.Stat; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.concurrent.locks.Lock; import static android.provider.DocumentsContract.Document.MIME_TYPE_DIR; public final class ProviderBase extends ContextWrapper { public static final String DEFAULT_MIME = "application/octet-stream"; private static final String ALT_DIR_MIME = "inode/directory"; private static final String FIFO_MIME = "inode/fifo"; private static final String CHAR_DEV_MIME = "inode/chardevice"; private static final String SOCK_MIME = "inode/socket"; private static final String LINK_MIME = "inode/symlink"; private static final String BLOCK_MIME = "inode/blockdevice"; private static final String EMPTY_MIME = "application/x-empty"; private static final int MY_PID = Process.myPid(); public static int myPid() { return MY_PID; } private static final LruCache<String, TimestampedMime> fileTypeCache = new LruCache<>(7); private final String authority; private volatile MountInfo mounts; private volatile OS rooted; private volatile Magic magic; ProviderBase(Context context, String authority) throws IOException { super(context); this.authority = authority; this.magic = Magic.getInstance(getBaseContext()); } @Nullable OS getOS() { if (rooted == null) { synchronized (this) { if (rooted == null) { try { reset(); } catch (IOException e) { return null; } } } } return rooted; } private void reset() throws IOException { if (rooted != null) { throw new AssertionError(); } OS os = null; try { os = RootSingleton.get(getApplicationContext()); } catch (IOException e) { e.printStackTrace(); // ok } if (os == null) { os = OS.getInstance(); } rooted = os; mounts = MountsSingleton.get(os); } @NonNull public String getTypeFastest(int dirFd, CharSequence name, Stat stat) { if (stat.type != null) { switch (stat.type) { case DIRECTORY: return MIME_TYPE_DIR; case CHAR_DEV: return CHAR_DEV_MIME; } } final OS os = getOS(); if (os == null) { return DEFAULT_MIME; } // Do we have an option to open the file? Does it even make sense? // It is a bad idea to open sockets, special files and, especially, pipes // (this can cause block, take some time or have other unwelcome side-effects). // Trying to content-sniff ordinary zero-sized files also does not make sense // Unless they are on some special filesystem (such as procfs), that incorrectly // reports zero sizes to stat(). boolean canSniffContent = stat.type == FsType.FILE; int fd = Fd.NIL; try { final boolean isLink = stat.type == FsType.LINK; if (isLink) { // we must exclude the possibility that this is a symlink to directory if (!os.faccessat(dirFd, name, OS.F_OK)) { // the link is broken or target is inaccessible, bail return LINK_MIME; } try { os.fstatat(dirFd, name, stat, 0); switch (stat.type) { case DIRECTORY: return MIME_TYPE_DIR; case CHAR_DEV: return CHAR_DEV_MIME; } } catch (IOException ioe) { LogUtil.logCautiously("Unable to stat target of " + name, ioe); } } else { try { if (canSniffContent) { fd = os.openat(dirFd, name, OS.O_RDONLY, 0); os.fstat(fd, stat); } else { os.fstatat(dirFd, name, stat, 0); } } catch (IOException ioe) { LogUtil.logCautiously("Unable to directly stat " + name, ioe); } } final String extension = getExtensionFast(name); final MimeTypeMap mimeMap = MimeTypeMap.getSingleton(); final String foundMime = mimeMap.getMimeTypeFromExtension(extension); if (foundMime != null) { return foundMime; } canSniffContent = canSniffContent && (stat.st_size != 0 || isPossiblySpecial(stat)); if (isLink) { // check if link target has a usable extension CharSequence resolved = null; // Some filesystem (procfs, you!!) do export files as symlinks, but don't allow them // to be open via these symlinks. Gotta be careful here. if (canSniffContent) { try { fd = os.openat(dirFd, name, OS.O_RDONLY, 0); resolved = os.readlinkat(DirFd.NIL, fdPath(fd)); } catch (IOException ioe) { LogUtil.logCautiously("Unable to open target of " + name, ioe); } } if (resolved == null) { try { resolved = os.readlinkat(dirFd, name); if (resolved.charAt(0) == '/') { resolved = canonString(resolved); } } catch (IOException linkErr) { return LINK_MIME; } } final String linkTargetExtension = getExtensionFromPath(resolved); if (linkTargetExtension != null && !linkTargetExtension.equals(extension)) { final String sortaFastMime = mimeMap.getMimeTypeFromExtension(linkTargetExtension); if (sortaFastMime != null) { return sortaFastMime; } } // let's try to open by resolved name too, see above name = resolved; } if (canSniffContent) { if (fd < 0) { fd = os.openat(dirFd, name, OS.O_RDONLY, 0); } final String contentInfo = magic.guessMime(fd); if (contentInfo != null) { return contentInfo; } } } catch (IOException ioe) { LogUtil.logCautiously("Failed to guess type of " + name, ioe); } finally { if (fd > 0) { os.dispose(fd); } } if (stat.type == null) { return DEFAULT_MIME; } switch (stat.type) { case LINK: return LINK_MIME; case DOMAIN_SOCKET: return SOCK_MIME; case NAMED_PIPE: return FIFO_MIME; case FILE: if (stat.st_size == 0) { return EMPTY_MIME; } default: return DEFAULT_MIME; } } public String getTypeFast(@CanonPath String path, String name, Stat stat) throws FileNotFoundException { if ("/".equals(path)) { return MIME_TYPE_DIR; } final OS os = getOS(); if (os == null) { return DEFAULT_MIME; } try { @Fd int fd = Fd.NIL; @DirFd int parentFd = os.opendir(extractParent(path)); try { int flags = 0; if (!os.faccessat(parentFd, name, OS.F_OK)) { flags = OS.AT_SYMLINK_NOFOLLOW; } os.fstatat(parentFd, name, stat, flags); if (stat.type == null) { return DEFAULT_MIME; } switch (stat.type) { case LINK: return LINK_MIME; case DIRECTORY: return MIME_TYPE_DIR; case CHAR_DEV: return CHAR_DEV_MIME; default: } final String extension = getExtensionFast(name); final MimeTypeMap mimeMap = MimeTypeMap.getSingleton(); final String foundMime = mimeMap.getMimeTypeFromExtension(extension); if (foundMime != null) { return foundMime; } final boolean canSniffContent = stat.type == FsType.FILE && (stat.st_size != 0 || isPossiblySpecial(stat)); if (flags == 0) { os.fstatat(parentFd, name, stat, OS.AT_SYMLINK_NOFOLLOW); if (stat.type == FsType.LINK) { CharSequence resolved = null; if (canSniffContent) { try { fd = os.openat(parentFd, name, OS.O_RDONLY, 0); resolved = os.readlinkat(DirFd.NIL, fdPath(fd)); } catch (IOException ioe) { LogUtil.logCautiously("Unable to open target of " + name, ioe); } } if (resolved == null) { resolved = os.readlinkat(parentFd, name); if (resolved.charAt(0) == '/') { resolved = canonString(resolved); } } final String linkTargetExtension = getExtensionFromPath(resolved); if (linkTargetExtension != null && !linkTargetExtension.equals(extension)) { final String sortaFastMime = mimeMap.getMimeTypeFromExtension(linkTargetExtension); if (sortaFastMime != null) { return sortaFastMime; } } } } if (canSniffContent) { if (fd < 0) { fd = os.openat(parentFd, name, OS.O_RDONLY, 0); } final String contentInfo = magic.guessMime(fd); if (contentInfo != null) { return contentInfo; } } switch (stat.type) { case LINK: return LINK_MIME; case DOMAIN_SOCKET: return SOCK_MIME; case NAMED_PIPE: return FIFO_MIME; case FILE: if (stat.st_size == 0) { return EMPTY_MIME; } default: return DEFAULT_MIME; } } finally { if (fd > 0) { os.dispose(fd); } os.dispose(parentFd); } } catch (IOException e) { LogUtil.logCautiously("Encountered IO error during mime sniffing", e); throw new FileNotFoundException("Failed to stat " + name); } } private static String getExtensionFast(CharSequence name) { // XXX suspect conversion final String nameStr = name.toString(); final int dot = nameStr.lastIndexOf('.'); if (dot == -1 || dot == name.length() - 1) { return null; } else { return nameStr.substring(dot + 1, name.length()); } } public static String getExtensionFromPath(@CanonPath CharSequence path) { // XXX suspect conversion final String pathStr = path.toString(); final int dot = pathStr.lastIndexOf('.'); if (dot == -1 || dot == path.length() - 1 || dot < pathStr.lastIndexOf('/')) { return null; } else { return pathStr.substring(dot + 1, path.length()); } } @SuppressWarnings("SimplifiableIfStatement") public static boolean mimeTypeMatches(String filter, String test) { if (test == null) { return false; } else if (filter == null || "*/*".equals(filter)) { return true; } else if (filter.equals(test)) { return true; } else if (filter.endsWith("/*")) { return filter.regionMatches(0, test, 0, filter.indexOf('/')); } else { return false; } } @NonNull public static String extractParent(String chars) { final int lastSlash = chars.lastIndexOf('/'); switch (lastSlash) { case 0: return chars; case -1: // oops throw new IllegalStateException(chars + " must have at least one slash!"); default: return chars.substring(0, lastSlash + 1); } } public static String extractName(String chars) { final int lastSlash = chars.lastIndexOf('/'); switch (lastSlash) { case 0: case -1: return chars; default: return chars.substring(lastSlash + 1, chars.length()); } } public static boolean isCanon(CharSequence s) { // XXX suspect conversion final String str = s.toString(); int l = s.length(); // check for dots at the end if (s.charAt(l - 1) == '.') { final int i = str.lastIndexOf('/'); if (i == l - 2 || (i == l - 3 && s.charAt(l - 2) == '.')) { return false; } } // detect slash-dot-slash segments int start = 0; int idx; do { idx = str.indexOf('/', start); if (idx == -1) { break; } switch (l - idx) { default: case 4: // at least three more chars remaining to right if (s.charAt(idx + 1) == '.' && s.charAt(idx + 2) == '.' && s.charAt(idx + 3) == '/') { return false; } case 3: // at least two more chars remaining to right if (s.charAt(idx + 1) == '.' && s.charAt(idx + 2) == '/') { return false; } case 2: // at least one more char remaining to right if (s.charAt(idx + 1) == '/') { return false; } case 1: } start += 2; } while (start < l); return true; } public static void stripSlashes(StringBuilder chars) { int length = chars.length(); int prevSlash = -1; for (int i = length - 1; i >= 0; --i) { if ('/' == chars.charAt(i)) { if (prevSlash == i + 1) { chars.deleteCharAt(i); --prevSlash; } else { prevSlash = i; } } } } public static void removeDotSegments(StringBuilder chars) { if (chars.charAt(0) != '/') return; /** * / proc / self / .. / vmstat */ int seg1 = 0, seg2 = 0, seg3 = 0, seg4 = 0; int segCnt = 0; for (int i = 1; i < chars.length(); ++i) { if ('/' == chars.charAt(i)) { int segStart = 0; switch (segCnt) { case 0: // seg2 == 0 seg2 = i; segStart = seg1; ++segCnt; break; case 1: // seg3 = 0 seg3 = i; segStart = seg2; ++segCnt; break; case 2: // seg4 = 0 seg4 = i; segStart = seg3; ++segCnt; break; case 3: // seg4 > 0, carry seg1 = seg2; seg2 = seg3; seg3 = seg4; seg4 = i; segStart = seg3; } final int segLength = i - segStart; switch (segLength) { default: break; case 2: if (chars.charAt(segStart + 1) == '.') { chars.delete(segStart, i); --segCnt; i = segStart; } break; case 3: if (chars.charAt(segStart + 1) == '.' && chars.charAt(segStart + 2) == '.') { final int prevSegmentStart; if (segCnt == 3) { prevSegmentStart = seg2; segCnt = 1; } else { prevSegmentStart = seg1; segCnt = 0; } chars.delete(prevSegmentStart, i); i = prevSegmentStart; } } } } final int resultLength = chars.length(); if ('/' != chars.charAt(resultLength - 1)) { int lastSlash, prevSlash = 0; switch (segCnt) { case 3: lastSlash = seg4; prevSlash = seg3; break; case 2: lastSlash = seg3; prevSlash = seg2; break; case 1: lastSlash = seg2; prevSlash = seg1; break; default: lastSlash = 0; } switch (resultLength - lastSlash) { case 2: if (chars.charAt(lastSlash + 1) == '.') { chars.delete(lastSlash + 1, resultLength + 1); } break; case 3: if (chars.charAt(lastSlash + 1) == '.' && chars.charAt(lastSlash + 2) == '.') { chars.delete(prevSlash + 1, resultLength + 1); } break; } } } public static String appendPathPart(String parent, String displayName) { final String result; final StringBuilder builder = acquire(parent.length() + displayName.length() + 1); try { builder.append(parent).append('/').append(displayName); result = builder.toString(); } finally { release(builder); } return result; } private static Pools.Pool<StringBuilder> builderPool = new Pools.SynchronizedPool<>(2); public static StringBuilder acquire(int length) { StringBuilder builder = builderPool.acquire(); if (builder == null) { builder = new StringBuilder(length); } builder.ensureCapacity(length); return builder; } public static void release(StringBuilder builder) { builder.setLength(0); builderPool.release(builder); } static String canonString(StringBuilder builder) { stripSlashes(builder); removeDotSegments(builder); return builder.toString(); } static String canonString(CharSequence path) { // XXX suspect conversion final String pathStr = path.toString(); if (isCanon(pathStr)) return pathStr; final StringBuilder builder = acquire(path.length()); final String result; try { builder.append(path); stripSlashes(builder); removeDotSegments(builder); result = builder.toString(); } finally { release(builder); } return result; } @Nullable String[] getStreamTypes(@CanonPath String filepath, String mimeTypeFilter) { final TimestampedMime guess = guessMimeInternal(filepath); if (guess == null) { return null; } final String[] types = guess.mime; final ArrayList<String> acceptedTypes = new ArrayList<>(); for (String type : types) { if (mimeTypeMatches(mimeTypeFilter, type)) acceptedTypes.add(type); } return acceptedTypes.isEmpty() ? null : acceptedTypes.toArray(new String[acceptedTypes.size()]); } public CharSequence resolve(String externalPath) { final OS os = getOS(); if (os == null) { return null; } try { //noinspection WrongConstant @Fd int fd = os.open(externalPath, NativeBits.O_PATH, OS.DEF_FILE_MODE); try { return os.readlinkat(DirFd.NIL, fdPath(fd)); } finally { os.dispose(fd); } } catch (IOException e) { } return null; } @Nullable private TimestampedMime guessMimeInternal(String filepath) { final TimestampedMime cachedResult = fileTypeCache.get(filepath); if (cachedResult != null && System.nanoTime() - cachedResult.when > 2_000_000_000) { return cachedResult; } final OS os = getOS(); if (os == null) { return null; } final String[] guessed = guessTypes(os, filepath); if (guessed == null) { return null; } final TimestampedMime mime = new TimestampedMime(); mime.mime = guessed; mime.when = System.nanoTime(); fileTypeCache.put(filepath, mime); return mime; } private String[] guessTypes(OS os, String filepath) { if ("/".equals(filepath)) { return new String[] { MIME_TYPE_DIR, ALT_DIR_MIME }; } final ObjectSet<String> mimeCandidates = new ObjectHashSet<>(); int fd = 0; try { final Stat s = new Stat(); final String filename = extractName(filepath); @DirFd int parentFd = os.opendir(extractParent(filepath)); try { try { os.fstatat(parentFd, filename, s, 0); } catch (IOException ioe) { LogUtil.logCautiously("Failed to invoke stat() on " + filename, ioe); } FsType origType = null; if (s.type != null) { origType = s.type; switch (s.type) { case DIRECTORY: return new String[] { MIME_TYPE_DIR, ALT_DIR_MIME }; case CHAR_DEV: return new String[] { CHAR_DEV_MIME }; case NAMED_PIPE: mimeCandidates.add(FIFO_MIME); break; case DOMAIN_SOCKET: mimeCandidates.add(SOCK_MIME); break; case BLOCK_DEV: mimeCandidates.add(BLOCK_MIME); case FILE: try { fd = os.openat(parentFd, filename, OS.O_RDONLY, 0); } catch (IOException ioe) { LogUtil.logCautiously("Failed to invoke open() on " + filename, ioe); } default: } } os.fstatat(parentFd, filename, s, OS.AT_SYMLINK_NOFOLLOW); if (s.type == FsType.LINK) { try { CharSequence resolved = os.readlinkat(parentFd, filename); if (!filepath.contentEquals(resolved)) { addNameCandidates(resolved, mimeCandidates); } if (origType == FsType.FILE) { fd = os.openat(parentFd, resolved, OS.O_RDONLY, 0); } } catch (IOException e) { LogUtil.logCautiously("Error during path resolution", e); } } } finally { os.dispose(parentFd); } if (fd > 0) { final String contentInfo = magic.guessMime(fd); if (!TextUtils.isEmpty(contentInfo) && !DEFAULT_MIME.equals(contentInfo)) { mimeCandidates.add(contentInfo); } } } catch (IOException e) { LogUtil.logCautiously("Error during guessing mime type", e); } finally { if (fd > 0) { os.dispose(fd); } } addNameCandidates(filepath, mimeCandidates); return mimeCandidates.toArray(String.class); } public static String fdPath(int fd) { final String result; final StringBuilder builder = acquire(30); try { result = builder.append("/proc/").append(MY_PID).append("/fd/").append(fd).toString(); } finally { release(builder); } return result; } private boolean isPossiblySpecial(Stat s) { if (s == null) return true; final Lock lock = mounts.getLock(); lock.lock(); try { final MountInfo.Mount mount = mounts.mountMap.get(s.st_dev); if (mount == null || mounts.isVolatile(mount)) { return true; } } finally { lock.unlock(); } return false; } private void addNameCandidates(CharSequence filepath, ObjectSet<String> mimeCandidates) { // XXX suspect conversion final String filepathStr = filepath.toString(); final String name = extractName(filepathStr); if (TextUtils.isEmpty(name)) { return; } final int dot = name.lastIndexOf('.'); if (dot == -1 || dot == name.length() - 1) { return; } final String extension = name.substring(dot + 1, name.length()); mimeCandidates.add("application/x-" + extension); final MimeTypeMap map = MimeTypeMap.getSingleton(); final String foundMime = map.getMimeTypeFromExtension(extension); if (!TextUtils.isEmpty(foundMime) && !DEFAULT_MIME.equals(foundMime)) { mimeCandidates.add(foundMime); } } void assertAbsolute(Uri uri) throws FileNotFoundException { if (uri == null || !authority.equals(uri.getAuthority()) || uri.getPath() == null) { throw new FileNotFoundException(); } assertAbsolute(uri.getPath()); } static void assertFilename(String nameStr) throws FileNotFoundException { if (nameStr.length() == 0) { throw new FileNotFoundException("The name is empty"); } if (nameStr.indexOf('\0') != -1) { throw new FileNotFoundException("The file name contains illegal characters"); } if (nameStr.indexOf('/') != -1) { throw new FileNotFoundException(nameStr + " is not a valid filename, must not contain '/'"); } } static void assertAbsolute(String pathStr) throws FileNotFoundException { if (pathStr.length() == 0) { throw new FileNotFoundException("The path is empty"); } if (pathStr.indexOf('\0') != -1) { throw new FileNotFoundException("The file path contains illegal characters"); } if (pathStr.charAt(0) != '/') { throw new FileNotFoundException(pathStr + " is invalid in this context, must be absolute"); } } /** * @return {@code true} if filesystem is in list of filesystems, known to support telldir, Linux filename conventions etc. {@code false} otherwise */ public static boolean isPosix(String filesystemName) { switch (filesystemName) { case "ext3": case "ext4": case "xfs": case "f2fS": case "procfs": case "sysfs": case "tmpfs": case "devpts": case "rootfs": return true; default: return false; } } private void wow() { PackageManager pm = getPackageManager(); } private static final class TimestampedMime { long size = -1; long when; String[] mime; } }