src/java.prefs/macosx/native/libprefs/MacOSXPreferencesFile.m
changeset 47216 71c04702a3d5
parent 33653 c1ee09fe3274
child 50471 f0aeede1b855
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.prefs/macosx/native/libprefs/MacOSXPreferencesFile.m	Tue Sep 12 19:03:39 2017 +0200
@@ -0,0 +1,1064 @@
+/*
+ * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+   Hierarchical storage layout:
+
+   <dict>
+     <key>/</key>
+     <dict>
+       <key>foo</key>
+       <string>/foo's value</string>
+       <key>foo/</key>
+       <dict>
+         <key>bar</key>
+         <string>/foo/bar's value</string>
+       </dict>
+     </dict>
+   </dict>
+
+   Java pref nodes are stored in several different files. Pref nodes
+   with at least three components in the node name (e.g. /com/MyCompany/MyApp/)
+   are stored in a CF prefs file with the first three components as the name.
+   This way, all preferences for MyApp end up in com.MyCompany.MyApp.plist .
+   Pref nodes with shorter names are stored in com.apple.java.util.prefs.plist
+
+   The filesystem is assumed to be case-insensitive (like HFS+).
+   Java pref node names are case-sensitive. If two pref node names differ
+   only in case, they may end up in the same pref file. This is ok
+   because the CF keys identifying the node span the entire absolute path
+   to the node and are case-sensitive.
+
+   Java node names may contain '.' . When mapping to the CF file name,
+   these dots are left as-is, even though '/' is mapped to '.' .
+   This is ok because the CF key contains the correct node name.
+*/
+
+
+
+#include <CoreFoundation/CoreFoundation.h>
+
+#include "jni_util.h"
+#include "jlong.h"
+#include "jvm.h"
+
+/*
+ * Declare library specific JNI_Onload entry if static build
+ */
+DEF_STATIC_JNI_OnLoad
+
+
+// Throw an OutOfMemoryError with the given message.
+static void throwOutOfMemoryError(JNIEnv *env, const char *msg)
+{
+    static jclass exceptionClass = NULL;
+    jclass c;
+
+    (*env)->ExceptionClear(env);  // If an exception is pending, clear it before
+                                  // calling FindClass() and/or ThrowNew().
+    if (exceptionClass) {
+        c = exceptionClass;
+    } else {
+        c = (*env)->FindClass(env, "java/lang/OutOfMemoryError");
+        if ((*env)->ExceptionOccurred(env)) return;
+        exceptionClass = (*env)->NewGlobalRef(env, c);
+    }
+
+    (*env)->ThrowNew(env, c, msg);
+}
+
+
+// throwIfNull macro
+// If var is NULL, throw an OutOfMemoryError and goto badvar.
+// var must be a variable. env must be the current JNIEnv.
+// fixme throw BackingStoreExceptions sometimes?
+#define throwIfNull(var, msg) \
+    do { \
+        if (var == NULL) { \
+            throwOutOfMemoryError(env, msg); \
+            goto bad##var; \
+        } \
+    } while (0)
+
+
+// Converts CFNumber, CFBoolean, CFString to CFString
+// returns NULL if value is of some other type
+// throws and returns NULL on memory error
+// result must be released (even if value was already a CFStringRef)
+// value must not be null
+static CFStringRef copyToCFString(JNIEnv *env, CFTypeRef value)
+{
+    CFStringRef result;
+    CFTypeID type;
+
+    type = CFGetTypeID(value);
+
+    if (type == CFStringGetTypeID()) {
+        result = (CFStringRef)CFRetain(value);
+    }
+    else if (type == CFBooleanGetTypeID()) {
+        // Java Preferences API expects "true" and "false" for boolean values.
+        result = CFStringCreateCopy(NULL, (value == kCFBooleanTrue) ? CFSTR("true") : CFSTR("false"));
+        throwIfNull(result, "copyToCFString failed");
+    }
+    else if (type == CFNumberGetTypeID()) {
+        CFNumberRef number = (CFNumberRef) value;
+        if (CFNumberIsFloatType(number)) {
+            double d;
+            CFNumberGetValue(number, kCFNumberDoubleType, &d);
+            result = CFStringCreateWithFormat(NULL, NULL, CFSTR("%g"), d);
+            throwIfNull(result, "copyToCFString failed");
+        }
+        else {
+            long l;
+            CFNumberGetValue(number, kCFNumberLongType, &l);
+            result = CFStringCreateWithFormat(NULL, NULL, CFSTR("%ld"), l);
+            throwIfNull(result, "copyToCFString failed");
+        }
+    }
+    else {
+        // unknown type - return NULL
+        result = NULL;
+    }
+
+ badresult:
+    return result;
+}
+
+
+// Create a Java string from the given CF string.
+// returns NULL if cfString is NULL
+// throws and returns NULL on memory error
+static jstring toJavaString(JNIEnv *env, CFStringRef cfString)
+{
+    if (cfString == NULL) {
+        return NULL;
+    } else {
+        jstring javaString = NULL;
+
+        CFIndex length = CFStringGetLength(cfString);
+        const UniChar *constchars = CFStringGetCharactersPtr(cfString);
+        if (constchars) {
+            javaString = (*env)->NewString(env, constchars, length);
+        } else {
+            UniChar *chars = malloc(length * sizeof(UniChar));
+            throwIfNull(chars, "toJavaString failed");
+            CFStringGetCharacters(cfString, CFRangeMake(0, length), chars);
+            javaString = (*env)->NewString(env, chars, length);
+            free(chars);
+        }
+    badchars:
+        return javaString;
+    }
+}
+
+
+
+// Create a CF string from the given Java string.
+// returns NULL if javaString is NULL
+// throws and returns NULL on memory error
+static CFStringRef toCF(JNIEnv *env, jstring javaString)
+{
+    if (javaString == NULL) {
+        return NULL;
+    } else {
+        CFStringRef result = NULL;
+        jsize length = (*env)->GetStringLength(env, javaString);
+        const jchar *chars = (*env)->GetStringChars(env, javaString, NULL);
+        throwIfNull(chars, "toCF failed");
+        result =
+            CFStringCreateWithCharacters(NULL, (const UniChar *)chars, length);
+        (*env)->ReleaseStringChars(env, javaString, chars);
+        throwIfNull(result, "toCF failed");
+    badchars:
+    badresult:
+        return result;
+    }
+}
+
+
+// Create an empty Java string array of the given size.
+// Throws and returns NULL on error.
+static jarray createJavaStringArray(JNIEnv *env, CFIndex count)
+{
+    static jclass stringClass = NULL;
+    jclass c;
+
+    if (stringClass) {
+        c = stringClass;
+    } else {
+        c = (*env)->FindClass(env, "java/lang/String");
+        if ((*env)->ExceptionOccurred(env)) return NULL;
+        stringClass = (*env)->NewGlobalRef(env, c);
+    }
+
+    return (*env)->NewObjectArray(env, count, c, NULL); // AWT_THREADING Safe (known object)
+}
+
+
+// Java accessors for CF constants.
+JNIEXPORT jlong JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_currentUser(JNIEnv *env,
+                                                       jobject klass)
+{
+    return ptr_to_jlong(kCFPreferencesCurrentUser);
+}
+
+JNIEXPORT jlong JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_anyUser(JNIEnv *env, jobject klass)
+{
+    return ptr_to_jlong(kCFPreferencesAnyUser);
+}
+
+JNIEXPORT jlong JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_currentHost(JNIEnv *env,
+                                                       jobject klass)
+{
+    return ptr_to_jlong(kCFPreferencesCurrentHost);
+}
+
+JNIEXPORT jlong JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_anyHost(JNIEnv *env, jobject klass)
+{
+    return ptr_to_jlong(kCFPreferencesAnyHost);
+}
+
+
+// Create an empty node.
+// Does not store the node in any prefs file.
+// returns NULL on memory error
+static CFMutableDictionaryRef createEmptyNode(void)
+{
+    return CFDictionaryCreateMutable(NULL, 0,
+                                     &kCFTypeDictionaryKeyCallBacks,
+                                     &kCFTypeDictionaryValueCallBacks);
+}
+
+
+// Create a string that consists of path minus its last component.
+// path must end with '/'
+// The result will end in '/' (unless path itself is '/')
+static CFStringRef copyParentOf(CFStringRef path)
+{
+    CFRange searchRange;
+    CFRange slashRange;
+    CFRange parentRange;
+    Boolean found;
+
+    searchRange = CFRangeMake(0, CFStringGetLength(path) - 1);
+    found = CFStringFindWithOptions(path, CFSTR("/"), searchRange,
+                                    kCFCompareBackwards, &slashRange);
+    if (!found) return CFSTR("");
+    parentRange = CFRangeMake(0, slashRange.location + 1); // include '/'
+    return CFStringCreateWithSubstring(NULL, path, parentRange);
+}
+
+
+// Create a string that consists of path's last component.
+// path must end with '/'
+// The result will end in '/'.
+// The result will not start with '/' (unless path itself is '/')
+static CFStringRef copyChildOf(CFStringRef path)
+{
+    CFRange searchRange;
+    CFRange slashRange;
+    CFRange childRange;
+    Boolean found;
+    CFIndex length = CFStringGetLength(path);
+
+    searchRange = CFRangeMake(0, length - 1);
+    found = CFStringFindWithOptions(path, CFSTR("/"), searchRange,
+                                    kCFCompareBackwards, &slashRange);
+    if (!found) return CFSTR("");
+    childRange = CFRangeMake(slashRange.location + 1,
+                             length - slashRange.location - 1); // skip '/'
+    return CFStringCreateWithSubstring(NULL, path, childRange);
+}
+
+
+// Return the first three components of path, with leading and trailing '/'.
+// If path does not have three components, return NULL.
+// path must begin and end in '/'
+static CFStringRef copyFirstThreeComponentsOf(CFStringRef path)
+{
+    CFRange searchRange;
+    CFRange slashRange;
+    CFRange prefixRange;
+    CFStringRef prefix;
+    Boolean found;
+    CFIndex length = CFStringGetLength(path);
+
+    searchRange = CFRangeMake(1, length - 1);  // skip leading '/'
+    found = CFStringFindWithOptions(path, CFSTR("/"), searchRange, 0,
+                                    &slashRange);
+    if (!found) return NULL;  // no second slash!
+
+    searchRange = CFRangeMake(slashRange.location + 1,
+                              length - slashRange.location - 1);
+    found = CFStringFindWithOptions(path, CFSTR("/"), searchRange, 0,
+                                    &slashRange);
+    if (!found) return NULL;  // no third slash!
+
+    searchRange = CFRangeMake(slashRange.location + 1,
+                              length - slashRange.location - 1);
+    found = CFStringFindWithOptions(path, CFSTR("/"), searchRange, 0,
+                                    &slashRange);
+    if (!found) return NULL;  // no fourth slash!
+
+    prefixRange = CFRangeMake(0, slashRange.location + 1); // keep last '/'
+    prefix = CFStringCreateWithSubstring(NULL, path, prefixRange);
+
+    return prefix;
+}
+
+
+// Copy the CFPreferences key and value at the base of path's tree.
+// path must end in '/'
+// topKey or topValue may be NULL
+// Returns NULL on error or if there is no tree for path in this file.
+static void copyTreeForPath(CFStringRef path, CFStringRef name,
+                            CFStringRef user, CFStringRef host,
+                            CFStringRef *topKey, CFDictionaryRef *topValue)
+{
+    CFStringRef key;
+    CFPropertyListRef value;
+
+    if (topKey) *topKey = NULL;
+    if (topValue) *topValue = NULL;
+
+    if (CFEqual(name, CFSTR("com.apple.java.util.prefs"))) {
+        // Top-level file. Only key "/" is an acceptable root.
+        key = (CFStringRef) CFRetain(CFSTR("/"));
+    } else {
+        // Second-level file. Key must be the first three components of path.
+        key = copyFirstThreeComponentsOf(path);
+        if (!key) return;
+    }
+
+    value = CFPreferencesCopyValue(key, name, user, host);
+    if (value) {
+        if (CFGetTypeID(value) == CFDictionaryGetTypeID()) {
+            // (key, value) is acceptable
+            if (topKey) *topKey = (CFStringRef)CFRetain(key);
+            if (topValue) *topValue = (CFDictionaryRef)CFRetain(value);
+        }
+        CFRelease(value);
+    }
+    CFRelease(key);
+}
+
+
+// Find the node for path in the given tree.
+// Returns NULL on error or if path doesn't have a node in this tree.
+// path must end in '/'
+static CFDictionaryRef copyNodeInTree(CFStringRef path, CFStringRef topKey,
+                                      CFDictionaryRef topValue)
+{
+    CFMutableStringRef p;
+    CFDictionaryRef result = NULL;
+
+    p = CFStringCreateMutableCopy(NULL, 0, path);
+    if (!p) return NULL;
+    CFStringDelete(p, CFRangeMake(0, CFStringGetLength(topKey)));
+    result = topValue;
+
+    while (CFStringGetLength(p) > 0) {
+        CFDictionaryRef child;
+        CFStringRef part = NULL;
+        CFRange slashRange = CFStringFind(p, CFSTR("/"), 0);
+        // guaranteed to succeed because path must end in '/'
+        CFRange partRange = CFRangeMake(0, slashRange.location + 1);
+        part = CFStringCreateWithSubstring(NULL, p, partRange);
+        if (!part) { result = NULL; break; }
+        CFStringDelete(p, partRange);
+
+        child = CFDictionaryGetValue(result, part);
+        CFRelease(part);
+        if (child  &&  CFGetTypeID(child) == CFDictionaryGetTypeID()) {
+            // continue search
+            result = child;
+        } else {
+            // didn't find target node
+            result = NULL;
+            break;
+        }
+    }
+
+    CFRelease(p);
+    if (result) return (CFDictionaryRef)CFRetain(result);
+    else return NULL;
+}
+
+
+// Return a retained copy of the node at path from the given file.
+// path must end in '/'
+// returns NULL if node doesn't exist.
+// returns NULL if the value for key "path" isn't a valid node.
+static CFDictionaryRef copyNodeIfPresent(CFStringRef path, CFStringRef name,
+                                         CFStringRef user, CFStringRef host)
+{
+    CFStringRef topKey;
+    CFDictionaryRef topValue;
+    CFDictionaryRef result;
+
+    copyTreeForPath(path, name, user, host, &topKey, &topValue);
+    if (!topKey) return NULL;
+
+    result = copyNodeInTree(path, topKey, topValue);
+
+    CFRelease(topKey);
+    if (topValue) CFRelease(topValue);
+    return result;
+}
+
+
+// Create a new tree that would store path in the given file.
+// Only the root of the tree is created, not all of the links leading to path.
+// returns NULL on error
+static void createTreeForPath(CFStringRef path, CFStringRef name,
+                              CFStringRef user, CFStringRef host,
+                              CFStringRef *outTopKey,
+                              CFMutableDictionaryRef *outTopValue)
+{
+    *outTopKey = NULL;
+    *outTopValue = NULL;
+
+    // if name is "com.apple.java.util.prefs" then create tree "/"
+    // else create tree "/foo/bar/baz/"
+    // "com.apple.java.util.prefs.plist" is also in MacOSXPreferences.java
+    if (CFEqual(name, CFSTR("com.apple.java.util.prefs"))) {
+        *outTopKey = CFSTR("/");
+        *outTopValue = createEmptyNode();
+    } else {
+        CFStringRef prefix = copyFirstThreeComponentsOf(path);
+        if (prefix) {
+            *outTopKey = prefix;
+            *outTopValue = createEmptyNode();
+        }
+    }
+}
+
+
+// Return a mutable copy of the tree containing path and the dict for
+//   path itself. *outTopKey and *outTopValue can be used to write the
+//   modified tree back to the prefs file.
+// *outTopKey and *outTopValue must be released iff the actual return
+//   value is not NULL.
+static CFMutableDictionaryRef
+copyMutableNode(CFStringRef path, CFStringRef name,
+                CFStringRef user, CFStringRef host,
+                CFStringRef *outTopKey,
+                CFMutableDictionaryRef *outTopValue)
+{
+    CFStringRef topKey = NULL;
+    CFDictionaryRef oldTopValue = NULL;
+    CFMutableDictionaryRef topValue;
+    CFMutableDictionaryRef result = NULL;
+    CFMutableStringRef p;
+
+    if (outTopKey) *outTopKey = NULL;
+    if (outTopValue) *outTopValue = NULL;
+
+    copyTreeForPath(path, name, user, host, &topKey, &oldTopValue);
+    if (!topKey) {
+        createTreeForPath(path, name, user, host, &topKey, &topValue);
+    } else {
+        topValue = (CFMutableDictionaryRef)
+            CFPropertyListCreateDeepCopy(NULL, (CFPropertyListRef)oldTopValue,
+                                         kCFPropertyListMutableContainers);
+    }
+    if (!topValue) goto badtopValue;
+
+    p = CFStringCreateMutableCopy(NULL, 0, path);
+    if (!p) goto badp;
+    CFStringDelete(p, CFRangeMake(0, CFStringGetLength(topKey)));
+    result = topValue;
+
+    while (CFStringGetLength(p) > 0) {
+        CFMutableDictionaryRef child;
+        CFStringRef part = NULL;
+        CFRange slashRange = CFStringFind(p, CFSTR("/"), 0);
+        // guaranteed to succeed because path must end in '/'
+        CFRange partRange = CFRangeMake(0, slashRange.location + 1);
+        part = CFStringCreateWithSubstring(NULL, p, partRange);
+        if (!part) { result = NULL; break; }
+        CFStringDelete(p, partRange);
+
+        child = (CFMutableDictionaryRef)CFDictionaryGetValue(result, part);
+        if (child  &&  CFGetTypeID(child) == CFDictionaryGetTypeID()) {
+            // continue search
+            result = child;
+        } else {
+            // didn't find target node - add it and continue
+            child = createEmptyNode();
+            if (!child) { CFRelease(part); result = NULL; break; }
+            CFDictionaryAddValue(result, part, child);
+            result = child;
+        }
+        CFRelease(part);
+    }
+
+    if (result) {
+        *outTopKey = (CFStringRef)CFRetain(topKey);
+        *outTopValue = (CFMutableDictionaryRef)CFRetain(topValue);
+        CFRetain(result);
+    }
+
+    CFRelease(p);
+ badp:
+    CFRelease(topValue);
+ badtopValue:
+    if (topKey) CFRelease(topKey);
+    if (oldTopValue) CFRelease(oldTopValue);
+    return result;
+}
+
+
+JNIEXPORT jboolean JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_addNode
+(JNIEnv *env, jobject klass, jobject jpath,
+ jobject jname, jlong juser, jlong jhost)
+{
+    CFStringRef path = NULL;
+    CFStringRef name = NULL;
+
+    path = toCF(env, jpath);
+    if (path != NULL) {
+        name = toCF(env, jname);
+    }
+    CFStringRef user = (CFStringRef)jlong_to_ptr(juser);
+    CFStringRef host = (CFStringRef)jlong_to_ptr(jhost);
+    CFDictionaryRef node = NULL;
+    jboolean neededNewNode = false;
+
+    if (!path  ||  !name) goto badparams;
+
+    node = copyNodeIfPresent(path, name, user, host);
+
+    if (node) {
+        neededNewNode = false;
+        CFRelease(node);
+    } else {
+        CFStringRef topKey = NULL;
+        CFMutableDictionaryRef topValue = NULL;
+
+        neededNewNode = true;
+
+        // copyMutableNode creates the node if necessary
+        node = copyMutableNode(path, name, user, host, &topKey, &topValue);
+        throwIfNull(node, "copyMutableNode failed");
+
+        CFPreferencesSetValue(topKey, topValue, name, user, host);
+
+        CFRelease(node);
+        if (topKey) CFRelease(topKey);
+        if (topValue) CFRelease(topValue);
+    }
+
+ badnode:
+ badparams:
+    if (path) CFRelease(path);
+    if (name) CFRelease(name);
+
+    return neededNewNode;
+}
+
+
+JNIEXPORT void JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_removeNode
+(JNIEnv *env, jobject klass, jobject jpath,
+ jobject jname, jlong juser, jlong jhost)
+{
+    CFStringRef path = NULL;
+    CFStringRef name = NULL;
+
+    path = toCF(env, jpath);
+    if (path != NULL) {
+        name = toCF(env, jname);
+    }
+    CFStringRef user = (CFStringRef)jlong_to_ptr(juser);
+    CFStringRef host = (CFStringRef)jlong_to_ptr(jhost);
+    CFStringRef parentName;
+    CFStringRef childName;
+    CFDictionaryRef constParent;
+
+    if (!path  ||  !name) goto badparams;
+
+    parentName = copyParentOf(path);
+    throwIfNull(parentName, "copyParentOf failed");
+    childName  = copyChildOf(path);
+    throwIfNull(childName, "copyChildOf failed");
+
+    // root node is not allowed to be removed, so parentName is never empty
+
+    constParent = copyNodeIfPresent(parentName, name, user, host);
+    if (constParent  &&  CFDictionaryContainsKey(constParent, childName)) {
+        CFStringRef topKey;
+        CFMutableDictionaryRef topValue;
+        CFMutableDictionaryRef parent;
+
+        parent = copyMutableNode(parentName, name, user, host,
+                                 &topKey, &topValue);
+        throwIfNull(parent, "copyMutableNode failed");
+
+        CFDictionaryRemoveValue(parent, childName);
+        CFPreferencesSetValue(topKey, topValue, name, user, host);
+
+        CFRelease(parent);
+        if (topKey) CFRelease(topKey);
+        if (topValue) CFRelease(topValue);
+    } else {
+        // might be trying to remove the root itself in a non-root file
+        CFStringRef topKey;
+        CFDictionaryRef topValue;
+        copyTreeForPath(path, name, user, host, &topKey, &topValue);
+        if (topKey) {
+            if (CFEqual(topKey, path)) {
+                CFPreferencesSetValue(topKey, NULL, name, user, host);
+            }
+
+            if (topKey) CFRelease(topKey);
+            if (topValue) CFRelease(topValue);
+        }
+    }
+
+
+ badparent:
+    if (constParent) CFRelease(constParent);
+    CFRelease(childName);
+ badchildName:
+    CFRelease(parentName);
+ badparentName:
+ badparams:
+    if (path) CFRelease(path);
+    if (name) CFRelease(name);
+}
+
+
+// child must end with '/'
+JNIEXPORT Boolean JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_addChildToNode
+(JNIEnv *env, jobject klass, jobject jpath, jobject jchild,
+ jobject jname, jlong juser, jlong jhost)
+{
+    // like addNode, but can put a three-level-deep dict into the root file
+    CFStringRef path = NULL;
+    CFStringRef child = NULL;
+    CFStringRef name = NULL;
+
+    path = toCF(env, jpath);
+    if (path != NULL) {
+        child = toCF(env, jchild);
+    }
+    if (child != NULL) {
+        name = toCF(env, jname);
+    }
+    CFStringRef user = (CFStringRef)jlong_to_ptr(juser);
+    CFStringRef host = (CFStringRef)jlong_to_ptr(jhost);
+    CFMutableDictionaryRef parent;
+    CFDictionaryRef node;
+    CFStringRef topKey;
+    CFMutableDictionaryRef topValue;
+    Boolean beforeAdd = false;
+
+    if (!path  ||  !child  ||  !name) goto badparams;
+
+    node = createEmptyNode();
+    throwIfNull(node, "createEmptyNode failed");
+
+    // copyMutableNode creates the node if necessary
+    parent = copyMutableNode(path, name, user, host, &topKey, &topValue);
+    throwIfNull(parent, "copyMutableNode failed");
+    beforeAdd = CFDictionaryContainsKey(parent, child);
+    CFDictionaryAddValue(parent, child, node);
+    if (!beforeAdd)
+        beforeAdd = CFDictionaryContainsKey(parent, child);
+    else
+        beforeAdd = false;
+    CFPreferencesSetValue(topKey, topValue, name, user, host);
+
+    CFRelease(parent);
+    if (topKey) CFRelease(topKey);
+    if (topValue) CFRelease(topValue);
+ badparent:
+    CFRelease(node);
+ badnode:
+ badparams:
+    if (path) CFRelease(path);
+    if (child) CFRelease(child);
+    if (name) CFRelease(name);
+    return beforeAdd;
+}
+
+
+JNIEXPORT void JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_removeChildFromNode
+(JNIEnv *env, jobject klass, jobject jpath, jobject jchild,
+ jobject jname, jlong juser, jlong jhost)
+{
+    CFStringRef path = NULL;
+    CFStringRef child = NULL;
+    CFStringRef name = NULL;
+
+    path = toCF(env, jpath);
+    if (path != NULL) {
+        child = toCF(env, jchild);
+    }
+    if (child != NULL) {
+        name = toCF(env, jname);
+    }
+    CFStringRef user = (CFStringRef)jlong_to_ptr(juser);
+    CFStringRef host = (CFStringRef)jlong_to_ptr(jhost);
+    CFDictionaryRef constParent;
+
+    if (!path  ||  !child  ||  !name) goto badparams;
+
+    constParent = copyNodeIfPresent(path, name, user, host);
+    if (constParent  &&  CFDictionaryContainsKey(constParent, child)) {
+        CFStringRef topKey;
+        CFMutableDictionaryRef topValue;
+        CFMutableDictionaryRef parent;
+
+        parent = copyMutableNode(path, name, user, host, &topKey, &topValue);
+        throwIfNull(parent, "copyMutableNode failed");
+
+        CFDictionaryRemoveValue(parent, child);
+        CFPreferencesSetValue(topKey, topValue, name, user, host);
+
+        CFRelease(parent);
+        if (topKey) CFRelease(topKey);
+        if (topValue) CFRelease(topValue);
+    }
+
+ badparent:
+    if (constParent) CFRelease(constParent);
+ badparams:
+    if (path) CFRelease(path);
+    if (child) CFRelease(child);
+    if (name) CFRelease(name);
+}
+
+
+
+JNIEXPORT void JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_addKeyToNode
+(JNIEnv *env, jobject klass, jobject jpath, jobject jkey, jobject jvalue,
+ jobject jname, jlong juser, jlong jhost)
+{
+    CFStringRef path = NULL;
+    CFStringRef key = NULL;
+    CFStringRef value = NULL;
+    CFStringRef name = NULL;
+
+    path = toCF(env, jpath);
+    if (path != NULL) {
+        key = toCF(env, jkey);
+    }
+    if (key != NULL) {
+        value = toCF(env, jvalue);
+    }
+    if (value != NULL) {
+        name = toCF(env, jname);
+    }
+    CFStringRef user = (CFStringRef)jlong_to_ptr(juser);
+    CFStringRef host = (CFStringRef)jlong_to_ptr(jhost);
+    CFMutableDictionaryRef node = NULL;
+    CFStringRef topKey;
+    CFMutableDictionaryRef topValue;
+
+    if (!path  ||  !key  || !value  ||  !name) goto badparams;
+
+    // fixme optimization: check whether old value and new value are identical
+    node = copyMutableNode(path, name, user, host, &topKey, &topValue);
+    throwIfNull(node, "copyMutableNode failed");
+
+    CFDictionarySetValue(node, key, value);
+    CFPreferencesSetValue(topKey, topValue, name, user, host);
+
+    CFRelease(node);
+    if (topKey) CFRelease(topKey);
+    if (topValue) CFRelease(topValue);
+
+ badnode:
+ badparams:
+    if (path) CFRelease(path);
+    if (key) CFRelease(key);
+    if (value) CFRelease(value);
+    if (name) CFRelease(name);
+}
+
+
+JNIEXPORT void JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_removeKeyFromNode
+(JNIEnv *env, jobject klass, jobject jpath, jobject jkey,
+ jobject jname, jlong juser, jlong jhost)
+{
+    CFStringRef path = NULL;
+    CFStringRef key = NULL;
+    CFStringRef name = NULL;
+
+    path = toCF(env, jpath);
+    if (path != NULL) {
+        key = toCF(env, jkey);
+    }
+    if (key != NULL) {
+        name = toCF(env, jname);
+    }
+    CFStringRef user = (CFStringRef)jlong_to_ptr(juser);
+    CFStringRef host = (CFStringRef)jlong_to_ptr(jhost);
+    CFDictionaryRef constNode;
+
+    if (!path  ||  !key  ||  !name) goto badparams;
+
+    constNode = copyNodeIfPresent(path, name, user, host);
+    if (constNode  &&  CFDictionaryContainsKey(constNode, key)) {
+        CFStringRef topKey;
+        CFMutableDictionaryRef topValue;
+        CFMutableDictionaryRef node;
+
+        node = copyMutableNode(path, name, user, host, &topKey, &topValue);
+        throwIfNull(node, "copyMutableNode failed");
+
+        CFDictionaryRemoveValue(node, key);
+        CFPreferencesSetValue(topKey, topValue, name, user, host);
+
+        CFRelease(node);
+        if (topKey) CFRelease(topKey);
+        if (topValue) CFRelease(topValue);
+    }
+
+ badnode:
+    if (constNode) CFRelease(constNode);
+ badparams:
+    if (path) CFRelease(path);
+    if (key) CFRelease(key);
+    if (name) CFRelease(name);
+}
+
+
+// path must end in '/'
+JNIEXPORT jstring JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_getKeyFromNode
+(JNIEnv *env, jobject klass, jobject jpath, jobject jkey,
+ jobject jname, jlong juser, jlong jhost)
+{
+    CFStringRef path = NULL;
+    CFStringRef key = NULL;
+    CFStringRef name = NULL;
+
+    path = toCF(env, jpath);
+    if (path != NULL) {
+        key = toCF(env, jkey);
+    }
+    if (key != NULL) {
+        name = toCF(env, jname);
+    }
+    CFStringRef user = (CFStringRef)jlong_to_ptr(juser);
+    CFStringRef host = (CFStringRef)jlong_to_ptr(jhost);
+    CFPropertyListRef value;
+    CFDictionaryRef node;
+    jstring result = NULL;
+
+    if (!path  ||  !key  ||  !name) goto badparams;
+
+    node = copyNodeIfPresent(path, name, user, host);
+    if (node) {
+        value = (CFPropertyListRef)CFDictionaryGetValue(node, key);
+        if (!value) {
+            // key doesn't exist, or other error - no Java errors available
+            result = NULL;
+        } else {
+            CFStringRef cfString = copyToCFString(env, value);
+            if ((*env)->ExceptionOccurred(env)) {
+                // memory error in copyToCFString
+                result = NULL;
+            } else if (cfString == NULL) {
+                // bogus value type in prefs file - no Java errors available
+                result = NULL;
+            } else {
+                // good cfString
+                result = toJavaString(env, cfString);
+                CFRelease(cfString);
+            }
+        }
+        CFRelease(node);
+    }
+
+ badparams:
+    if (path) CFRelease(path);
+    if (key) CFRelease(key);
+    if (name) CFRelease(name);
+
+    return result;
+}
+
+
+typedef struct {
+    jarray result;
+    JNIEnv *env;
+    CFIndex used;
+    Boolean allowSlash;
+} BuildJavaArrayArgs;
+
+// CFDictionary applier function that builds an array of Java strings
+//   from a CFDictionary of CFPropertyListRefs.
+// If args->allowSlash, only strings that end in '/' are added to the array,
+//   with the slash removed. Otherwise, only strings that do not end in '/'
+//   are added.
+// args->result must already exist and be large enough to hold all
+//   strings from the dictionary.
+// After complete application, args->result may not be full because
+//   some of the dictionary values weren't convertible to string. In
+//   this case, args->used will be the count of used elements.
+static void BuildJavaArrayFn(const void *key, const void *value, void *context)
+{
+    BuildJavaArrayArgs *args = (BuildJavaArrayArgs *)context;
+    CFPropertyListRef propkey = (CFPropertyListRef)key;
+    CFStringRef cfString = NULL;
+    JNIEnv *env = args->env;
+
+    if ((*env)->ExceptionOccurred(env)) return; // already failed
+
+    cfString = copyToCFString(env, propkey);
+    if ((*env)->ExceptionOccurred(env)) {
+        // memory error in copyToCFString
+    } else if (!cfString) {
+        // bogus value type in prefs file - no Java errors available
+    } else if (args->allowSlash != CFStringHasSuffix(cfString, CFSTR("/"))) {
+        // wrong suffix - ignore
+    } else {
+        // good cfString
+        jstring javaString;
+        if (args->allowSlash) {
+            CFRange range = CFRangeMake(0, CFStringGetLength(cfString) - 1);
+            CFStringRef s = CFStringCreateWithSubstring(NULL, cfString, range);
+            CFRelease(cfString);
+            cfString = s;
+        }
+        if (CFStringGetLength(cfString) <= 0) goto bad; // ignore empty
+        javaString = toJavaString(env, cfString);
+        if ((*env)->ExceptionOccurred(env)) goto bad;
+        (*env)->SetObjectArrayElement(env, args->result,args->used,javaString);
+        if ((*env)->ExceptionOccurred(env)) goto bad;
+        args->used++;
+    }
+
+ bad:
+    if (cfString) CFRelease(cfString);
+}
+
+
+static jarray getStringsForNode(JNIEnv *env, jobject klass, jobject jpath,
+                                jobject jname, jlong juser, jlong jhost,
+                                Boolean allowSlash)
+{
+    CFStringRef path = NULL;
+    CFStringRef name = NULL;
+
+    path = toCF(env, jpath);
+    if (path != NULL) {
+        name = toCF(env, jname);
+    }
+    CFStringRef user = (CFStringRef)jlong_to_ptr(juser);
+    CFStringRef host = (CFStringRef)jlong_to_ptr(jhost);
+    CFDictionaryRef node;
+    jarray result = NULL;
+    CFIndex count;
+
+    if (!path  ||  !name) goto badparams;
+
+    node = copyNodeIfPresent(path, name, user, host);
+    if (!node) {
+        result = createJavaStringArray(env, 0);
+    } else {
+        count = CFDictionaryGetCount(node);
+        result = createJavaStringArray(env, count);
+        if (result) {
+            BuildJavaArrayArgs args;
+            args.result = result;
+            args.env = env;
+            args.used = 0;
+            args.allowSlash = allowSlash;
+            CFDictionaryApplyFunction(node, BuildJavaArrayFn, &args);
+            if (!(*env)->ExceptionOccurred(env)) {
+                // array construction succeeded
+                if (args.used < count) {
+                    // finished array is smaller than expected.
+                    // Make a new array of precisely the right size.
+                    jarray newresult = createJavaStringArray(env, args.used);
+                    if (newresult) {
+                        JVM_ArrayCopy(env,0, result,0, newresult,0, args.used);
+                        result = newresult;
+                    }
+                }
+            }
+        }
+
+        CFRelease(node);
+    }
+
+ badparams:
+    if (path) CFRelease(path);
+    if (name) CFRelease(name);
+
+    return result;
+}
+
+
+JNIEXPORT jarray JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_getKeysForNode
+(JNIEnv *env, jobject klass, jobject jpath,
+ jobject jname, jlong juser, jlong jhost)
+{
+    return getStringsForNode(env, klass, jpath, jname, juser, jhost, false);
+}
+
+JNIEXPORT jarray JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_getChildrenForNode
+(JNIEnv *env, jobject klass, jobject jpath,
+ jobject jname, jlong juser, jlong jhost)
+{
+    return getStringsForNode(env, klass, jpath, jname, juser, jhost, true);
+}
+
+
+// Returns false on error instead of throwing.
+JNIEXPORT jboolean JNICALL
+Java_java_util_prefs_MacOSXPreferencesFile_synchronize
+(JNIEnv *env, jobject klass,
+ jstring jname, jlong juser, jlong jhost)
+{
+    CFStringRef name = toCF(env, jname);
+    CFStringRef user = (CFStringRef)jlong_to_ptr(juser);
+    CFStringRef host = (CFStringRef)jlong_to_ptr(jhost);
+    jboolean result = 0;
+
+    if (name) {
+        result = CFPreferencesSynchronize(name, user, host);
+        CFRelease(name);
+    }
+
+    return result;
+}