1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.internal.util; 17 18 import static android.system.Os.readlink; 19 import static android.system.Os.symlink; 20 21 import android.content.Context; 22 import android.net.Uri; 23 import android.os.Build.VERSION_CODES; 24 import android.system.ErrnoException; 25 import androidx.annotation.RequiresApi; 26 import com.google.android.libraries.mobiledatadownload.file.backends.AndroidUri; 27 import com.google.android.libraries.mobiledatadownload.file.backends.AndroidUriAdapter; 28 import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException; 29 import java.io.IOException; 30 31 /** Utility class to create symlinks (if supported). */ 32 @RequiresApi(VERSION_CODES.LOLLIPOP) 33 public final class SymlinkUtil { SymlinkUtil()34 private SymlinkUtil() {} 35 36 /** 37 * Creates a symlink at the given link Uri to the given target Uri. 38 * 39 * <p>This method will only work for API level 21+ since this is when Android added a platform 40 * level symlink function. 41 * 42 * @param context the caller's context 43 * @param linkUri location to create the symlink 44 * @param targetUri location where the symlink should point 45 * @throws IOException when symlink could not be created 46 */ 47 @RequiresApi(VERSION_CODES.LOLLIPOP) createSymlink(Context context, Uri linkUri, Uri targetUri)48 public static void createSymlink(Context context, Uri linkUri, Uri targetUri) throws IOException { 49 try { 50 AndroidUriAdapter adapter = AndroidUriAdapter.forContext(context); 51 symlink( 52 adapter.toFile(targetUri).getAbsolutePath(), adapter.toFile(linkUri).getAbsolutePath()); 53 } catch (MalformedUriException | ErrnoException e) { 54 // wrap the exception so it isn't explicitly referenced at higher levels that run on 55 // API level 21 or below. 56 throw new IOException("Unable to create symlink", e); 57 } 58 } 59 60 /** 61 * Reads the given symlink Uri and returns its target Uri. 62 * 63 * <p>This method will only work for API level 21+ since this is when Android added a platform 64 * level readlink function. It wraps around Android's readlink function and exposes it in a 65 * version safe way behind the @RequiresApi annotation. 66 * 67 * <p>NOTE: This method only verifies the given input as a valid symlink and points to a target 68 * uri. However, it makes no guarantees about the target uri (i.e. whether the target exists). 69 * 70 * @param context the caller's context 71 * @param symlinkUri the symlink location that should be checked 72 * @return Uri to the target location of the symlink 73 * @throws IOException when symlink is unable to be checked 74 */ 75 @RequiresApi(VERSION_CODES.LOLLIPOP) readSymlink(Context context, Uri symlinkUri)76 public static Uri readSymlink(Context context, Uri symlinkUri) throws IOException { 77 try { 78 AndroidUriAdapter adapter = AndroidUriAdapter.forContext(context); 79 String targetPath = readlink(adapter.toFile(symlinkUri).getAbsolutePath()); 80 81 if (targetPath == null) { 82 throw new IOException("Unable to read symlink"); 83 } 84 85 return AndroidUri.builder(context) 86 .fromAbsolutePath(targetPath, /* accountManager= */ null) 87 .build(); 88 } catch (MalformedUriException | ErrnoException e) { 89 // wrap the exception so it isn't explicitly referenced at higher levels that run on 90 // API level 21 or below. 91 throw new IOException("Unable to read symlink", e); 92 } 93 } 94 } 95