Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | package org.mozilla.gecko; |
michael@0 | 6 | |
michael@0 | 7 | import java.io.File; |
michael@0 | 8 | import java.util.Enumeration; |
michael@0 | 9 | import java.util.HashMap; |
michael@0 | 10 | import java.util.Map; |
michael@0 | 11 | |
michael@0 | 12 | import org.mozilla.gecko.util.INIParser; |
michael@0 | 13 | import org.mozilla.gecko.util.INISection; |
michael@0 | 14 | |
michael@0 | 15 | import android.content.Context; |
michael@0 | 16 | |
michael@0 | 17 | /** |
michael@0 | 18 | * <code>GeckoProfileDirectories</code> manages access to mappings from profile |
michael@0 | 19 | * names to salted profile directory paths, as well as the default profile name. |
michael@0 | 20 | * |
michael@0 | 21 | * This class will eventually come to encapsulate the remaining logic embedded |
michael@0 | 22 | * in profiles.ini; for now it's a read-only wrapper. |
michael@0 | 23 | */ |
michael@0 | 24 | public class GeckoProfileDirectories { |
michael@0 | 25 | @SuppressWarnings("serial") |
michael@0 | 26 | public static class NoMozillaDirectoryException extends Exception { |
michael@0 | 27 | public NoMozillaDirectoryException(Throwable cause) { |
michael@0 | 28 | super(cause); |
michael@0 | 29 | } |
michael@0 | 30 | |
michael@0 | 31 | public NoMozillaDirectoryException(String reason) { |
michael@0 | 32 | super(reason); |
michael@0 | 33 | } |
michael@0 | 34 | |
michael@0 | 35 | public NoMozillaDirectoryException(String reason, Throwable cause) { |
michael@0 | 36 | super(reason, cause); |
michael@0 | 37 | } |
michael@0 | 38 | } |
michael@0 | 39 | |
michael@0 | 40 | @SuppressWarnings("serial") |
michael@0 | 41 | public static class NoSuchProfileException extends Exception { |
michael@0 | 42 | public NoSuchProfileException(String detailMessage, Throwable cause) { |
michael@0 | 43 | super(detailMessage, cause); |
michael@0 | 44 | } |
michael@0 | 45 | |
michael@0 | 46 | public NoSuchProfileException(String detailMessage) { |
michael@0 | 47 | super(detailMessage); |
michael@0 | 48 | } |
michael@0 | 49 | } |
michael@0 | 50 | |
michael@0 | 51 | private interface INISectionPredicate { |
michael@0 | 52 | public boolean matches(INISection section); |
michael@0 | 53 | } |
michael@0 | 54 | |
michael@0 | 55 | private static final String MOZILLA_DIR_NAME = "mozilla"; |
michael@0 | 56 | |
michael@0 | 57 | /** |
michael@0 | 58 | * Returns true if the supplied profile entry represents the default profile. |
michael@0 | 59 | */ |
michael@0 | 60 | private static INISectionPredicate sectionIsDefault = new INISectionPredicate() { |
michael@0 | 61 | @Override |
michael@0 | 62 | public boolean matches(INISection section) { |
michael@0 | 63 | return section.getIntProperty("Default") == 1; |
michael@0 | 64 | } |
michael@0 | 65 | }; |
michael@0 | 66 | |
michael@0 | 67 | /** |
michael@0 | 68 | * Returns true if the supplied profile entry has a 'Name' field. |
michael@0 | 69 | */ |
michael@0 | 70 | private static INISectionPredicate sectionHasName = new INISectionPredicate() { |
michael@0 | 71 | @Override |
michael@0 | 72 | public boolean matches(INISection section) { |
michael@0 | 73 | final String name = section.getStringProperty("Name"); |
michael@0 | 74 | return name != null; |
michael@0 | 75 | } |
michael@0 | 76 | }; |
michael@0 | 77 | |
michael@0 | 78 | /** |
michael@0 | 79 | * Package-scoped because GeckoProfile needs to dig into this in order to do writes. |
michael@0 | 80 | * This will be fixed in Bug 975212. |
michael@0 | 81 | */ |
michael@0 | 82 | static INIParser getProfilesINI(File mozillaDir) { |
michael@0 | 83 | return new INIParser(new File(mozillaDir, "profiles.ini")); |
michael@0 | 84 | } |
michael@0 | 85 | |
michael@0 | 86 | /** |
michael@0 | 87 | * Utility method to compute a salted profile name: eight random alphanumeric |
michael@0 | 88 | * characters, followed by a period, followed by the profile name. |
michael@0 | 89 | */ |
michael@0 | 90 | public static String saltProfileName(final String name) { |
michael@0 | 91 | if (name == null) { |
michael@0 | 92 | throw new IllegalArgumentException("Cannot salt null profile name."); |
michael@0 | 93 | } |
michael@0 | 94 | |
michael@0 | 95 | final String allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789"; |
michael@0 | 96 | final int scale = allowedChars.length(); |
michael@0 | 97 | final int saltSize = 8; |
michael@0 | 98 | |
michael@0 | 99 | final StringBuilder saltBuilder = new StringBuilder(saltSize + 1 + name.length()); |
michael@0 | 100 | for (int i = 0; i < saltSize; i++) { |
michael@0 | 101 | saltBuilder.append(allowedChars.charAt((int)(Math.random() * scale))); |
michael@0 | 102 | } |
michael@0 | 103 | saltBuilder.append('.'); |
michael@0 | 104 | saltBuilder.append(name); |
michael@0 | 105 | return saltBuilder.toString(); |
michael@0 | 106 | } |
michael@0 | 107 | |
michael@0 | 108 | /** |
michael@0 | 109 | * Return the Mozilla directory within the files directory of the provided |
michael@0 | 110 | * context. This should always be the same within a running application. |
michael@0 | 111 | * |
michael@0 | 112 | * This method is package-scoped so that new {@link GeckoProfile} instances can |
michael@0 | 113 | * contextualize themselves. |
michael@0 | 114 | * |
michael@0 | 115 | * @return a new File object for the Mozilla directory. |
michael@0 | 116 | * @throws NoMozillaDirectoryException |
michael@0 | 117 | * if the directory did not exist and could not be created. |
michael@0 | 118 | */ |
michael@0 | 119 | static File getMozillaDirectory(Context context) throws NoMozillaDirectoryException { |
michael@0 | 120 | final File mozillaDir = new File(context.getFilesDir(), MOZILLA_DIR_NAME); |
michael@0 | 121 | if (mozillaDir.exists() || mozillaDir.mkdirs()) { |
michael@0 | 122 | return mozillaDir; |
michael@0 | 123 | } |
michael@0 | 124 | |
michael@0 | 125 | // Although this leaks a path to the system log, the path is |
michael@0 | 126 | // predictable (unlike a profile directory), so this is fine. |
michael@0 | 127 | throw new NoMozillaDirectoryException("Unable to create mozilla directory at " + mozillaDir.getAbsolutePath()); |
michael@0 | 128 | } |
michael@0 | 129 | |
michael@0 | 130 | /** |
michael@0 | 131 | * Discover the default profile name by examining profiles.ini. |
michael@0 | 132 | * |
michael@0 | 133 | * Package-scoped because {@link GeckoProfile} needs access to it. |
michael@0 | 134 | * |
michael@0 | 135 | * @return null if there is no "Default" entry in profiles.ini, or the profile |
michael@0 | 136 | * name if there is. |
michael@0 | 137 | * @throws NoMozillaDirectoryException |
michael@0 | 138 | * if the Mozilla directory did not exist and could not be created. |
michael@0 | 139 | */ |
michael@0 | 140 | static String findDefaultProfileName(final Context context) throws NoMozillaDirectoryException { |
michael@0 | 141 | final INIParser parser = GeckoProfileDirectories.getProfilesINI(getMozillaDirectory(context)); |
michael@0 | 142 | |
michael@0 | 143 | for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) { |
michael@0 | 144 | final INISection section = e.nextElement(); |
michael@0 | 145 | if (section.getIntProperty("Default") == 1) { |
michael@0 | 146 | return section.getStringProperty("Name"); |
michael@0 | 147 | } |
michael@0 | 148 | } |
michael@0 | 149 | |
michael@0 | 150 | return null; |
michael@0 | 151 | } |
michael@0 | 152 | |
michael@0 | 153 | static Map<String, String> getDefaultProfile(final File mozillaDir) { |
michael@0 | 154 | return getMatchingProfiles(mozillaDir, sectionIsDefault, true); |
michael@0 | 155 | } |
michael@0 | 156 | |
michael@0 | 157 | static Map<String, String> getProfilesNamed(final File mozillaDir, final String name) { |
michael@0 | 158 | final INISectionPredicate predicate = new INISectionPredicate() { |
michael@0 | 159 | @Override |
michael@0 | 160 | public boolean matches(final INISection section) { |
michael@0 | 161 | return name.equals(section.getStringProperty("Name")); |
michael@0 | 162 | } |
michael@0 | 163 | }; |
michael@0 | 164 | return getMatchingProfiles(mozillaDir, predicate, true); |
michael@0 | 165 | } |
michael@0 | 166 | |
michael@0 | 167 | /** |
michael@0 | 168 | * Calls {@link GeckoProfileDirectories#getMatchingProfiles(File, INISectionPredicate, boolean)} |
michael@0 | 169 | * with a filter to ensure that all profiles are named. |
michael@0 | 170 | */ |
michael@0 | 171 | static Map<String, String> getAllProfiles(final File mozillaDir) { |
michael@0 | 172 | return getMatchingProfiles(mozillaDir, sectionHasName, false); |
michael@0 | 173 | } |
michael@0 | 174 | |
michael@0 | 175 | /** |
michael@0 | 176 | * Return a mapping from the names of all matching profiles (that is, |
michael@0 | 177 | * profiles appearing in profiles.ini that match the supplied predicate) to |
michael@0 | 178 | * their absolute paths on disk. |
michael@0 | 179 | * |
michael@0 | 180 | * @param mozillaDir |
michael@0 | 181 | * a directory containing profiles.ini. |
michael@0 | 182 | * @param predicate |
michael@0 | 183 | * a predicate to use when evaluating whether to include a |
michael@0 | 184 | * particular INI section. |
michael@0 | 185 | * @param stopOnSuccess |
michael@0 | 186 | * if true, this method will return with the first result that |
michael@0 | 187 | * matches the predicate; if false, all matching results are |
michael@0 | 188 | * included. |
michael@0 | 189 | * @return a {@link Map} from name to path. |
michael@0 | 190 | */ |
michael@0 | 191 | public static Map<String, String> getMatchingProfiles(final File mozillaDir, INISectionPredicate predicate, boolean stopOnSuccess) { |
michael@0 | 192 | final HashMap<String, String> result = new HashMap<String, String>(); |
michael@0 | 193 | final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir); |
michael@0 | 194 | |
michael@0 | 195 | for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) { |
michael@0 | 196 | final INISection section = e.nextElement(); |
michael@0 | 197 | if (predicate == null || predicate.matches(section)) { |
michael@0 | 198 | final String name = section.getStringProperty("Name"); |
michael@0 | 199 | final String pathString = section.getStringProperty("Path"); |
michael@0 | 200 | final boolean isRelative = section.getIntProperty("IsRelative") == 1; |
michael@0 | 201 | final File path = isRelative ? new File(mozillaDir, pathString) : new File(pathString); |
michael@0 | 202 | result.put(name, path.getAbsolutePath()); |
michael@0 | 203 | |
michael@0 | 204 | if (stopOnSuccess) { |
michael@0 | 205 | return result; |
michael@0 | 206 | } |
michael@0 | 207 | } |
michael@0 | 208 | } |
michael@0 | 209 | return result; |
michael@0 | 210 | } |
michael@0 | 211 | |
michael@0 | 212 | public static File findProfileDir(final File mozillaDir, final String profileName) throws NoSuchProfileException { |
michael@0 | 213 | // Open profiles.ini to find the correct path. |
michael@0 | 214 | final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir); |
michael@0 | 215 | |
michael@0 | 216 | for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) { |
michael@0 | 217 | final INISection section = e.nextElement(); |
michael@0 | 218 | final String name = section.getStringProperty("Name"); |
michael@0 | 219 | if (name != null && name.equals(profileName)) { |
michael@0 | 220 | if (section.getIntProperty("IsRelative") == 1) { |
michael@0 | 221 | return new File(mozillaDir, section.getStringProperty("Path")); |
michael@0 | 222 | } |
michael@0 | 223 | return new File(section.getStringProperty("Path")); |
michael@0 | 224 | } |
michael@0 | 225 | } |
michael@0 | 226 | |
michael@0 | 227 | throw new NoSuchProfileException("No profile " + profileName); |
michael@0 | 228 | } |
michael@0 | 229 | } |