Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fe738fbd authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Restore file truncation where expected.

Several years ago ParcelFileDescriptor.parseMode() was fixed to match
the behavior of fopen(), since developers expect consistent behavior
between managed and native code.  FileUtilsTest.testTranslateMode()
verifies that all these modes are correctly translated.

However, this unintentionally changed the behavior of
ContentResolver.openOutputStream(), which only sends the 'w' mode
to the remote process.  Developers expect this API to behave like
the FileOutputStream constructor, which always truncates the file
unless opened with the append mode.

Since some remote providers may not be prepared to handle the 't'
mode, this change carefully uses Os.ftruncate() to restore this
expected behavior in all cases.

For other APIs that return opened files, this strategy is applied
to restore the original behavior, but only when the target SDK of
the app is expecting this truncation to take place.  The reason for
this is that moving forward our goal should always enable
ContentInterface APIs to be a transparent conversation between apps
without attempting to alter the behavior.  Apps talking with older
providers can apply the Os.ftruncate() logic themselves, if
desired, once they target Android Q or higher.

Bug: 157888856, 180680924
Test: atest CtsContentTestCases:ContentResolverTest
Change-Id: Iacec49164c4ce3891db0270635e9f458dea7becd
parent ddcd076c
Loading
Loading
Loading
Loading
+15 −22
Original line number Diff line number Diff line
@@ -1885,9 +1885,9 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall
     * in {@link android.provider.MediaStore.MediaColumns}.</p>
     *
     * @param uri The URI whose file is to be opened.
     * @param mode Access mode for the file.  May be "r" for read-only access,
     * "rw" for read and write access, or "rwt" for read and write access
     * that truncates any existing file.
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt". See
     *            {@link ParcelFileDescriptor#parseMode} for more details.
     *
     * @return Returns a new ParcelFileDescriptor which you can use to access
     * the file.
@@ -1948,10 +1948,9 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall
     * in {@link android.provider.MediaStore.MediaColumns}.</p>
     *
     * @param uri The URI whose file is to be opened.
     * @param mode Access mode for the file. May be "r" for read-only access,
     *            "w" for write-only access, "rw" for read and write access, or
     *            "rwt" for read and write access that truncates any existing
     *            file.
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt". See
     *            {@link ParcelFileDescriptor#parseMode} for more details.
     * @param signal A signal to cancel the operation in progress, or
     *            {@code null} if none. For example, if you are downloading a
     *            file from the network to service a "rw" mode request, you
@@ -2011,11 +2010,9 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall
     * containing at least the columns specified by {@link android.provider.OpenableColumns}.</p>
     *
     * @param uri The URI whose file is to be opened.
     * @param mode Access mode for the file.  May be "r" for read-only access,
     * "w" for write-only access (erasing whatever data is currently in
     * the file), "wa" for write-only access to append to any existing data,
     * "rw" for read and write access on any existing data, and "rwt" for read
     * and write access that truncates any existing file.
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt". See
     *            {@link ParcelFileDescriptor#parseMode} for more details.
     *
     * @return Returns a new AssetFileDescriptor which you can use to access
     * the file.
@@ -2068,11 +2065,9 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall
     * containing at least the columns specified by {@link android.provider.OpenableColumns}.</p>
     *
     * @param uri The URI whose file is to be opened.
     * @param mode Access mode for the file.  May be "r" for read-only access,
     * "w" for write-only access (erasing whatever data is currently in
     * the file), "wa" for write-only access to append to any existing data,
     * "rw" for read and write access on any existing data, and "rwt" for read
     * and write access that truncates any existing file.
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt". See
     *            {@link ParcelFileDescriptor#parseMode} for more details.
     * @param signal A signal to cancel the operation in progress, or
     *            {@code null} if none. For example, if you are downloading a
     *            file from the network to service a "rw" mode request, you
@@ -2103,11 +2098,9 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall
     * by looking up a column named "_data" at the given URI.
     *
     * @param uri The URI to be opened.
     * @param mode The file mode.  May be "r" for read-only access,
     * "w" for write-only access (erasing whatever data is currently in
     * the file), "wa" for write-only access to append to any existing data,
     * "rw" for read and write access on any existing data, and "rwt" for read
     * and write access that truncates any existing file.
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt". See
     *            {@link ParcelFileDescriptor#parseMode} for more details.
     *
     * @return Returns a new ParcelFileDescriptor that can be used by the
     * client to access the file.
+73 −13
Original line number Diff line number Diff line
@@ -63,7 +63,9 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.system.ErrnoException;
import android.system.Int64Ref;
import android.system.Os;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
@@ -76,8 +78,10 @@ import com.android.internal.util.MimeIconUtils;
import dalvik.system.CloseGuard;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -864,6 +868,20 @@ public abstract class ContentResolver implements ContentInterface {
        return wrap((ContentInterface) wrapped);
    }

    /**
     * Offer to locally truncate the given file when opened using the write-only
     * mode. This is typically used to preserve legacy compatibility behavior.
     */
    private static void maybeTruncate(FileDescriptor fd, String mode) throws FileNotFoundException {
        if ("w".equals(mode)) {
            try {
                Os.ftruncate(fd, 0);
            } catch (ErrnoException e) {
                throw new FileNotFoundException("Failed to truncate: " + e.getMessage());
            }
        }
    }

    /** @hide */
    @SuppressWarnings("HiddenAbstractMethod")
    @UnsupportedAppUsage
@@ -1525,8 +1543,20 @@ public abstract class ContentResolver implements ContentInterface {
    }

    /**
     * Synonym for {@link #openOutputStream(Uri, String)
     * openOutputStream(uri, "w")}.
     * Open a stream on to the content associated with a content URI.  If there
     * is no data associated with the URI, FileNotFoundException is thrown.
     *
     * <h5>Accepts the following URI schemes:</h5>
     * <ul>
     * <li>content ({@link #SCHEME_CONTENT})</li>
     * <li>file ({@link #SCHEME_FILE})</li>
     * </ul>
     *
     * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information
     * on these schemes.
     *
     * <p>This method behaves like {@link FileOutputStream} and automatically
     * truncates any existing contents.
     *
     * @param uri The desired URI.
     * @return an OutputStream or {@code null} if the provider recently crashed.
@@ -1534,7 +1564,16 @@ public abstract class ContentResolver implements ContentInterface {
     */
    public final @Nullable OutputStream openOutputStream(@NonNull Uri uri)
            throws FileNotFoundException {
        return openOutputStream(uri, "w");
        AssetFileDescriptor fd = openAssetFileDescriptor(uri, "w", null);
        if (fd == null) return null;
        try {
            final FileOutputStream res = fd.createOutputStream();
            // Unconditionally truncate to mirror FileOutputStream behavior
            maybeTruncate(res.getFD(), "w");
            return res;
        } catch (IOException e) {
            throw new FileNotFoundException("Unable to create stream");
        }
    }

    /**
@@ -1551,7 +1590,9 @@ public abstract class ContentResolver implements ContentInterface {
     * on these schemes.
     *
     * @param uri The desired URI.
     * @param mode May be "w", "wa", "rw", or "rwt".
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt". See
     *            {@link ParcelFileDescriptor#parseMode} for more details.
     * @return an OutputStream or {@code null} if the provider recently crashed.
     * @throws FileNotFoundException if the provided URI could not be opened.
     * @see #openAssetFileDescriptor(Uri, String)
@@ -1559,8 +1600,14 @@ public abstract class ContentResolver implements ContentInterface {
    public final @Nullable OutputStream openOutputStream(@NonNull Uri uri, @NonNull String mode)
            throws FileNotFoundException {
        AssetFileDescriptor fd = openAssetFileDescriptor(uri, mode, null);
        if (fd == null) return null;
        try {
            return fd != null ? fd.createOutputStream() : null;
            final FileOutputStream res = fd.createOutputStream();
            // Preserve legacy behavior by offering to truncate
            if (mTargetSdkVersion < Build.VERSION_CODES.Q) {
                maybeTruncate(res.getFD(), mode);
            }
            return res;
        } catch (IOException e) {
            throw new FileNotFoundException("Unable to create stream");
        }
@@ -1607,8 +1654,9 @@ public abstract class ContentResolver implements ContentInterface {
     * provider, use {@link ParcelFileDescriptor#closeWithError(String)}.
     *
     * @param uri The desired URI to open.
     * @param mode The file mode to use, as per {@link ContentProvider#openFile
     * ContentProvider.openFile}.
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt". See
     *            {@link ParcelFileDescriptor#parseMode} for more details.
     * @return Returns a new ParcelFileDescriptor pointing to the file or {@code null} if the
     * provider recently crashed. You own this descriptor and are responsible for closing it
     * when done.
@@ -1650,8 +1698,9 @@ public abstract class ContentResolver implements ContentInterface {
     * provider, use {@link ParcelFileDescriptor#closeWithError(String)}.
     *
     * @param uri The desired URI to open.
     * @param mode The file mode to use, as per {@link ContentProvider#openFile
     * ContentProvider.openFile}.
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt". See
     *            {@link ParcelFileDescriptor#parseMode} for more details.
     * @param cancellationSignal A signal to cancel the operation in progress,
     *         or null if none. If the operation is canceled, then
     *         {@link OperationCanceledException} will be thrown.
@@ -1744,8 +1793,9 @@ public abstract class ContentResolver implements ContentInterface {
     * from any built-in data conversion that a provider implements.
     *
     * @param uri The desired URI to open.
     * @param mode The file mode to use, as per {@link ContentProvider#openAssetFile
     * ContentProvider.openAssetFile}.
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt". See
     *            {@link ParcelFileDescriptor#parseMode} for more details.
     * @return Returns a new ParcelFileDescriptor pointing to the file or {@code null} if the
     * provider recently crashed. You own this descriptor and are responsible for closing it
     * when done.
@@ -1798,8 +1848,9 @@ public abstract class ContentResolver implements ContentInterface {
     * from any built-in data conversion that a provider implements.
     *
     * @param uri The desired URI to open.
     * @param mode The file mode to use, as per {@link ContentProvider#openAssetFile
     * ContentProvider.openAssetFile}.
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt". See
     *            {@link ParcelFileDescriptor#parseMode} for more details.
     * @param cancellationSignal A signal to cancel the operation in progress, or null if
     *            none. If the operation is canceled, then
     *            {@link OperationCanceledException} will be thrown.
@@ -1835,6 +1886,10 @@ public abstract class ContentResolver implements ContentInterface {
        } else if (SCHEME_FILE.equals(scheme)) {
            ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
                    new File(uri.getPath()), ParcelFileDescriptor.parseMode(mode));
            // Preserve legacy behavior by offering to truncate
            if (mTargetSdkVersion < Build.VERSION_CODES.Q) {
                maybeTruncate(pfd.getFileDescriptor(), mode);
            }
            return new AssetFileDescriptor(pfd, 0, -1);
        } else {
            if ("r".equals(mode)) {
@@ -1892,6 +1947,11 @@ public abstract class ContentResolver implements ContentInterface {
                    // ParcelFileDescriptorInner do that when it is closed.
                    stableProvider = null;

                    // Preserve legacy behavior by offering to truncate
                    if (mTargetSdkVersion < Build.VERSION_CODES.Q) {
                        maybeTruncate(pfd.getFileDescriptor(), mode);
                    }

                    return new AssetFileDescriptor(pfd, fd.getStartOffset(),
                            fd.getDeclaredLength());

+49 −9
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import static android.system.OsConstants.S_ISLNK;
import static android.system.OsConstants.S_ISREG;
import static android.system.OsConstants.S_IWOTH;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
@@ -63,6 +64,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.UncheckedIOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.DatagramSocket;
import java.net.Socket;
import java.nio.ByteOrder;
@@ -110,6 +113,20 @@ public class ParcelFileDescriptor implements Parcelable, Closeable {

    private final CloseGuard mGuard = CloseGuard.get();

    /** @hide */
    @IntDef(prefix = {"MODE_"}, value = {
            MODE_WORLD_READABLE,
            MODE_WORLD_WRITEABLE,
            MODE_READ_ONLY,
            MODE_WRITE_ONLY,
            MODE_READ_WRITE,
            MODE_CREATE,
            MODE_TRUNCATE,
            MODE_APPEND,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Mode { }

    /**
     * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied and
     * this file doesn't already exist, then create the file with permissions
@@ -227,7 +244,8 @@ public class ParcelFileDescriptor implements Parcelable, Closeable {
     *             be opened with the requested mode.
     * @see #parseMode(String)
     */
    public static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException {
    public static ParcelFileDescriptor open(File file, @Mode int mode)
            throws FileNotFoundException {
        final FileDescriptor fd = openInternal(file, mode);
        if (fd == null) return null;

@@ -259,7 +277,7 @@ public class ParcelFileDescriptor implements Parcelable, Closeable {
    // We can't accept a generic Executor here, since we need to use
    // MessageQueue.addOnFileDescriptorEventListener()
    @SuppressLint("ExecutorRegistration")
    public static ParcelFileDescriptor open(File file, int mode, Handler handler,
    public static ParcelFileDescriptor open(File file, @Mode int mode, Handler handler,
            final OnCloseListener listener) throws IOException {
        if (handler == null) {
            throw new IllegalArgumentException("Handler must not be null");
@@ -330,7 +348,8 @@ public class ParcelFileDescriptor implements Parcelable, Closeable {
        return pfd;
    }

    private static FileDescriptor openInternal(File file, int mode) throws FileNotFoundException {
    private static FileDescriptor openInternal(File file, @Mode int mode)
            throws FileNotFoundException {
        final int flags = FileUtils.translateModePfdToPosix(mode) | ifAtLeastQ(O_CLOEXEC);

        int realMode = S_IRWXU | S_IRWXG;
@@ -623,15 +642,36 @@ public class ParcelFileDescriptor implements Parcelable, Closeable {
    }

    /**
     * Converts a string representing a file mode, such as "rw", into a bitmask suitable for use
     * with {@link #open}.
     * Converts a string representing a file mode, such as "rw", into a bitmask
     * suitable for use with {@link #open}.
     * <p>
     * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
     *             or "rwt".
     * The argument must define at least one of the following base access modes:
     * <ul>
     * <li>"r" indicates the file should be opened in read-only mode, equivalent
     * to {@link OsConstants#O_RDONLY}.
     * <li>"w" indicates the file should be opened in write-only mode,
     * equivalent to {@link OsConstants#O_WRONLY}.
     * <li>"rw" indicates the file should be opened in read-write mode,
     * equivalent to {@link OsConstants#O_RDWR}.
     * </ul>
     * In addition to a base access mode, the following additional modes may
     * requested:
     * <ul>
     * <li>"a" indicates the file should be opened in append mode, equivalent to
     * {@link OsConstants#O_APPEND}. Before each write, the file offset is
     * positioned at the end of the file.
     * <li>"t" indicates the file should be opened in truncate mode, equivalent
     * to {@link OsConstants#O_TRUNC}. If the file already exists and is a
     * regular file and is opened for writing, it will be truncated to length 0.
     * </ul>
     *
     * @param mode The string representation of the file mode. Can be "r", "w",
     *            "wt", "wa", "rw" or "rwt".
     * @return A bitmask representing the given file mode.
     * @throws IllegalArgumentException if the given string does not match a known file mode.
     * @throws IllegalArgumentException if the given string does not match a
     *             known file mode.
     */
    public static int parseMode(String mode) {
    public static @Mode int parseMode(String mode) {
        return FileUtils.translateModePosixToPfd(FileUtils.translateModeStringToPosix(mode));
    }