From f3961ba66f1ee716936181478ebca4c8c6ba61ae Mon Sep 17 00:00:00 2001 From: Stu Black Date: Thu, 16 Oct 2025 21:33:47 -0400 Subject: [PATCH] Reimplement the PropList data structure in Rust. While this large change has some unit tests, it has not been integration tested thoroughly. Removing the global case insensitivity flag may be an issue in particular. A few of PropList API functions have been modified (mostly to get rid of varargs). The definition of one such function has been left in C for cleanup later. --- WINGs/WINGs/WUtil.h | 34 +- WINGs/proplist.c | 1834 +----------------------------- WINGs/userdefaults.c | 37 +- WPrefs.app/Appearance.c | 4 +- WPrefs.app/HotCornerShortcuts.c | 2 +- WPrefs.app/Paths.c | 4 +- WPrefs.app/WPrefs.c | 4 +- WPrefs.app/main.c | 7 +- src/appicon.c | 2 +- src/defaults.c | 7 +- src/dialog.c | 2 +- src/dock.c | 20 +- src/menu.c | 5 +- src/screen.c | 9 +- src/session.c | 37 +- src/wdefaults.c | 49 +- src/winspector.c | 15 +- src/workspace.c | 4 +- util/convertfonts.c | 7 +- util/geticonset.c | 2 +- util/getstyle.c | 9 +- util/setstyle.c | 7 +- util/wdwrite.c | 2 +- util/wmsetbg.c | 4 +- wutil-rs/Cargo.toml | 5 + wutil-rs/Makefile.am | 5 +- wutil-rs/src/data.rs | 13 + wutil-rs/src/find_file.rs | 196 +++- wutil-rs/src/lib.rs | 1 + wutil-rs/src/prop_list.rs | 871 ++++++++++++++ wutil-rs/src/prop_list/parser.rs | 1463 ++++++++++++++++++++++++ wutil-rs/src/prop_list/writer.rs | 722 ++++++++++++ 32 files changed, 3417 insertions(+), 1966 deletions(-) create mode 100644 wutil-rs/src/prop_list.rs create mode 100644 wutil-rs/src/prop_list/parser.rs create mode 100644 wutil-rs/src/prop_list/writer.rs diff --git a/WINGs/WINGs/WUtil.h b/WINGs/WINGs/WUtil.h index a4390640..9475e2b4 100644 --- a/WINGs/WINGs/WUtil.h +++ b/WINGs/WINGs/WUtil.h @@ -249,6 +249,9 @@ char* wgethomedir(void); /* ---[ WINGs/proplist.c ]------------------------------------------------ */ +/* + * Creates the directory path and all its parents. + */ int wmkdirhier(const char *path); int wrmdirhier(const char *path); @@ -719,11 +722,13 @@ void WMEnqueueCoalesceNotification(WMNotificationQueue *queue, unsigned coalesceMask); -/* ---[ WINGs/proplist.c ]------------------------------------------------ */ - /* Property Lists handling */ -void WMPLSetCaseSensitive(Bool caseSensitive); +/* ---[ WINGs/proplist.c ]------------------------------------------------ */ + +WMPropList* WMCreatePLArray(WMPropList *elem, ...); + +/* ---[ wutil-rs/src/prop_list.rs ]--------------------------------------- */ WMPropList* WMCreatePLString(const char *str); @@ -731,9 +736,13 @@ WMPropList* WMCreatePLData(WMData *data); WMPropList* WMCreatePLDataWithBytes(const unsigned char *bytes, unsigned int length); -WMPropList* WMCreatePLArray(WMPropList *elem, ...); +WMPropList* WMCreatePLArrayFromSlice(WMPropList *elems, unsigned int length); -WMPropList* WMCreatePLDictionary(WMPropList *key, WMPropList *value, ...); +WMPropList* WMCreateEmptyPLArray(); + +WMPropList* WMCreatePLDictionary(WMPropList *key, WMPropList *value); + +WMPropList* WMCreateEmptyPLDictionary(); WMPropList* WMRetainPropList(WMPropList *plist); @@ -782,14 +791,6 @@ Bool WMIsPropListEqualTo(WMPropList *plist, WMPropList *other); /* Returns a reference. Do not free it! */ char* WMGetFromPLString(WMPropList *plist); -/* Returns a reference. Do not free it! */ -WMData* WMGetFromPLData(WMPropList *plist); - -/* Returns a reference. Do not free it! */ -const unsigned char* WMGetPLDataBytes(WMPropList *plist); - -int WMGetPLDataLength(WMPropList *plist); - /* Returns a reference. */ WMPropList* WMGetFromPLArray(WMPropList *plist, int index); @@ -797,14 +798,9 @@ WMPropList* WMGetFromPLArray(WMPropList *plist, int index); WMPropList* WMGetFromPLDictionary(WMPropList *plist, WMPropList *key); /* Returns a PropList array with all the dictionary keys. Release it when - * you're done. Keys in array are retained from the original dictionary - * not copied and need NOT to be released individually. */ + * you're done. */ WMPropList* WMGetPLDictionaryKeys(WMPropList *plist); -/* Creates only the first level deep object. All the elements inside are - * retained from the original */ -WMPropList* WMShallowCopyPropList(WMPropList *plist); - /* Makes a completely separate replica of the original proplist */ WMPropList* WMDeepCopyPropList(WMPropList *plist); diff --git a/WINGs/proplist.c b/WINGs/proplist.c index 9ee6e3a1..4a574cd6 100644 --- a/WINGs/proplist.c +++ b/WINGs/proplist.c @@ -1,961 +1,21 @@ - -#include -#include - -#include -#include -#include #include -#include -#include -#include -#include -#include #include "WUtil.h" -#include "wconfig.h" - -typedef enum { - WPLString = 0x57504c01, - WPLData = 0x57504c02, - WPLArray = 0x57504c03, - WPLDictionary = 0x57504c04 -} WPLType; - -typedef struct W_PropList { - WPLType type; - - union { - char *string; - WMData *data; - WMArray *array; - WMHashTable *dict; - } d; - - int retainCount; -} W_PropList; - -typedef struct PLData { - const char *ptr; - int pos; - const char *filename; - int lineNumber; -} PLData; - -typedef struct StringBuffer { - char *str; - int size; -} StringBuffer; - -static unsigned hashPropList(const void *param); -static WMPropList *getPLString(PLData * pldata); -static WMPropList *getPLQString(PLData * pldata); -static WMPropList *getPLData(PLData * pldata); -static WMPropList *getPLArray(PLData * pldata); -static WMPropList *getPLDictionary(PLData * pldata); -static WMPropList *getPropList(PLData * pldata); - -typedef Bool(*isEqualFunc) (const void *, const void *); - -static const WMHashTableCallbacks WMPropListHashCallbacks = { - hashPropList, - (isEqualFunc) WMIsPropListEqualTo, - NULL, - NULL -}; - -static Bool caseSensitive = True; - -#define BUFFERSIZE 8192 -#define BUFFERSIZE_INCREMENT 1024 - -#if 0 -# define DPUT(s) puts(s) -#else -# define DPUT(s) -#endif - -#define COMPLAIN(pld, msg) wwarning(_("syntax error in %s %s, line %i: %s"),\ - (pld)->filename ? "file" : "PropList",\ - (pld)->filename ? (pld)->filename : "description",\ - (pld)->lineNumber, msg) - -#define ISSTRINGABLE(c) (isalnum(c) || (c)=='.' || (c)=='_' || (c)=='/' \ - || (c)=='+') - -#define CHECK_BUFFER_SIZE(buf, ptr) \ - if ((ptr) >= (buf).size-1) {\ - (buf).size += BUFFERSIZE_INCREMENT;\ - (buf).str = wrealloc((buf).str, (buf).size);\ - } - -#define inrange(ch, min, max) ((ch)>=(min) && (ch)<=(max)) -#define noquote(ch) (inrange(ch, 'a', 'z') || inrange(ch, 'A', 'Z') || inrange(ch, '0', '9') || ((ch)=='_') || ((ch)=='.') || ((ch)=='$')) -#define charesc(ch) (inrange(ch, 0x07, 0x0c) || ((ch)=='"') || ((ch)=='\\')) -#define numesc(ch) (((ch)<=0x06) || inrange(ch, 0x0d, 0x1f) || ((ch)>0x7e)) -#define ishexdigit(ch) (inrange(ch, 'a', 'f') || inrange(ch, 'A', 'F') || inrange(ch, '0', '9')) -#define char2num(ch) (inrange(ch,'0','9') ? ((ch)-'0') : (inrange(ch,'a','f') ? ((ch)-0x57) : ((ch)-0x37))) -#define num2char(num) ((num) < 0xa ? ((num)+'0') : ((num)+0x57)) - -#define MaxHashLength 64 - -static unsigned hashPropList(const void *param) -{ - WMPropList *plist= (WMPropList *) param; - unsigned ret = 0; - unsigned ctr = 0; - const char *key; - int i, len; - - switch (plist->type) { - case WPLString: - key = plist->d.string; - len = WMIN(strlen(key), MaxHashLength); - for (i = 0; i < len; i++) { - ret ^= tolower(key[i]) << ctr; - ctr = (ctr + 1) % sizeof(char *); - } - /*while (*key) { - ret ^= tolower(*key++) << ctr; - ctr = (ctr + 1) % sizeof (char *); - } */ - break; - - case WPLData: - key = WMDataBytes(plist->d.data); - len = WMIN(WMGetDataLength(plist->d.data), MaxHashLength); - for (i = 0; i < len; i++) { - ret ^= key[i] << ctr; - ctr = (ctr + 1) % sizeof(char *); - } - break; - - default: - wwarning(_("Only string or data is supported for a proplist dictionary key")); - wassertrv(False, 0); - break; - } - - return ret; -} - -static WMPropList *retainPropListByCount(WMPropList * plist, int count) -{ - WMPropList *key, *value; - WMHashEnumerator e; - int i; - - plist->retainCount += count; - - switch (plist->type) { - case WPLString: - case WPLData: - break; - case WPLArray: - for (i = 0; i < WMGetArrayItemCount(plist->d.array); i++) { - retainPropListByCount(WMGetFromArray(plist->d.array, i), count); - } - break; - case WPLDictionary: - e = WMEnumerateHashTable(plist->d.dict); - while (WMNextHashEnumeratorItemAndKey(&e, (void **)&value, (void **)&key)) { - retainPropListByCount(key, count); - retainPropListByCount(value, count); - } - break; - default: - wwarning(_("Used proplist functions on non-WMPropLists objects")); - wassertrv(False, NULL); - break; - } - - return plist; -} - -static void releasePropListByCount(WMPropList * plist, int count) -{ - WMPropList *key, *value; - WMHashEnumerator e; - int i; - - plist->retainCount -= count; - - switch (plist->type) { - case WPLString: - if (plist->retainCount < 1) { - wfree(plist->d.string); - wfree(plist); - } - break; - case WPLData: - if (plist->retainCount < 1) { - WMReleaseData(plist->d.data); - wfree(plist); - } - break; - case WPLArray: - for (i = 0; i < WMGetArrayItemCount(plist->d.array); i++) { - releasePropListByCount(WMGetFromArray(plist->d.array, i), count); - } - if (plist->retainCount < 1) { - WMFreeArray(plist->d.array); - wfree(plist); - } - break; - case WPLDictionary: - e = WMEnumerateHashTable(plist->d.dict); - while (WMNextHashEnumeratorItemAndKey(&e, (void **)&value, (void **)&key)) { - releasePropListByCount(key, count); - releasePropListByCount(value, count); - } - if (plist->retainCount < 1) { - WMFreeHashTable(plist->d.dict); - wfree(plist); - } - break; - default: - wwarning(_("Used proplist functions on non-WMPropLists objects")); - wassertr(False); - break; - } -} - -static char *dataDescription(WMPropList * plist) -{ - const unsigned char *data; - char *retVal; - int i, j, length; - - data = WMDataBytes(plist->d.data); - length = WMGetDataLength(plist->d.data); - - retVal = (char *)wmalloc(2 * length + length / 4 + 3); - - retVal[0] = '<'; - for (i = 0, j = 1; i < length; i++) { - retVal[j++] = num2char((data[i] >> 4) & 0x0f); - retVal[j++] = num2char(data[i] & 0x0f); - if ((i & 0x03) == 3 && i != length - 1) { - /* if we've just finished a 32-bit int, add a space */ - retVal[j++] = ' '; - } - } - retVal[j++] = '>'; - retVal[j] = '\0'; - - return retVal; -} - -static char *stringDescription(WMPropList * plist) -{ - const char *str; - char *retVal, *sPtr, *dPtr; - int len, quote; - unsigned char ch; - - str = plist->d.string; - - if (strlen(str) == 0) { - return wstrdup("\"\""); - } - - /* FIXME: make this work with unichars. */ - - quote = 0; - sPtr = (char *)str; - len = 0; - while ((ch = *sPtr)) { - if (!noquote(ch)) { - quote = 1; - if (charesc(ch)) - len++; - else if (numesc(ch)) - len += 3; - } - sPtr++; - len++; - } - - if (quote) - len += 2; - - retVal = (char *)wmalloc(len + 1); - - sPtr = (char *)str; - dPtr = retVal; - - if (quote) - *dPtr++ = '"'; - - while ((ch = *sPtr)) { - if (charesc(ch)) { - *(dPtr++) = '\\'; - switch (ch) { - case '\a': - *dPtr = 'a'; - break; - case '\b': - *dPtr = 'b'; - break; - case '\t': - *dPtr = 't'; - break; - case '\n': - *dPtr = 'n'; - break; - case '\v': - *dPtr = 'v'; - break; - case '\f': - *dPtr = 'f'; - break; - default: - *dPtr = ch; /* " or \ */ - } - } else if (numesc(ch)) { - *(dPtr++) = '\\'; - *(dPtr++) = '0' + ((ch >> 6) & 07); - *(dPtr++) = '0' + ((ch >> 3) & 07); - *dPtr = '0' + (ch & 07); - } else { - *dPtr = ch; - } - sPtr++; - dPtr++; - } - - if (quote) - *dPtr++ = '"'; - - *dPtr = '\0'; - - return retVal; -} - -static char *description(WMPropList * plist) -{ - WMPropList *key, *val; - char *retstr = NULL; - char *str, *tmp, *skey, *sval; - WMHashEnumerator e; - int i; - - switch (plist->type) { - case WPLString: - retstr = stringDescription(plist); - break; - case WPLData: - retstr = dataDescription(plist); - break; - case WPLArray: - retstr = wstrdup("("); - for (i = 0; i < WMGetArrayItemCount(plist->d.array); i++) { - str = description(WMGetFromArray(plist->d.array, i)); - if (i == 0) { - retstr = wstrappend(retstr, str); - } else { - tmp = (char *)wmalloc(strlen(retstr) + strlen(str) + 3); - sprintf(tmp, "%s, %s", retstr, str); - wfree(retstr); - retstr = tmp; - } - wfree(str); - } - retstr = wstrappend(retstr, ")"); - break; - case WPLDictionary: - retstr = wstrdup("{"); - e = WMEnumerateHashTable(plist->d.dict); - while (WMNextHashEnumeratorItemAndKey(&e, (void **)&val, (void **)&key)) { - skey = description(key); - sval = description(val); - tmp = (char *)wmalloc(strlen(retstr) + strlen(skey) + strlen(sval) + 5); - sprintf(tmp, "%s%s = %s;", retstr, skey, sval); - wfree(skey); - wfree(sval); - wfree(retstr); - retstr = tmp; - } - retstr = wstrappend(retstr, "}"); - break; - default: - wwarning(_("Used proplist functions on non-WMPropLists objects")); - wassertrv(False, NULL); - break; - } - - return retstr; -} - -static char *indentedDescription(WMPropList * plist, int level) -{ - WMPropList *key, *val; - char *retstr = NULL; - char *str, *tmp, *skey, *sval; - WMHashEnumerator e; - int i; - - if (plist->type == WPLArray /* || plist->type==WPLDictionary */ ) { - retstr = description(plist); - - if (retstr && ((2 * (level + 1) + strlen(retstr)) <= 77)) { - return retstr; - } else if (retstr) { - wfree(retstr); - retstr = NULL; - } - } - - switch (plist->type) { - case WPLString: - retstr = stringDescription(plist); - break; - case WPLData: - retstr = dataDescription(plist); - break; - case WPLArray: - retstr = wstrdup("(\n"); - for (i = 0; i < WMGetArrayItemCount(plist->d.array); i++) { - str = indentedDescription(WMGetFromArray(plist->d.array, i), level + 1); - if (i == 0) { - tmp = (char *)wmalloc(2 * (level + 1) + strlen(retstr) + strlen(str) + 1); - sprintf(tmp, "%s%*s%s", retstr, 2 * (level + 1), "", str); - wfree(retstr); - retstr = tmp; - } else { - tmp = (char *)wmalloc(2 * (level + 1) + strlen(retstr) + strlen(str) + 3); - sprintf(tmp, "%s,\n%*s%s", retstr, 2 * (level + 1), "", str); - wfree(retstr); - retstr = tmp; - } - wfree(str); - } - tmp = (char *)wmalloc(strlen(retstr) + 2 * level + 3); - sprintf(tmp, "%s\n%*s)", retstr, 2 * level, ""); - wfree(retstr); - retstr = tmp; - break; - case WPLDictionary: - retstr = wstrdup("{\n"); - e = WMEnumerateHashTable(plist->d.dict); - while (WMNextHashEnumeratorItemAndKey(&e, (void **)&val, (void **)&key)) { - skey = indentedDescription(key, level + 1); - sval = indentedDescription(val, level + 1); - tmp = (char *)wmalloc(2 * (level + 1) + strlen(retstr) + strlen(skey) - + strlen(sval) + 6); - sprintf(tmp, "%s%*s%s = %s;\n", retstr, 2 * (level + 1), "", skey, sval); - wfree(skey); - wfree(sval); - wfree(retstr); - retstr = tmp; - } - tmp = (char *)wmalloc(strlen(retstr) + 2 * level + 2); - sprintf(tmp, "%s%*s}", retstr, 2 * level, ""); - wfree(retstr); - retstr = tmp; - break; - default: - wwarning(_("Used proplist functions on non-WMPropLists objects")); - wassertrv(False, NULL); - break; - } - - return retstr; -} - -static inline int getChar(PLData * pldata) -{ - int c; - - c = pldata->ptr[pldata->pos]; - if (c == 0) { - return 0; - } - - pldata->pos++; - - if (c == '\n') - pldata->lineNumber++; - - return c; -} - -static inline int getNonSpaceChar(PLData * pldata) -{ - int c; - - while (1) { - c = pldata->ptr[pldata->pos]; - if (c == 0) { - break; - } - pldata->pos++; - if (c == '\n') { - pldata->lineNumber++; - } else if (!isspace(c)) { - break; - } - } - - return c; -} - -static char *unescapestr(const char *src) -{ - char *dest = wmalloc(strlen(src) + 1); - char *dPtr; - char ch; - - for (dPtr = dest; ; dPtr++) { - ch = *src++; - if (ch == '\0') - break; - else if (ch != '\\') - *dPtr = ch; - else { - ch = *(src++); - if (ch == '\0') { - *dPtr = '\\'; - break; - } else if ((ch >= '0') && (ch <= '7')) { - char wch; - - /* Convert octal number to character */ - wch = (ch & 07); - ch = *src; - if ((ch >= '0') && (ch <= '7')) { - src++; - wch = (wch << 3) | (ch & 07); - ch = *src; - if ((ch >= '0') && (ch <= '7')) { - src++; - wch = (wch << 3) | (ch & 07); - } - } - *dPtr = wch; - } else { - switch (ch) { - case 'a': - *dPtr = '\a'; - break; - case 'b': - *dPtr = '\b'; - break; - case 't': - *dPtr = '\t'; - break; - case 'r': - *dPtr = '\r'; - break; - case 'n': - *dPtr = '\n'; - break; - case 'v': - *dPtr = '\v'; - break; - case 'f': - *dPtr = '\f'; - break; - default: - *dPtr = ch; - } - } - } - } - - *dPtr = 0; - - return dest; -} - -static WMPropList *getPLString(PLData * pldata) -{ - WMPropList *plist; - StringBuffer sBuf; - int ptr = 0; - int c; - - sBuf.str = wmalloc(BUFFERSIZE); - sBuf.size = BUFFERSIZE; - - while (1) { - c = getChar(pldata); - if (ISSTRINGABLE(c)) { - CHECK_BUFFER_SIZE(sBuf, ptr); - sBuf.str[ptr++] = c; - } else { - if (c != 0) { - pldata->pos--; - } - break; - } - } - - sBuf.str[ptr] = 0; - - if (ptr == 0) { - plist = NULL; - } else { - char *tmp = unescapestr(sBuf.str); - plist = WMCreatePLString(tmp); - wfree(tmp); - } - - wfree(sBuf.str); - - return plist; -} - -static WMPropList *getPLQString(PLData * pldata) -{ - WMPropList *plist; - int ptr = 0, escaping = 0, ok = 1; - int c; - StringBuffer sBuf; - - sBuf.str = wmalloc(BUFFERSIZE); - sBuf.size = BUFFERSIZE; - - while (1) { - c = getChar(pldata); - if (!escaping) { - if (c == '\\') { - escaping = 1; - continue; - } else if (c == '"') { - break; - } - } else { - CHECK_BUFFER_SIZE(sBuf, ptr); - sBuf.str[ptr++] = '\\'; - escaping = 0; - } - - if (c == 0) { - COMPLAIN(pldata, _("unterminated PropList string")); - ok = 0; - break; - } else { - CHECK_BUFFER_SIZE(sBuf, ptr); - sBuf.str[ptr++] = c; - } - } - - sBuf.str[ptr] = 0; - - if (!ok) { - plist = NULL; - } else { - char *tmp = unescapestr(sBuf.str); - plist = WMCreatePLString(tmp); - wfree(tmp); - } - - wfree(sBuf.str); - - return plist; -} - -static WMPropList *getPLData(PLData * pldata) -{ - int ok = 1; - int len = 0; - int c1, c2; - unsigned char buf[BUFFERSIZE], byte; - WMPropList *plist; - WMData *data; - - data = WMCreateDataWithCapacity(0); - - while (1) { - c1 = getNonSpaceChar(pldata); - if (c1 == 0) { - COMPLAIN(pldata, _("unterminated PropList data")); - ok = 0; - break; - } else if (c1 == '>') { - break; - } else if (ishexdigit(c1)) { - c2 = getNonSpaceChar(pldata); - if (c2 == 0 || c2 == '>') { - COMPLAIN(pldata, _("unterminated PropList data (missing hexdigit)")); - ok = 0; - break; - } else if (ishexdigit(c2)) { - byte = char2num(c1) << 4; - byte |= char2num(c2); - buf[len++] = byte; - if (len == sizeof(buf)) { - WMAppendDataBytes(data, buf, len); - len = 0; - } - } else { - COMPLAIN(pldata, _("non hexdigit character in PropList data")); - ok = 0; - break; - } - } else { - COMPLAIN(pldata, _("non hexdigit character in PropList data")); - ok = 0; - break; - } - } - - if (!ok) { - WMReleaseData(data); - return NULL; - } - - if (len > 0) - WMAppendDataBytes(data, buf, len); - - plist = WMCreatePLData(data); - WMReleaseData(data); - - return plist; -} - -static WMPropList *getPLArray(PLData * pldata) -{ - Bool first = True; - int ok = 1; - int c; - WMPropList *array, *obj; - - array = WMCreatePLArray(NULL); - - while (1) { - c = getNonSpaceChar(pldata); - if (c == 0) { - COMPLAIN(pldata, _("unterminated PropList array")); - ok = 0; - break; - } else if (c == ')') { - break; - } else if (c == ',') { - /* continue normally */ - } else if (!first) { - COMPLAIN(pldata, _("missing or unterminated PropList array")); - ok = 0; - break; - } else { - pldata->pos--; - } - first = False; - - obj = getPropList(pldata); - if (!obj) { - COMPLAIN(pldata, _("could not get PropList array element")); - ok = 0; - break; - } - WMAddToPLArray(array, obj); - WMReleasePropList(obj); - } - - if (!ok) { - WMReleasePropList(array); - array = NULL; - } - - return array; -} - -static WMPropList *getPLDictionary(PLData * pldata) -{ - int ok = 1; - int c; - WMPropList *dict, *key, *value; - - dict = WMCreatePLDictionary(NULL, NULL); - - while (1) { - c = getNonSpaceChar(pldata); - if (c == 0) { - COMPLAIN(pldata, _("unterminated PropList dictionary")); - ok = 0; - break; - } else if (c == '}') { - break; - } - - DPUT("getting PropList dictionary key"); - if (c == '<') { - key = getPLData(pldata); - } else if (c == '"') { - key = getPLQString(pldata); - } else if (ISSTRINGABLE(c)) { - pldata->pos--; - key = getPLString(pldata); - } else { - if (c == '=') { - COMPLAIN(pldata, _("missing PropList dictionary key")); - } else { - COMPLAIN(pldata, _("missing PropList dictionary entry key " - "or unterminated dictionary")); - } - ok = 0; - break; - } - - if (!key) { - COMPLAIN(pldata, _("error parsing PropList dictionary key")); - ok = 0; - break; - } - - c = getNonSpaceChar(pldata); - if (c != '=') { - WMReleasePropList(key); - COMPLAIN(pldata, _("missing = in PropList dictionary entry")); - ok = 0; - break; - } - - DPUT("getting PropList dictionary entry value for key"); - value = getPropList(pldata); - if (!value) { - COMPLAIN(pldata, _("error parsing PropList dictionary entry value")); - WMReleasePropList(key); - ok = 0; - break; - } - - c = getNonSpaceChar(pldata); - if (c != ';') { - COMPLAIN(pldata, _("missing ; in PropList dictionary entry")); - WMReleasePropList(key); - WMReleasePropList(value); - ok = 0; - break; - } - - WMPutInPLDictionary(dict, key, value); - WMReleasePropList(key); - WMReleasePropList(value); - } - - if (!ok) { - WMReleasePropList(dict); - dict = NULL; - } - - return dict; -} - -static WMPropList *getPropList(PLData * pldata) -{ - WMPropList *plist; - int c; - - c = getNonSpaceChar(pldata); - - switch (c) { - case 0: - DPUT("End of PropList"); - plist = NULL; - break; - - case '{': - DPUT("Getting PropList dictionary"); - plist = getPLDictionary(pldata); - break; - - case '(': - DPUT("Getting PropList array"); - plist = getPLArray(pldata); - break; - - case '<': - DPUT("Getting PropList data"); - plist = getPLData(pldata); - break; - - case '"': - DPUT("Getting PropList quoted string"); - plist = getPLQString(pldata); - break; - - default: - if (ISSTRINGABLE(c)) { - DPUT("Getting PropList string"); - pldata->pos--; - plist = getPLString(pldata); - } else { - COMPLAIN(pldata, _("was expecting a string, data, array or " - "dictionary. If it's a string, try enclosing " "it with \".")); - if (c == '#' || c == '/') { - wwarning(_("Comments are not allowed inside WindowMaker owned" " domain files.")); - } - plist = NULL; - } - break; - } - - return plist; -} - -void WMPLSetCaseSensitive(Bool caseSensitiveness) -{ - caseSensitive = caseSensitiveness; -} - -WMPropList *WMCreatePLString(const char *str) -{ - WMPropList *plist; - - wassertrv(str != NULL, NULL); - - plist = (WMPropList *) wmalloc(sizeof(W_PropList)); - plist->type = WPLString; - plist->d.string = wstrdup(str); - plist->retainCount = 1; - - return plist; -} - -WMPropList *WMCreatePLData(WMData * data) -{ - WMPropList *plist; - - wassertrv(data != NULL, NULL); - - plist = (WMPropList *) wmalloc(sizeof(W_PropList)); - plist->type = WPLData; - plist->d.data = WMRetainData(data); - plist->retainCount = 1; - - return plist; -} - -WMPropList *WMCreatePLDataWithBytes(const unsigned char *bytes, unsigned int length) -{ - WMPropList *plist; - - wassertrv(bytes != NULL, NULL); - - plist = (WMPropList *) wmalloc(sizeof(W_PropList)); - plist->type = WPLData; - plist->d.data = WMCreateDataWithBytes(bytes, length); - plist->retainCount = 1; - - return plist; -} +/* + * This should be written in Rust whenever va_args support improves. + */ WMPropList *WMCreatePLArray(WMPropList * elem, ...) { WMPropList *plist, *nelem; va_list ap; - plist = (WMPropList *) wmalloc(sizeof(W_PropList)); - plist->type = WPLArray; - plist->d.array = WMCreateArray(4); - plist->retainCount = 1; + plist = WMCreateEmptyPLArray(); if (!elem) return plist; - WMAddToArray(plist->d.array, WMRetainPropList(elem)); + WMAddToPLArray(plist, elem); va_start(ap, elem); @@ -965,888 +25,6 @@ WMPropList *WMCreatePLArray(WMPropList * elem, ...) va_end(ap); return plist; } - WMAddToArray(plist->d.array, WMRetainPropList(nelem)); + WMAddToPLArray(plist, elem); } } - -WMPropList *WMCreatePLDictionary(WMPropList * key, WMPropList * value, ...) -{ - WMPropList *plist, *nkey, *nvalue, *k, *v; - va_list ap; - - plist = (WMPropList *) wmalloc(sizeof(W_PropList)); - plist->type = WPLDictionary; - plist->d.dict = WMCreateHashTable(WMPropListHashCallbacks); - plist->retainCount = 1; - - if (!key || !value) - return plist; - - WMHashInsert(plist->d.dict, WMRetainPropList(key), WMRetainPropList(value)); - - va_start(ap, value); - - while (1) { - nkey = va_arg(ap, WMPropList *); - if (!nkey) { - va_end(ap); - return plist; - } - nvalue = va_arg(ap, WMPropList *); - if (!nvalue) { - va_end(ap); - return plist; - } - if (WMHashGetItemAndKey(plist->d.dict, nkey, (void **)&v, (void **)&k)) { - WMHashRemove(plist->d.dict, k); - WMReleasePropList(k); - WMReleasePropList(v); - } - WMHashInsert(plist->d.dict, WMRetainPropList(nkey), WMRetainPropList(nvalue)); - } -} - -WMPropList *WMRetainPropList(WMPropList * plist) -{ - WMPropList *key, *value; - WMHashEnumerator e; - int i; - - plist->retainCount++; - - switch (plist->type) { - case WPLString: - case WPLData: - break; - case WPLArray: - for (i = 0; i < WMGetArrayItemCount(plist->d.array); i++) { - WMRetainPropList(WMGetFromArray(plist->d.array, i)); - } - break; - case WPLDictionary: - e = WMEnumerateHashTable(plist->d.dict); - while (WMNextHashEnumeratorItemAndKey(&e, (void **)&value, (void **)&key)) { - WMRetainPropList(key); - WMRetainPropList(value); - } - break; - default: - wwarning(_("Used proplist functions on non-WMPropLists objects")); - wassertrv(False, NULL); - break; - } - - return plist; -} - -void WMReleasePropList(WMPropList * plist) -{ - WMPropList *key, *value; - WMHashEnumerator e; - int i; - - plist->retainCount--; - - switch (plist->type) { - case WPLString: - if (plist->retainCount < 1) { - wfree(plist->d.string); - wfree(plist); - } - break; - case WPLData: - if (plist->retainCount < 1) { - WMReleaseData(plist->d.data); - wfree(plist); - } - break; - case WPLArray: - for (i = 0; i < WMGetArrayItemCount(plist->d.array); i++) { - WMReleasePropList(WMGetFromArray(plist->d.array, i)); - } - if (plist->retainCount < 1) { - WMFreeArray(plist->d.array); - wfree(plist); - } - break; - case WPLDictionary: - e = WMEnumerateHashTable(plist->d.dict); - while (WMNextHashEnumeratorItemAndKey(&e, (void **)&value, (void **)&key)) { - WMReleasePropList(key); - WMReleasePropList(value); - } - if (plist->retainCount < 1) { - WMFreeHashTable(plist->d.dict); - wfree(plist); - } - break; - default: - wwarning(_("Used proplist functions on non-WMPropLists objects")); - wassertr(False); - break; - } -} - -void WMInsertInPLArray(WMPropList * plist, int index, WMPropList * item) -{ - wassertr(plist->type == WPLArray); - - retainPropListByCount(item, plist->retainCount); - WMInsertInArray(plist->d.array, index, item); -} - -void WMAddToPLArray(WMPropList * plist, WMPropList * item) -{ - wassertr(plist->type == WPLArray); - - retainPropListByCount(item, plist->retainCount); - WMAddToArray(plist->d.array, item); -} - -void WMDeleteFromPLArray(WMPropList * plist, int index) -{ - WMPropList *item; - - wassertr(plist->type == WPLArray); - - item = WMGetFromArray(plist->d.array, index); - if (item != NULL) { - WMDeleteFromArray(plist->d.array, index); - releasePropListByCount(item, plist->retainCount); - } -} - -void WMRemoveFromPLArray(WMPropList * plist, WMPropList * item) -{ - WMPropList *iPtr; - int i; - - wassertr(plist->type == WPLArray); - - for (i = 0; i < WMGetArrayItemCount(plist->d.array); i++) { - iPtr = WMGetFromArray(plist->d.array, i); - if (WMIsPropListEqualTo(item, iPtr)) { - WMDeleteFromArray(plist->d.array, i); - releasePropListByCount(iPtr, plist->retainCount); - break; - } - } -} - -void WMPutInPLDictionary(WMPropList * plist, WMPropList * key, WMPropList * value) -{ - wassertr(plist->type == WPLDictionary); - - /*WMRetainPropList(key); */ - WMRemoveFromPLDictionary(plist, key); - retainPropListByCount(key, plist->retainCount); - retainPropListByCount(value, plist->retainCount); - WMHashInsert(plist->d.dict, key, value); - /*WMReleasePropList(key); */ -} - -void WMRemoveFromPLDictionary(WMPropList * plist, WMPropList * key) -{ - WMPropList *k, *v; - - wassertr(plist->type == WPLDictionary); - - if (WMHashGetItemAndKey(plist->d.dict, key, (void **)&v, (void **)&k)) { - WMHashRemove(plist->d.dict, k); - releasePropListByCount(k, plist->retainCount); - releasePropListByCount(v, plist->retainCount); - } -} - -WMPropList *WMMergePLDictionaries(WMPropList * dest, WMPropList * source, Bool recursive) -{ - WMPropList *key, *value, *dvalue; - WMHashEnumerator e; - - wassertrv(source->type == WPLDictionary && dest->type == WPLDictionary, NULL); - - if (source == dest) - return dest; - - e = WMEnumerateHashTable(source->d.dict); - while (WMNextHashEnumeratorItemAndKey(&e, (void **)&value, (void **)&key)) { - if (recursive && value->type == WPLDictionary) { - dvalue = WMHashGet(dest->d.dict, key); - if (dvalue && dvalue->type == WPLDictionary) { - WMMergePLDictionaries(dvalue, value, True); - } else { - WMPutInPLDictionary(dest, key, value); - } - } else { - WMPutInPLDictionary(dest, key, value); - } - } - - return dest; -} - -WMPropList *WMSubtractPLDictionaries(WMPropList * dest, WMPropList * source, Bool recursive) -{ - WMPropList *key, *value, *dvalue; - WMHashEnumerator e; - - wassertrv(source->type == WPLDictionary && dest->type == WPLDictionary, NULL); - - if (source == dest) { - WMPropList *keys = WMGetPLDictionaryKeys(dest); - int i; - - for (i = 0; i < WMGetArrayItemCount(keys->d.array); i++) { - WMRemoveFromPLDictionary(dest, WMGetFromArray(keys->d.array, i)); - } - WMReleasePropList(keys); - return dest; - } - - e = WMEnumerateHashTable(source->d.dict); - while (WMNextHashEnumeratorItemAndKey(&e, (void **)&value, (void **)&key)) { - dvalue = WMHashGet(dest->d.dict, key); - if (!dvalue) - continue; - if (WMIsPropListEqualTo(value, dvalue)) { - WMRemoveFromPLDictionary(dest, key); - } else if (recursive && value->type == WPLDictionary && dvalue->type == WPLDictionary) { - WMSubtractPLDictionaries(dvalue, value, True); - } - } - - return dest; -} - -int WMGetPropListItemCount(WMPropList * plist) -{ - switch (plist->type) { - case WPLString: - case WPLData: - return 0; /* should this be 1 instead? */ - case WPLArray: - return WMGetArrayItemCount(plist->d.array); - case WPLDictionary: - return (int)WMCountHashTable(plist->d.dict); - default: - wwarning(_("Used proplist functions on non-WMPropLists objects")); - wassertrv(False, 0); - break; - } - - return 0; -} - -Bool WMIsPLString(WMPropList * plist) -{ - if (plist) - return (plist->type == WPLString); - else - return False; -} - -Bool WMIsPLData(WMPropList * plist) -{ - if (plist) - return (plist->type == WPLData); - else - return False; -} - -Bool WMIsPLArray(WMPropList * plist) -{ - if (plist) - return (plist->type == WPLArray); - else - return False; -} - -Bool WMIsPLDictionary(WMPropList * plist) -{ - if (plist) - return (plist->type == WPLDictionary); - else - return False; -} - -Bool WMIsPropListEqualTo(WMPropList * plist, WMPropList * other) -{ - WMPropList *key1, *item1, *item2; - WMHashEnumerator enumerator; - int n, i; - - if (plist->type != other->type) - return False; - - switch (plist->type) { - case WPLString: - if (caseSensitive) { - return (strcmp(plist->d.string, other->d.string) == 0); - } else { - return (strcasecmp(plist->d.string, other->d.string) == 0); - } - case WPLData: - return WMIsDataEqualToData(plist->d.data, other->d.data); - case WPLArray: - n = WMGetArrayItemCount(plist->d.array); - if (n != WMGetArrayItemCount(other->d.array)) - return False; - for (i = 0; i < n; i++) { - item1 = WMGetFromArray(plist->d.array, i); - item2 = WMGetFromArray(other->d.array, i); - if (!WMIsPropListEqualTo(item1, item2)) - return False; - } - return True; - case WPLDictionary: - if (WMCountHashTable(plist->d.dict) != WMCountHashTable(other->d.dict)) - return False; - enumerator = WMEnumerateHashTable(plist->d.dict); - while (WMNextHashEnumeratorItemAndKey(&enumerator, (void **)&item1, (void **)&key1)) { - item2 = WMHashGet(other->d.dict, key1); - if (!item2 || !item1 || !WMIsPropListEqualTo(item1, item2)) - return False; - } - return True; - default: - wwarning(_("Used proplist functions on non-WMPropLists objects")); - wassertrv(False, False); - break; - } - - return False; -} - -char *WMGetFromPLString(WMPropList * plist) -{ - wassertrv(plist->type == WPLString, NULL); - - return plist->d.string; -} - -WMData *WMGetFromPLData(WMPropList * plist) -{ - wassertrv(plist->type == WPLData, NULL); - - return plist->d.data; -} - -const unsigned char *WMGetPLDataBytes(WMPropList * plist) -{ - wassertrv(plist->type == WPLData, NULL); - - return WMDataBytes(plist->d.data); -} - -int WMGetPLDataLength(WMPropList * plist) -{ - wassertrv(plist->type == WPLData, 0); - - return WMGetDataLength(plist->d.data); -} - -WMPropList *WMGetFromPLArray(WMPropList * plist, int index) -{ - wassertrv(plist->type == WPLArray, NULL); - - return WMGetFromArray(plist->d.array, index); -} - -WMPropList *WMGetFromPLDictionary(WMPropList * plist, WMPropList * key) -{ - wassertrv(plist->type == WPLDictionary, NULL); - - return WMHashGet(plist->d.dict, key); -} - -WMPropList *WMGetPLDictionaryKeys(WMPropList * plist) -{ - WMPropList *array, *key; - WMHashEnumerator enumerator; - - wassertrv(plist->type == WPLDictionary, NULL); - - array = (WMPropList *) wmalloc(sizeof(W_PropList)); - array->type = WPLArray; - array->d.array = WMCreateArray(WMCountHashTable(plist->d.dict)); - array->retainCount = 1; - - enumerator = WMEnumerateHashTable(plist->d.dict); - while ((key = WMNextHashEnumeratorKey(&enumerator))) { - WMAddToArray(array->d.array, WMRetainPropList(key)); - } - - return array; -} - -WMPropList *WMShallowCopyPropList(WMPropList * plist) -{ - WMPropList *ret = NULL; - WMPropList *key, *item; - WMHashEnumerator e; - WMData *data; - int i; - - switch (plist->type) { - case WPLString: - ret = WMCreatePLString(plist->d.string); - break; - case WPLData: - data = WMCreateDataWithData(plist->d.data); - ret = WMCreatePLData(data); - WMReleaseData(data); - break; - case WPLArray: - ret = (WMPropList *) wmalloc(sizeof(W_PropList)); - ret->type = WPLArray; - ret->d.array = WMCreateArrayWithArray(plist->d.array); - ret->retainCount = 1; - - for (i = 0; i < WMGetArrayItemCount(ret->d.array); i++) - WMRetainPropList(WMGetFromArray(ret->d.array, i)); - - break; - case WPLDictionary: - ret = WMCreatePLDictionary(NULL, NULL); - e = WMEnumerateHashTable(plist->d.dict); - while (WMNextHashEnumeratorItemAndKey(&e, (void **)&item, (void **)&key)) { - WMPutInPLDictionary(ret, key, item); - } - break; - default: - wwarning(_("Used proplist functions on non-WMPropLists objects")); - wassertrv(False, NULL); - break; - } - - return ret; -} - -WMPropList *WMDeepCopyPropList(WMPropList * plist) -{ - WMPropList *ret = NULL; - WMPropList *key, *item; - WMHashEnumerator e; - WMData *data; - int i; - - switch (plist->type) { - case WPLString: - ret = WMCreatePLString(plist->d.string); - break; - case WPLData: - data = WMCreateDataWithData(plist->d.data); - ret = WMCreatePLData(data); - WMReleaseData(data); - break; - case WPLArray: - ret = WMCreatePLArray(NULL); - for (i = 0; i < WMGetArrayItemCount(plist->d.array); i++) { - item = WMDeepCopyPropList(WMGetFromArray(plist->d.array, i)); - WMAddToArray(ret->d.array, item); - } - break; - case WPLDictionary: - ret = WMCreatePLDictionary(NULL, NULL); - e = WMEnumerateHashTable(plist->d.dict); - /* While we copy an existing dictionary there is no way that we can - * have duplicate keys, so we don't need to first remove a key/value - * pair before inserting the new key/value. - */ - while (WMNextHashEnumeratorItemAndKey(&e, (void **)&item, (void **)&key)) { - WMHashInsert(ret->d.dict, WMDeepCopyPropList(key), WMDeepCopyPropList(item)); - } - break; - default: - wwarning(_("Used proplist functions on non-WMPropLists objects")); - wassertrv(False, NULL); - break; - } - - return ret; -} - -WMPropList *WMCreatePropListFromDescription(const char *desc) -{ - WMPropList *plist = NULL; - PLData *pldata; - - pldata = (PLData *) wmalloc(sizeof(PLData)); - pldata->ptr = desc; - pldata->lineNumber = 1; - - plist = getPropList(pldata); - - if (getNonSpaceChar(pldata) != 0 && plist) { - COMPLAIN(pldata, _("extra data after end of property list")); - /* - * We can't just ignore garbage after the end of the description - * (especially if the description was read from a file), because - * the "garbage" can be the real data and the real garbage is in - * fact in the beginning of the file (which is now inside plist) - */ - WMReleasePropList(plist); - plist = NULL; - } - - wfree(pldata); - - return plist; -} - -char *WMGetPropListDescription(WMPropList * plist, Bool indented) -{ - return (indented ? indentedDescription(plist, 0) : description(plist)); -} - -WMPropList *WMReadPropListFromFile(const char *file) -{ - WMPropList *plist = NULL; - PLData *pldata; - char *read_buf; - FILE *f; - struct stat stbuf; - size_t length; - - f = fopen(file, "rb"); - if (!f) { - /* let the user print the error message if he really needs to */ - /*werror(_("could not open domain file '%s' for reading"), file); */ - return NULL; - } - - if (stat(file, &stbuf) == 0) { - length = (size_t) stbuf.st_size; - } else { - werror(_("could not get size for file '%s'"), file); - fclose(f); - return NULL; - } - - read_buf = wmalloc(length + 1); - if (fread(read_buf, length, 1, f) != 1) { - if (ferror(f)) { - werror(_("error reading from file '%s'"), file); - } - fclose(f); - wfree(read_buf); - return NULL; - } - read_buf[length] = '\0'; - fclose(f); - - pldata = (PLData *) wmalloc(sizeof(PLData)); - pldata->ptr = read_buf; - pldata->filename = file; - pldata->lineNumber = 1; - - plist = getPropList(pldata); - - if (getNonSpaceChar(pldata) != 0 && plist) { - COMPLAIN(pldata, _("extra data after end of property list")); - /* - * We can't just ignore garbage after the end of the description - * (especially if the description was read from a file), because - * the "garbage" can be the real data and the real garbage is in - * fact in the beginning of the file (which is now inside plist) - */ - WMReleasePropList(plist); - plist = NULL; - } - - wfree(read_buf); - wfree(pldata); - - return plist; -} - -WMPropList *WMReadPropListFromPipe(const char *command) -{ - FILE *file; - WMPropList *plist; - PLData *pldata; - char *read_buf, *read_ptr; - size_t remain_size, line_size; - const size_t block_read_size = 4096; - const size_t block_read_margin = 512; - - file = popen(command, "r"); - - if (!file) { - werror(_("%s:could not open menu file"), command); - return NULL; - } - - /* read from file till EOF or OOM and fill proplist buffer*/ - remain_size = block_read_size; - read_buf = wmalloc(remain_size); - read_ptr = read_buf; - while (fgets(read_ptr, remain_size, file) != NULL) { - line_size = strlen(read_ptr); - - remain_size -= line_size; - read_ptr += line_size; - - if (remain_size < block_read_margin) { - size_t read_length; - - read_length = read_ptr - read_buf; - read_buf = wrealloc(read_buf, read_length + block_read_size); - read_ptr = read_buf + read_length; - remain_size = block_read_size; - } - } - - pclose(file); - - pldata = (PLData *) wmalloc(sizeof(PLData)); - pldata->ptr = read_buf; - pldata->filename = command; - pldata->lineNumber = 1; - - plist = getPropList(pldata); - - if (getNonSpaceChar(pldata) != 0 && plist) { - COMPLAIN(pldata, _("extra data after end of property list")); - /* - * We can't just ignore garbage after the end of the description - * (especially if the description was read from a file), because - * the "garbage" can be the real data and the real garbage is in - * fact in the beginning of the file (which is now inside plist) - */ - WMReleasePropList(plist); - plist = NULL; - } - - wfree(read_buf); - wfree(pldata); - - return plist; -} - -/* TODO: review this function's code */ - -Bool WMWritePropListToFile(WMPropList * plist, const char *path) -{ - char *thePath = NULL; - char *desc; - FILE *theFile; -#ifdef HAVE_MKSTEMP - int fd, mask; -#endif - - if (!wmkdirhier(path)) - return False; - - /* Use the path name of the destination file as a prefix for the - * mkstemp() call so that we can be sure that both files are on - * the same filesystem and the subsequent rename() will work. */ - thePath = wstrconcat(path, ".XXXXXX"); - -#ifdef HAVE_MKSTEMP - /* - * We really just want to read the current umask, but as Coverity is - * pointing a possible security issue: - * some versions of mkstemp do not set file rights properly on the - * created file, so it is recommended so set the umask beforehand. - * As we need to set an umask to read the current value, we take this - * opportunity to set a temporary aggresive umask so Coverity won't - * complain, even if we do not really care in the present use case. - */ - mask = umask(S_IRWXG | S_IRWXO); - if ((fd = mkstemp(thePath)) < 0) { - werror(_("mkstemp (%s) failed"), thePath); - goto failure; - } - umask(mask); - fchmod(fd, 0666 & ~mask); - if ((theFile = fdopen(fd, "wb")) == NULL) { - close(fd); - } -#else - if (mktemp(thePath) == NULL) { - werror(_("mktemp (%s) failed"), thePath); - goto failure; - } - theFile = fopen(thePath, "wb"); -#endif - - if (theFile == NULL) { - werror(_("open (%s) failed"), thePath); - goto failure; - } - - desc = indentedDescription(plist, 0); - - if (fprintf(theFile, "%s\n", desc) != strlen(desc) + 1) { - werror(_("writing to file: %s failed"), thePath); - wfree(desc); - fclose(theFile); - goto failure; - } - - wfree(desc); - - (void)fsync(fileno(theFile)); - if (fclose(theFile) != 0) { - werror(_("fclose (%s) failed"), thePath); - goto failure; - } - - /* If we used a temporary file, we still need to rename() it be the - * real file. Also, we need to try to retain the file attributes of - * the original file we are overwriting (if we are) */ - if (rename(thePath, path) != 0) { - werror(_("rename ('%s' to '%s') failed"), thePath, path); - goto failure; - } - - wfree(thePath); - return True; - - failure: - unlink(thePath); - wfree(thePath); - return False; -} - -/* - * create a directory hierarchy - * - * if the last octet of `path' is `/', the full path is - * assumed to be a directory; otherwise path is assumed to be a - * file, and the last component is stripped off. the rest is the - * the hierarchy to be created. - * - * refuses to create anything outside $WMAKER_USER_ROOT - * - * returns 1 on success, 0 on failure - */ -int wmkdirhier(const char *path) -{ - const char *t; - char *thePath = NULL, buf[1024]; - size_t p, plen; - struct stat st; - - /* Only create directories under $WMAKER_USER_ROOT */ - if ((t = wusergnusteppath()) == NULL) - return 0; - if (strncmp(path, t, strlen(t)) != 0) - return 0; - - thePath = wstrdup(path); - /* Strip the trailing component if it is a file */ - p = strlen(thePath); - while (p && thePath[p] != '/') - thePath[p--] = '\0'; - - thePath[p] = '\0'; - - /* Shortcut if it already exists */ - if (stat(thePath, &st) == 0) { - wfree(thePath); - if (S_ISDIR(st.st_mode)) { - /* Is a directory alright */ - return 1; - } else { - /* Exists, but not a directory, the caller - * might just as well abort now */ - return 0; - } - } - - memset(buf, 0, sizeof(buf)); - strncpy(buf, t, sizeof(buf) - 1); - p = strlen(buf); - plen = strlen(thePath); - - do { - while (p++ < plen && thePath[p] != '/') - ; - - strncpy(buf, thePath, p); - if (mkdir(buf, 0777) == -1 && errno == EEXIST && - stat(buf, &st) == 0 && !S_ISDIR(st.st_mode)) { - werror(_("Could not create component %s"), buf); - wfree(thePath); - return 0; - } - } while (p < plen); - - wfree(thePath); - return 1; -} - -/* ARGSUSED2 */ -static int wrmdirhier_fn(const char *path, const struct stat *st, - int type, struct FTW *ftw) -{ - /* Parameter not used, but tell the compiler that it is ok */ - (void) st; - (void) ftw; - - switch(type) { - case FTW_D: - break; - case FTW_DP: - return rmdir(path); - break; - case FTW_F: - case FTW_SL: - case FTW_SLN: - return unlink(path); - break; - case FTW_DNR: - case FTW_NS: - default: - return EPERM; - } - - /* NOTREACHED */ - return 0; -} - -/* - * remove a directory hierarchy - * - * refuses to remove anything outside $WMAKER_USER_ROOT/Defaults or $WMAKER_USER_ROOT/Library - * - * returns 1 on success, 0 on failure - * - * TODO: revisit what's error and what's not - * - * with inspirations from OpenBSD's bin/rm/rm.c - */ -int wrmdirhier(const char *path) -{ - const char *libpath; - char *udefpath = NULL; - struct stat st; - int error; - - /* Only remove directories under $WMAKER_USER_ROOT/Defaults or $WMAKER_USER_ROOT/Library */ - libpath = wuserdatapath(); - if (strncmp(path, libpath, strlen(libpath)) == 0) - if (path[strlen(libpath)] == '/') - goto path_in_valid_tree; - - udefpath = wdefaultspathfordomain(""); - if (strncmp(path, udefpath, strlen(udefpath)) == 0) - /* Note: by side effect, 'udefpath' already contains a final '/' */ - goto path_in_valid_tree; - - wfree(udefpath); - return EPERM; - - path_in_valid_tree: - wfree(udefpath); - - /* Shortcut if it doesn't exist to begin with */ - if (stat(path, &st) == -1) - return ENOENT; - - error = nftw(path, wrmdirhier_fn, 1, FTW_PHYS); - - return error; -} diff --git a/WINGs/userdefaults.c b/WINGs/userdefaults.c index 78f8f836..40010c55 100644 --- a/WINGs/userdefaults.c +++ b/WINGs/userdefaults.c @@ -46,39 +46,6 @@ static void synchronizeUserDefaults(void *foo); #define UD_SYNC_INTERVAL 2000 #endif -const char *wusergnusteppath(void) -{ - static const char subdir[] = "/" GSUSER_SUBDIR; - static char *path = NULL; - char *gspath; - char *h; - int pathlen; - - if (path) - /* Value have been already computed, re-use it */ - return path; - - gspath = GETENV("WMAKER_USER_ROOT"); - if (gspath) { - gspath = wexpandpath(gspath); - if (gspath) { - path = gspath; - return path; - } - wwarning(_("variable WMAKER_USER_ROOT defined with invalid path, not used")); - } - - h = wgethomedir(); - - pathlen = strlen(h); - path = wmalloc(pathlen + sizeof(subdir)); - strcpy(path, h); - strcpy(path + pathlen, subdir); - wfree(h); - - return path; -} - const char *wuserdatapath(void) { static char *path = NULL; @@ -330,7 +297,7 @@ WMUserDefaults *WMGetStandardUserDefaults(void) /* terminate list */ defaults->searchList[2] = NULL; - defaults->searchListArray = WMCreatePLArray(NULL, NULL); + defaults->searchListArray = WMCreateEmptyPLArray(); i = 0; while (defaults->searchList[i]) { @@ -403,7 +370,7 @@ WMUserDefaults *WMGetDefaultsFromPath(const char *path) /* terminate list */ defaults->searchList[1] = NULL; - defaults->searchListArray = WMCreatePLArray(NULL, NULL); + defaults->searchListArray = WMCreateEmptyPLArray(); i = 0; while (defaults->searchList[i]) { diff --git a/WPrefs.app/Appearance.c b/WPrefs.app/Appearance.c index 041b3de5..028c96e0 100644 --- a/WPrefs.app/Appearance.c +++ b/WPrefs.app/Appearance.c @@ -2228,7 +2228,7 @@ static void prepareForClose(_Panel * panel) WMUserDefaults *udb = WMGetStandardUserDefaults(); int i; - textureList = WMCreatePLArray(NULL, NULL); + textureList = WMCreateEmptyPLArray(); /* store list of textures */ for (i = 8; i < WMGetListNumberOfRows(panel->texLs); i++) { @@ -2250,7 +2250,7 @@ static void prepareForClose(_Panel * panel) WMReleasePropList(textureList); /* store list of colors */ - textureList = WMCreatePLArray(NULL, NULL); + textureList = WMCreateEmptyPLArray(); for (i = 0; i < wlengthof(sample_colors); i++) { WMColor *color; char *str; diff --git a/WPrefs.app/HotCornerShortcuts.c b/WPrefs.app/HotCornerShortcuts.c index d3259d80..56b437a6 100644 --- a/WPrefs.app/HotCornerShortcuts.c +++ b/WPrefs.app/HotCornerShortcuts.c @@ -151,7 +151,7 @@ static void storeData(_Panel * panel) SetIntegerForKey(WMGetSliderValue(panel->hceS), "HotCornerEdge"); - list = WMCreatePLArray(NULL, NULL); + list = WMCreateEmptyPLArray(); for (i = 0; i < sizeof(panel->hcactionsT) / sizeof(WMTextField *); i++) { str = WMGetTextFieldText(panel->hcactionsT[i]); if (strlen(str) == 0) diff --git a/WPrefs.app/Paths.c b/WPrefs.app/Paths.c index f48eb484..d9fe4978 100644 --- a/WPrefs.app/Paths.c +++ b/WPrefs.app/Paths.c @@ -203,7 +203,7 @@ static void storeData(_Panel * panel) int i; char *p; - list = WMCreatePLArray(NULL, NULL); + list = WMCreateEmptyPLArray(); for (i = 0; i < WMGetListNumberOfRows(panel->icoL); i++) { p = WMGetListItem(panel->icoL, i)->text; tmp = WMCreatePLString(p); @@ -211,7 +211,7 @@ static void storeData(_Panel * panel) } SetObjectForKey(list, "IconPath"); - list = WMCreatePLArray(NULL, NULL); + list = WMCreateEmptyPLArray(); for (i = 0; i < WMGetListNumberOfRows(panel->pixL); i++) { p = WMGetListItem(panel->pixL, i)->text; tmp = WMCreatePLString(p); diff --git a/WPrefs.app/WPrefs.c b/WPrefs.app/WPrefs.c index cbffa5af..c95f8a17 100644 --- a/WPrefs.app/WPrefs.c +++ b/WPrefs.app/WPrefs.c @@ -705,10 +705,10 @@ static void loadConfigurations(WMScreen * scr, WMWindow * mainw) } if (!db) { - db = WMCreatePLDictionary(NULL, NULL); + db = WMCreateEmptyPLDictionary(); } if (!gdb) { - gdb = WMCreatePLDictionary(NULL, NULL); + gdb = WMCreateEmptyPLDictionary(); } GlobalDB = gdb; diff --git a/WPrefs.app/main.c b/WPrefs.app/main.c index 0c47a2f0..ede35fda 100644 --- a/WPrefs.app/main.c +++ b/WPrefs.app/main.c @@ -153,7 +153,12 @@ int main(int argc, char **argv) exit(0); } - WMPLSetCaseSensitive(False); + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ + /* WMPLSetCaseSensitive(False); */ Initialize(scr); diff --git a/src/appicon.c b/src/appicon.c index 9e94b961..4160696f 100644 --- a/src/appicon.c +++ b/src/appicon.c @@ -1360,7 +1360,7 @@ static void wApplicationSaveIconPathFor(const char *iconPath, const char *wm_ins val = WMGetFromPLDictionary(adict, iconk); } else { /* no dictionary for app, so create one */ - adict = WMCreatePLDictionary(NULL, NULL); + adict = WMCreateEmptyPLDictionary(); WMPutInPLDictionary(dict, key, adict); WMReleasePropList(adict); val = NULL; diff --git a/src/defaults.c b/src/defaults.c index 26961e60..1532692a 100644 --- a/src/defaults.c +++ b/src/defaults.c @@ -862,7 +862,12 @@ static void initDefaults(void) unsigned int i; WDefaultEntry *entry; - WMPLSetCaseSensitive(False); + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ + /* WMPLSetCaseSensitive(False); */ for (i = 0; i < wlengthof(optionList); i++) { entry = &optionList[i]; diff --git a/src/dialog.c b/src/dialog.c index a594abcf..53d52d04 100644 --- a/src/dialog.c +++ b/src/dialog.c @@ -250,7 +250,7 @@ static void SaveHistory(WMArray * history, const char *filename) int i; WMPropList *plhistory; - plhistory = WMCreatePLArray(NULL); + plhistory = WMCreateEmptyPLArray(); for (i = 0; i < WMGetArrayItemCount(history); ++i) WMAddToPLArray(plhistory, WMCreatePLString(WMGetFromArray(history, i))); diff --git a/src/dock.c b/src/dock.c index d59cf35c..08bb91b5 100644 --- a/src/dock.c +++ b/src/dock.c @@ -1603,11 +1603,13 @@ static WMPropList *make_icon_state(WAppIcon *btn) snprintf(buffer, sizeof(buffer), "%hi,%hi", wAppIconGetXIndex(btn), wAppIconGetYIndex(btn)); position = WMCreatePLString(buffer); - node = WMCreatePLDictionary(dCommand, command, - dName, name, - dAutoLaunch, autolaunch, - dLock, lock, - dForced, forced, dBuggyApplication, buggy, dPosition, position, NULL); + node = WMCreatePLDictionary(dCommand, command); + WMPutInPLDictionary(node, dName, name); + WMPutInPLDictionary(node, dAutoLaunch, autolaunch); + WMPutInPLDictionary(node, dLock, lock); + WMPutInPLDictionary(node, dForced, forced); + WMPutInPLDictionary(node, dBuggyApplication, buggy); + WMPutInPLDictionary(node, dPosition, position); WMReleasePropList(command); WMReleasePropList(name); WMReleasePropList(position); @@ -1642,7 +1644,7 @@ static WMPropList *dockSaveState(WDock *dock) WMPropList *value, *key; char buffer[256]; - list = WMCreatePLArray(NULL); + list = WMCreateEmptyPLArray(); for (i = (dock->type == WM_DOCK ? 0 : 1); i < dock->max_icons; i++) { WAppIcon *btn = dock->icon_array[i]; @@ -1657,7 +1659,7 @@ static WMPropList *dockSaveState(WDock *dock) } } - dock_state = WMCreatePLDictionary(dApplications, list, NULL); + dock_state = WMCreatePLDictionary(dApplications, list); if (dock->type == WM_DOCK) { snprintf(buffer, sizeof(buffer), "Applications%i", dock->screen_ptr->scr_height); @@ -5072,7 +5074,7 @@ static WMPropList *drawerSaveState(WDock *drawer) ai = drawer->icon_array[0]; /* Store its name */ pstr = WMCreatePLString(wAppIconGetWmInstance(ai)); - drawer_state = WMCreatePLDictionary(dName, pstr, NULL); /* we need this final NULL */ + drawer_state = WMCreatePLDictionary(dName, pstr); WMReleasePropList(pstr); /* Store its position */ @@ -5114,7 +5116,7 @@ void wDrawersSaveState(WScreen *scr) make_keys(); - all_drawers = WMCreatePLArray(NULL); + all_drawers = WMCreateEmptyPLArray(); for (i=0, dc = scr->drawers; i < scr->drawer_count; i++, dc = dc->next) { diff --git a/src/menu.c b/src/menu.c index e84b1caa..2a83633b 100644 --- a/src/menu.c +++ b/src/menu.c @@ -2309,7 +2309,8 @@ static void saveMenuInfo(WMPropList * dict, WMenu * menu, WMPropList * key) snprintf(buffer, sizeof(buffer), "%i,%i", menu->frame_x, menu->frame_y); value = WMCreatePLString(buffer); - list = WMCreatePLArray(value, NULL); + list = WMCreateEmptyPLArray(); + WMAddToPLArray(list, value); if (menu->flags.lowered) WMAddToPLArray(list, WMCreatePLString("lowered")); WMPutInPLDictionary(dict, key, list); @@ -2322,7 +2323,7 @@ void wMenuSaveState(WScreen * scr) WMPropList *menus, *key; int save_menus = 0; - menus = WMCreatePLDictionary(NULL, NULL); + menus = WMCreateEmptyPLDictionary(); if (scr->switch_menu && scr->switch_menu->flags.buttoned) { key = WMCreatePLString("SwitchMenu"); diff --git a/src/screen.c b/src/screen.c index 98e9f949..6bb7832c 100644 --- a/src/screen.c +++ b/src/screen.c @@ -968,8 +968,6 @@ void wScreenSaveState(WScreen * scr) old_state = scr->session_state; scr->session_state = WMCreatePLDictionary(NULL, NULL); - WMPLSetCaseSensitive(True); - /* save dock state to file */ if (!wPreferences.flags.nodock) { wDockSaveState(scr, old_state); @@ -1009,8 +1007,13 @@ void wScreenSaveState(WScreen * scr) WMPutInPLDictionary(scr->session_state, dWorkspace, foo); } + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ /* clean up */ - WMPLSetCaseSensitive(False); + /* WMPLSetCaseSensitive(False); */ wMenuSaveState(scr); diff --git a/src/session.c b/src/session.c index c01eba2f..ae5f1525 100644 --- a/src/session.c +++ b/src/session.c @@ -241,14 +241,15 @@ static WMPropList *makeWindowState(WWindow * wwin, WApplication * wapp) snprintf(buffer, sizeof(buffer), "%u", mask); shortcut = WMCreatePLString(buffer); - win_state = WMCreatePLDictionary(sName, name, - sCommand, cmd, - sWorkspace, workspace, - sShaded, shaded, - sMiniaturized, miniaturized, - sMaximized, maximized, - sHidden, hidden, - sShortcutMask, shortcut, sGeometry, geometry, NULL); + win_state = WMCreatePLDictionary(sName, name); + WMPutInPLDictionary(win_state, sCommand, cmd); + WMPutInPLDictionary(win_state, sWorkspace, workspace); + WMPutInPLDictionary(win_state, sShaded, shaded); + WMPutInPLDictionary(win_state, sMiniaturized, miniaturized); + WMPutInPLDictionary(win_state, sMaximized, maximized); + WMPutInPLDictionary(win_state, sHidden, hidden); + WMPutInPLDictionary(win_state, sShortcutMask, shortcut); + WMPutInPLDictionary(win_state, sGeometry, geometry); WMReleasePropList(name); WMReleasePropList(cmd); @@ -313,7 +314,7 @@ void wSessionSaveState(WScreen * scr) return; } - list = WMCreatePLArray(NULL); + list = WMCreateEmptyPLArray(); wapp_list = WMCreateArray(16); @@ -487,8 +488,6 @@ void wSessionRestoreState(WScreen *scr) if (!scr->session_state) return; - WMPLSetCaseSensitive(True); - apps = WMGetFromPLDictionary(scr->session_state, sApplications); if (!apps) return; @@ -579,8 +578,13 @@ void wSessionRestoreState(WScreen *scr) if (class) wfree(class); } + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ /* clean up */ - WMPLSetCaseSensitive(False); + /* WMPLSetCaseSensitive(False); */ } void wSessionRestoreLastWorkspace(WScreen * scr) @@ -594,8 +598,6 @@ void wSessionRestoreLastWorkspace(WScreen * scr) if (!scr->session_state) return; - WMPLSetCaseSensitive(True); - wks = WMGetFromPLDictionary(scr->session_state, sWorkspace); if (!wks || !WMIsPLString(wks)) return; @@ -605,8 +607,13 @@ void wSessionRestoreLastWorkspace(WScreen * scr) if (!value) return; + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ /* clean up */ - WMPLSetCaseSensitive(False); + /* WMPLSetCaseSensitive(False); */ /* Get the workspace number for the workspace name */ w = wGetWorkspaceNumber(scr, value); diff --git a/src/wdefaults.c b/src/wdefaults.c index 7aad3db5..e2fe5009 100644 --- a/src/wdefaults.c +++ b/src/wdefaults.c @@ -174,15 +174,18 @@ static WMPropList *get_value_from_instanceclass(const char *value) key = WMCreatePLString(value); - WMPLSetCaseSensitive(True); - if (w_global.domain.window_attr->dictionary) val = key ? WMGetFromPLDictionary(w_global.domain.window_attr->dictionary, key) : NULL; if (key) WMReleasePropList(key); - WMPLSetCaseSensitive(False); + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ + /* WMPLSetCaseSensitive(False); */ return val; } @@ -219,8 +222,6 @@ void wDefaultFillAttributes(const char *instance, const char *class, dn = get_value_from_instanceclass(instance); dc = get_value_from_instanceclass(class); - WMPLSetCaseSensitive(True); - if ((w_global.domain.window_attr->dictionary) && (useGlobalDefault)) da = WMGetFromPLDictionary(w_global.domain.window_attr->dictionary, AnyWindow); @@ -311,8 +312,13 @@ void wDefaultFillAttributes(const char *instance, const char *class, APPLY_VAL(value, no_language_button, ANoLanguageButton); #endif + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ /* clean up */ - WMPLSetCaseSensitive(False); + /* WMPLSetCaseSensitive(False); */ } static WMPropList *get_generic_value(const char *instance, const char *class, @@ -322,8 +328,6 @@ static WMPropList *get_generic_value(const char *instance, const char *class, value = NULL; - WMPLSetCaseSensitive(True); - /* Search the icon name using class and instance */ if (class && instance) { char *buffer; @@ -371,7 +375,12 @@ static WMPropList *get_generic_value(const char *instance, const char *class, value = WMGetFromPLDictionary(dict, option); } - WMPLSetCaseSensitive(False); + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ + /* WMPLSetCaseSensitive(False); */ return value; } @@ -545,15 +554,13 @@ void wDefaultChangeIcon(const char *instance, const char *class, const char *fil int same = 0; if (!dict) { - dict = WMCreatePLDictionary(NULL, NULL); + dict = WMCreateEmptyPLDictionary(); if (dict) db->dictionary = dict; else return; } - WMPLSetCaseSensitive(True); - if (instance && class) { char *buffer; @@ -570,7 +577,7 @@ void wDefaultChangeIcon(const char *instance, const char *class, const char *fil if (file) { value = WMCreatePLString(file); - icon_value = WMCreatePLDictionary(AIcon, value, NULL); + icon_value = WMCreatePLDictionary(AIcon, value); WMReleasePropList(value); def_win = WMGetFromPLDictionary(dict, AnyWindow); @@ -600,7 +607,12 @@ void wDefaultChangeIcon(const char *instance, const char *class, const char *fil if (icon_value) WMReleasePropList(icon_value); - WMPLSetCaseSensitive(False); + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ + /* WMPLSetCaseSensitive(False); */ } void wDefaultPurgeInfo(const char *instance, const char *class) @@ -612,8 +624,6 @@ void wDefaultPurgeInfo(const char *instance, const char *class) init_wdefaults(); } - WMPLSetCaseSensitive(True); - buffer = wmalloc(strlen(class) + strlen(instance) + 2); sprintf(buffer, "%s.%s", instance, class); key = WMCreatePLString(buffer); @@ -631,7 +641,12 @@ void wDefaultPurgeInfo(const char *instance, const char *class) wfree(buffer); WMReleasePropList(key); - WMPLSetCaseSensitive(False); + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ + /* WMPLSetCaseSensitive(False); */ } /* --------------------------- Local ----------------------- */ diff --git a/src/winspector.c b/src/winspector.c index eacfdf95..0af1a789 100644 --- a/src/winspector.c +++ b/src/winspector.c @@ -597,7 +597,7 @@ static void saveSettings(WMWidget *button, void *client_data) dict = db->dictionary; if (!dict) { - dict = WMCreatePLDictionary(NULL, NULL); + dict = WMCreateEmptyPLDictionary(); if (dict) { db->dictionary = dict; } else { @@ -609,10 +609,8 @@ static void saveSettings(WMWidget *button, void *client_data) if (showIconFor(WMWidgetScreen(button), panel, NULL, NULL, USE_TEXT_FIELD) < 0) return; - WMPLSetCaseSensitive(True); - - winDic = WMCreatePLDictionary(NULL, NULL); - appDic = WMCreatePLDictionary(NULL, NULL); + winDic = WMCreateEmptyPLDictionary(); + appDic = WMCreateEmptyPLDictionary(); /* Save the icon info */ /* The flag "Ignore client suplied icon is not selected" */ @@ -709,8 +707,13 @@ static void saveSettings(WMWidget *button, void *client_data) UpdateDomainFile(db); + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ /* clean up */ - WMPLSetCaseSensitive(False); + /* WMPLSetCaseSensitive(False); */ } static void applySettings(WMWidget *button, void *client_data) diff --git a/src/workspace.c b/src/workspace.c index dad14eb5..6686c1a1 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -852,10 +852,10 @@ void wWorkspaceSaveState(WScreen * scr, WMPropList * old_state) make_keys(); old_wks_state = WMGetFromPLDictionary(old_state, dWorkspaces); - parr = WMCreatePLArray(NULL); + parr = WMCreateEmptyPLArray(); for (i = 0; i < scr->workspace_count; i++) { pstr = WMCreatePLString(scr->workspaces[i]->name); - wks_state = WMCreatePLDictionary(dName, pstr, NULL); + wks_state = WMCreatePLDictionary(dName, pstr); WMReleasePropList(pstr); if (!wPreferences.flags.noclip) { pstr = wClipSaveWorkspaceState(scr, i); diff --git a/util/convertfonts.c b/util/convertfonts.c index 1674ced8..d653e724 100644 --- a/util/convertfonts.c +++ b/util/convertfonts.c @@ -131,7 +131,12 @@ int main(int argc, char **argv) /* this contradicts big time with getstyle */ setlocale(LC_ALL, ""); - WMPLSetCaseSensitive(False); + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ + /* WMPLSetCaseSensitive(False); */ style = WMReadPropListFromFile(file); if (!style) { diff --git a/util/geticonset.c b/util/geticonset.c index 4505551c..8941bb11 100644 --- a/util/geticonset.c +++ b/util/geticonset.c @@ -109,7 +109,7 @@ int main(int argc, char **argv) if (window_attrs && WMIsPLDictionary(window_attrs)) { icon_value = WMGetFromPLDictionary(window_attrs, icon_key); if (icon_value) { - icondic = WMCreatePLDictionary(icon_key, icon_value, NULL); + icondic = WMCreatePLDictionary(icon_key, icon_value); WMPutInPLDictionary(iconset, window_name, icondic); } } diff --git a/util/getstyle.c b/util/getstyle.c index a2935892..3cde01ac 100644 --- a/util/getstyle.c +++ b/util/getstyle.c @@ -337,7 +337,12 @@ int main(int argc, char **argv) return 1; } - WMPLSetCaseSensitive(False); + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ + /* WMPLSetCaseSensitive(False); */ path = wdefaultspathfordomain("WindowMaker"); @@ -357,7 +362,7 @@ int main(int argc, char **argv) prop = val; } - style = WMCreatePLDictionary(NULL, NULL); + style = WMCreateEmptyPLDictionary(); for (i = 0; options[i] != NULL; i++) { key = WMCreatePLString(options[i]); diff --git a/util/setstyle.c b/util/setstyle.c index 36599ae4..4fcdfcc2 100644 --- a/util/setstyle.c +++ b/util/setstyle.c @@ -427,7 +427,12 @@ int main(int argc, char **argv) file = argv[0]; - WMPLSetCaseSensitive(False); + /* + * Rust rewrite note: this API surface has been removed, but we leave in + * a record of where it was invoked to set a value other than the + * default, in case it helps to track down bugs in the future. + */ + /* WMPLSetCaseSensitive(False); */ path = wdefaultspathfordomain("WindowMaker"); diff --git a/util/wdwrite.c b/util/wdwrite.c index 2ff16706..c169ad64 100644 --- a/util/wdwrite.c +++ b/util/wdwrite.c @@ -103,7 +103,7 @@ int main(int argc, char **argv) dict = WMReadPropListFromFile(path); if (!dict) { - dict = WMCreatePLDictionary(key, value, NULL); + dict = WMCreatePLDictionary(key, value); } else { WMPutInPLDictionary(dict, key, value); } diff --git a/util/wmsetbg.c b/util/wmsetbg.c index dc747d04..2e65cace 100644 --- a/util/wmsetbg.c +++ b/util/wmsetbg.c @@ -1199,14 +1199,14 @@ static void changeTextureForWorkspace(const char *domain, char *texture, int wor array = getValueForKey("WindowMaker", "WorkspaceSpecificBack"); if (!array) { - array = WMCreatePLArray(NULL, NULL); + array = WMCreateEmptyPLArray(); } j = WMGetPropListItemCount(array); if (workspace >= j) { WMPropList *empty; - empty = WMCreatePLArray(NULL, NULL); + empty = WMCreateEmptyPLArray(); while (j++ < workspace - 1) { WMAddToPLArray(array, empty); diff --git a/wutil-rs/Cargo.toml b/wutil-rs/Cargo.toml index 9eff16fb..66e739ea 100644 --- a/wutil-rs/Cargo.toml +++ b/wutil-rs/Cargo.toml @@ -8,3 +8,8 @@ crate-type = ["staticlib"] [build-dependencies] cc = "1.0" + +[dependencies] +atomic-write-file = "0.3" +nom = "8.0" +nom-language = "0.1" diff --git a/wutil-rs/Makefile.am b/wutil-rs/Makefile.am index 8974f2d8..7955beb3 100644 --- a/wutil-rs/Makefile.am +++ b/wutil-rs/Makefile.am @@ -2,9 +2,12 @@ AUTOMAKE_OPTIONS = RUST_SOURCES = \ src/array.rs \ + src/defines.c \ + src/defines.rs \ src/find_file.rs \ src/lib.rs \ - src/memory.rs + src/memory.rs \ + src/prop_list.rs RUST_EXTRA = \ Cargo.lock \ diff --git a/wutil-rs/src/data.rs b/wutil-rs/src/data.rs index aff4788e..dab84a49 100644 --- a/wutil-rs/src/data.rs +++ b/wutil-rs/src/data.rs @@ -19,6 +19,19 @@ pub enum Format { /// Rust. pub struct Data(Rc>); +impl Data { + /// Runs `f` on a borrow of `self`'s data, returning whatever `f` does. + /// + /// This is provided because the internal structure of `Data` does not make + /// it easy to borrow its contents directly. As we migrate away from the + /// original C interfaces to WINGs data structures, we may be able to + /// provide a direct borrow, which would make this method obsolete. (That + /// would be a good thing.) + pub fn with_bytes(&self, f: impl FnOnce(&[u8]) -> R) -> R { + f(&self.0.borrow().bytes) + } +} + struct Inner { bytes: Vec, format: Format, diff --git a/wutil-rs/src/find_file.rs b/wutil-rs/src/find_file.rs index 5a0dbeee..bcc4447d 100644 --- a/wutil-rs/src/find_file.rs +++ b/wutil-rs/src/find_file.rs @@ -12,21 +12,34 @@ //! whose first component is `~` will still be resolved relatively to the //! current user's home directory. //! -//! Keep in mind that these utilities are not strictly correct as originally -//! designed: a file path that appears valid when it is checked in a subroutine -//! may become invalid if the file is deleted between when the path is checked -//! and when downstream code attempts to open the file. A better design would -//! open the file and return a live file pointer instead of simply returning a -//! path that is likely to work. Future work should redesign this module to -//! avoid this issue. +//! ## Rust rewrite notes +//! +//! Many of these utilities are not strictly correct as originally designed: a +//! file path that appears valid when it is checked in a subroutine may become +//! invalid if the file is deleted between when the path is checked and when +//! downstream code attempts to open the file. (This is a TOCTOU issue.) A +//! better design would open the file and return a live file pointer instead of +//! simply returning a path that is likely to work. Future work should redesign +//! this module to avoid this issue. + +use crate::defines; use std::{ env, - ffi::OsStr, + ffi::{CStr, OsStr}, fs::File, + io, path::{Component, Path, PathBuf}, }; +/// Tries to interpret `s` as a UTF-8 path. Returns `None` if decoding `s` +/// fails. +pub fn path_from_cstr(s: &CStr) -> Option { + String::from_utf8(s.to_bytes().iter().copied().collect()) + .ok() + .map(|p| PathBuf::from(p)) +} + /// If `file` is an absolute path can be opened, returns that path. Paths /// starting with `~` are treated as absolute, and the user's home directory is /// substituted for `~` (so `~/foo` becomes `(users's home directory)/foo`). If @@ -78,13 +91,116 @@ pub fn in_paths<'a>(paths: impl Iterator, file: &Path) -> Optio None } +/// Returns the root path that user data will be stored at. This is probably +/// `$HOME/GNUstep` (or some other default if a different path was specified for +/// [`defaults::gsuser_subdir`] at compilation time), unless the environment +/// variable `WMAKER_USER_ROOT` is defined, in which case it's that. +pub fn user_gnustep_path() -> Option { + match env::var("WMAKER_USER_ROOT") { + Ok(path) => { + if let Some(path) = absolute(&PathBuf::from(&path)) { + return Some(path); + } + } + Err(env::VarError::NotUnicode(_)) => { + // TODO: warn. + } + Err(env::VarError::NotPresent) => (), + } + + match (env::home_dir(), defines::gsuser_subdir()) { + (None, _) => { + // TODO: warn. + None + } + (Some(mut parent), Some(subdir)) => { + parent.push(subdir); + Some(parent) + } + (Some(_), None) => { + // TODO: warn. + None + } + } +} + +/// Creates the directory at `p` and all of its parents, respecting the current +/// umask as much as possible. Only allows creation of paths under +/// [`user_gnustep_path`]. +pub fn create_path_hierarchy>(p: P) -> io::Result<()> { + let p = p.as_ref(); + let Ok(canonical_p) = p.canonicalize() else { + return Err(io::Error::other("cannot canonicalize requested path")); + }; + let Some(wmaker_user_root) = user_gnustep_path() else { + return Err(io::Error::other("cannot determine WMAKER_USER_ROOT. try setting the environment variable WMAKER_USER_ROOT.")); + }; + let Ok(wmaker_user_root) = Path::new(&wmaker_user_root).canonicalize() else { + return Err(io::Error::other( + "cannot canonicalize path for WMAKER_USER_ROOT", + )); + }; + + if !canonical_p.starts_with(&wmaker_user_root) { + return Err(io::Error::other( + "requested path is not under WMAKER_USER_ROOT", + )); + } + + create_path_hierarchy_impl(&canonical_p) +} + +/// Removes the file at `p` (and anything under it, if `p` is a directory). Only +/// allows deletion of files under [`user_gnustep_path`]`/Defaults` or +/// [`user_gnustep_path`]`/Library`. +pub fn remove_path_hierarchy>(p: P) -> io::Result<()> { + let p = p.as_ref(); + let Ok(canonical_p) = p.canonicalize() else { + return Err(io::Error::other("cannot canonicalize requested path")); + }; + let Some(wmaker_user_root) = user_gnustep_path() else { + return Err(io::Error::other("cannot determine WMAKER_USER_ROOT. try setting the environment variable WMAKER_USER_ROOT.")); + }; + let Ok(wmaker_user_root) = Path::new(&wmaker_user_root).canonicalize() else { + return Err(io::Error::other( + "cannot canonicalize path for WMAKER_USER_ROOT", + )); + }; + + let mut defaults = wmaker_user_root.clone(); + defaults.push("Defaults"); + let mut library = wmaker_user_root; + library.push("Library"); + if !canonical_p.starts_with(&defaults) && !canonical_p.starts_with(&library) { + return Err(io::Error::other( + "requested path is not under WMAKER_USER_ROOT/Defaults or WMAKER_USER_ROOT/Library", + )); + } + + std::fs::remove_dir_all(canonical_p) +} + +#[cfg(target_family = "unix")] +fn create_path_hierarchy_impl(p: &Path) -> io::Result<()> { + use std::os::unix::fs::DirBuilderExt; + std::fs::DirBuilder::new() + .recursive(true) + .mode(0o777) + .create(p) +} + +#[cfg(not(target_family = "unix"))] +fn create_path_hierarchy_impl(p: &Path) -> io::Result<()> { + std::fs::DirBuilder::new().recursive(true).create(p) +} + pub mod ffi { - use super::{absolute, in_paths}; + use super::{absolute, create_path_hierarchy, in_paths, path_from_cstr, remove_path_hierarchy, user_gnustep_path}; use crate::memory::alloc_bytes; use std::{ env, - ffi::{CStr, OsStr, c_char, c_int}, + ffi::{c_char, c_int, CStr, OsStr}, iter, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, @@ -218,4 +334,64 @@ pub mod ffi { return -1; } } + + /// Delegates to [`create_path_hierarchy`]. + /// + /// ## Rust rewrite notes + /// + /// The original C implementation of this function stripped the last path + /// component if it did not end in a `'/'` (i.e., it looked like a regular + /// file instead of a directory). This behavior does not appear to be relied + /// upon by any existing callers except for `WMWritePropListToFile`, which + /// has been rewritten, and it is not super portable (and annoying because + /// it adds weird edge cases). So each component of `path` is treated as a + /// directory to be made. + #[unsafe(no_mangle)] + pub unsafe extern "C" fn wmkdirhier(path: *const c_char) -> c_int { + if path.is_null() { + return 0; + } + let path = unsafe { CStr::from_ptr(path) }; + let Some(path) = path_from_cstr(path) else { + return 0; + }; + if create_path_hierarchy(path).is_ok() { + return 1; + } else { + return 0; + } + } + + /// Delegates to [`remove_path_hierarchy`]. + #[unsafe(no_mangle)] + pub unsafe extern "C" fn wrmdirhier(path: *const c_char) -> c_int { + if path.is_null() { + return 0; + } + let path = unsafe { CStr::from_ptr(path) }; + let Some(path) = path_from_cstr(path) else { + return 0; + }; + if remove_path_hierarchy(path).is_ok() { + return 1; + } else { + return 0; + } + } + + /// Delegates to [`user_gnustep_path`]. Returns the path, which must be The + /// returned value must be freed with [`crate::memory::free_bytes`] or + /// [`crate::memory::ffi::wfree`], path cannot be determined. + /// + /// ## Rust rewrite notes + /// + /// This was originally in `WINGs/userdefaults.c`. + #[unsafe(no_mangle)] + pub unsafe extern "C" fn wusergnusteppath() -> *mut c_char { + if let Some(path) = user_gnustep_path() { + to_c_str(&path) + } else { + ptr::null_mut() + } + } } diff --git a/wutil-rs/src/lib.rs b/wutil-rs/src/lib.rs index 081e4412..47577b48 100644 --- a/wutil-rs/src/lib.rs +++ b/wutil-rs/src/lib.rs @@ -3,3 +3,4 @@ pub mod data; pub mod defines; pub mod find_file; pub mod memory; +pub mod prop_list; diff --git a/wutil-rs/src/prop_list.rs b/wutil-rs/src/prop_list.rs new file mode 100644 index 00000000..4a2376f0 --- /dev/null +++ b/wutil-rs/src/prop_list.rs @@ -0,0 +1,871 @@ +//! Property lists: shared, tree-structured data. +//! +//! ## Rust rewrite notes +//! +//! This implementation should be good enough to facilitate migrating from C to +//! Rust and trasitioning away from using property lists everywhere instead of +//! proper structs. `PropList`s are a really cool general-purpose tool for +//! attaching data to objects and persisting it to disk. But in Rust, it is +//! easier to use proper structs with typed fields and appropriate `#[derive]` +//! declarations to generate code for serialization and deserialization +//! (presumably using Serde). +//! +//! As code that uses `PropList`s is rewritte in Rust, we should work on +//! migrating away from use of `PropList`s. Objects whose fields that can be +//! statically typed should be represented as structs. They may still be +//! persisted by cramming them into `PropList`s and writing those to disk, but +//! it would be better still to implement Serde-based serialization to and from +//! the property list format. +//! +//! The `PropList` implementation itself can also be improved substantially. See +//! [`PropList`] for thoughts on this. + +use atomic_write_file::unix::OpenOptionsExt; + +use std::{ + cell::RefCell, + collections::{hash_map, HashMap}, + ffi::{CString, OsStr, OsString}, + fmt, hash, + io::{self, BufWriter, Write}, + path::Path, + process::Command, + ptr, + rc::Rc, +}; + +use crate::find_file; + +pub mod parser; +pub mod writer; + +/// Payload of a [`PropList`]. +#[derive(Eq, PartialEq)] +pub enum Node { + /// Text data. This is UTF-8 encoded and null-safe. + /// + /// ## Rust rewrite notes + /// + /// It would be better for this to be a `String`, but the C interface + /// requires borrows of C-style strings. + String(CString), + /// Binary data. + Data(Vec), + /// Array of child `PropList`s. + Array(Vec), + /// `PropList`-keyed table of child `PropList`s. Keys should only have + /// `Node::String` or `Node::Data` payloads, although there is almost no + /// enforcement of this. + Dictionary(HashMap), +} + +impl hash::Hash for Node { + fn hash(&self, h: &mut H) { + match self { + Node::String(s) => s.hash(h), + Node::Data(d) => d.hash(h), + Node::Array(a) => { + for p in a { + p.hash(h); + } + } + Node::Dictionary(d) => { + for (k, v) in d { + k.hash(h); + v.hash(h); + } + } + } + } +} + +impl fmt::Debug for Node { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + writer::Display { + inline: writer::Inline::Soft, + clear_left: false, + indentation: 0, + increment: 2, + node: self, + } + ) + } +} + +fn merge_shallow(dest: PropList, source: PropList) { + if ptr::eq(dest.0.as_ref(), source.0.as_ref()) { + return; + } + let Node::Dictionary(ref mut dest_items) = *dest.0.borrow_mut() else { + return; + }; + let Node::Dictionary(ref source) = *source.0.borrow() else { + return; + }; + for (k, v) in source { + if ptr::eq(dest.0.as_ptr(), k.0.as_ptr()) { + // Don't borrow k if it is already borrowed as dest. + continue; + } + dest_items.insert(k.clone(), v.clone()); + } +} + +fn merge_deep(dest: PropList, source: PropList) { + if ptr::eq(dest.0.as_ptr(), source.0.as_ptr()) { + return; + } + let Node::Dictionary(dest_items) = &mut *dest.0.borrow_mut() else { + return; + }; + let Node::Dictionary(source_items) = &*source.0.borrow() else { + return; + }; + + for (key, value) in source_items { + if key.0.try_borrow().is_err() || value.0.try_borrow().is_err() { + // Something has already borrowed key or value. This may happen if + // source contains pointers that are also in dest, or if dest is + // cyclic. This is bad, but we just bail out. + continue; + } + match dest_items.entry(key.clone()) { + hash_map::Entry::Vacant(v) => { + // Dest has nothing at key. Insert value from source. + v.insert(value.clone()); + } + hash_map::Entry::Occupied(mut o) => { + let recur = match *o.get().0.borrow() { + Node::Dictionary(_) => true, + _ => false, + }; + if recur { + // dest[key] is a dictionary. Recur on dest[key] and value from source. + merge_deep(o.get().clone(), value.clone()); + } else { + // dest[key] is not a dictionary. Overwrite with value from source. + o.insert(value.clone()); + } + } + } + } +} + +fn subtract_shallow(dest: PropList, source: PropList) { + if ptr::eq(dest.0.as_ptr(), source.0.as_ptr()) { + if let Node::Dictionary(ref mut items) = *dest.0.borrow_mut() { + items.clear(); + } + return; + } + + let Node::Dictionary(ref mut dest_items) = *dest.0.borrow_mut() else { + return; + }; + let Node::Dictionary(ref source_items) = *source.0.borrow() else { + return; + }; + for (k, v) in source_items.iter() { + if ptr::eq(dest.0.as_ptr(), k.0.as_ptr()) { + continue; + } + if let hash_map::Entry::Occupied(o) = dest_items.entry(k.clone()) { + if o.get() == v { + o.remove(); + } + } + } +} + +fn subtract_deep(dest: PropList, source: PropList) { + if ptr::eq(dest.0.as_ptr(), source.0.as_ptr()) { + if let Node::Dictionary(ref mut items) = *dest.0.borrow_mut() { + items.clear(); + } + return; + } + + let Node::Dictionary(ref mut dest_items) = *dest.0.borrow_mut() else { + return; + }; + let Node::Dictionary(ref source_items) = *source.0.borrow() else { + return; + }; + for (k, v) in source_items.iter() { + if ptr::eq(dest.0.as_ptr(), k.0.as_ptr()) { + continue; + } + if let hash_map::Entry::Occupied(o) = dest_items.entry(k.clone()) { + if o.get() == v { + o.remove(); + continue; + } + let recur = match (&*o.get().0.borrow(), &*v.0.borrow()) { + (Node::Dictionary(_), Node::Dictionary(_)) => true, + _ => false, + }; + if recur { + subtract_deep(o.get().clone(), v.clone()); + } + } + } +} + +/// Data graph with convenient (de)serialization to/from the [property +/// list](https://en.wikipedia.org/wiki/Property_list) format. +/// +/// ## Rust rewrite notes +/// +/// The original WUtils `PropList` was a reference-counted pointer, so it +/// supported shallow copy and shared-memory semantics that we have continued to +/// try to support in the Rust implementation. As a result, `PropList` is a thin +/// wrapper around an `Rc>`. There are several reasons why this is +/// probably unnecessary and something we should migrate away from: +/// +/// * It allows for the creation of non-tree structures, which was probably +/// never intended. (A degenerate `PropList` could even have itself as a child.) +/// * It complicates recursive operations on `PropList`s (equality checks, +/// merging, or taking differences) because a given `PropList` may occur +/// multiple times when traversing two `PropList`s, but `Rc` only allows it to be +/// mutably borrowed once. +/// * Allowing subtrees to be shared between two different `PropList`s +/// may lead to spooky action at a distance and may not actually be taken +/// advantage of by any client code. +/// +/// As client code is migrated into Rust, it would be great to move away from +/// this implementation to a simpler one. As discussed in the module-level +/// rewrite notes, we may even be able to do away with `PropList` itself +/// (perhaps in favor of using Serde to write to and from property list files on +/// disk). +#[derive(Clone)] +pub struct PropList(Rc>); + +impl PropList { + pub fn new(node: Node) -> Self { + PropList(Rc::new(RefCell::new(node))) + } + + /// Reads `r` to the end and tries to parse it into a `PropList`. + pub fn from_file>(path: P) -> Result { + let path = path.as_ref().to_path_buf(); + let buf = std::fs::read_to_string(&path).map_err(|e| format!("{}", e))?; + parser::from_str(buf.as_str()) + } + + // Runs `command` and tries to parse a PropList from its standard output. + pub fn from_command>(command: S) -> Result { + let command: OsString = command.as_ref().to_os_string(); + let output = Command::new("/bin/sh") + .arg("-c") + .arg(command.clone()) + .output() + .map_err(|e| format!("{}", e))?; + let output = str::from_utf8(&output.stdout).map_err(|e| format!("{}", e))?; + parser::from_str(&output) + } + + pub fn display_indented<'s>(&'s self) -> impl fmt::Display + 's { + writer::Display { + inline: writer::Inline::Soft, + clear_left: false, + indentation: 0, + increment: 2, + node: self.0.borrow(), + } + } + + pub fn display_unindented<'s>(&'s self) -> impl fmt::Display + 's { + writer::Display { + inline: writer::Inline::Hard, + clear_left: false, + indentation: 0, + increment: 0, + node: self.0.borrow(), + } + } +} + +impl Eq for PropList {} + +impl PartialEq for PropList { + fn eq(&self, other: &Self) -> bool { + *self.0.borrow() == *other.0.borrow() + } +} + +impl hash::Hash for PropList { + fn hash(&self, h: &mut H) { + self.0.borrow().hash(h) + } +} + +impl fmt::Debug for PropList { + fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result { + write!(out, "{:?}", self.0.borrow())?; + Ok(()) + } +} + +impl PropList { + pub fn deep_clone(&self) -> Self { + match &*self.0.borrow() { + Node::String(s) => PropList::new(Node::String(s.clone())), + Node::Data(d) => PropList::new(Node::Data(d.clone())), + Node::Array(items) => { + PropList::new(Node::Array(items.iter().map(|x| x.deep_clone()).collect())) + } + Node::Dictionary(items) => PropList::new(Node::Dictionary( + items + .iter() + .map(|(k, v)| (k.deep_clone(), v.deep_clone())) + .collect(), + )), + } + } + + /// Atomically serialize this `PropList` to `path`, creating any necessary + /// parent directories. + /// + /// `path` is written to atomically: either the serialized `PropList` will + /// be completely written to `path`, or the operation will fail and any + /// existing file at `path` will not be modified. + /// + /// ## Rust rewrite notes + /// + /// As originally noted in `proplist.c`, a Coverity security bug report + /// flagged the need to preserve the permissions on the file being written + /// to. This should be respected in the rewritten code under Unix-like + /// operataing systems. + pub fn write_to_file>(&self, path: &P) -> io::Result<()> { + self.write_to_file_impl(path.as_ref()) + } + + fn write_to_file_impl(&self, path: &Path) -> io::Result<()> { + if let Some(parent) = path.parent() { + find_file::create_path_hierarchy(parent)?; + } + let file = atomic_write_file::AtomicWriteFile::options() + .preserve_mode(true) + .open(&path)?; + let mut out = BufWriter::new(file); + writeln!(&mut out, "{}", self.display_indented())?; + out.into_inner()?.commit() + } +} + +pub mod ffi { + use crate::{data::Data, find_file::path_from_cstr}; + + use super::{ + merge_deep, merge_shallow, parser, subtract_deep, subtract_shallow, Node, PropList, + }; + + use std::{ + collections::HashMap, ffi::{c_char, c_int, c_uchar, c_uint, CStr, CString, OsString}, ptr, str::FromStr + }; + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMCreatePLString(s: *const c_char) -> *mut PropList { + if s.is_null() { + return ptr::null_mut(); + } + let s = unsafe { CStr::from_ptr(s) }; + Box::leak(Box::new(PropList::new(Node::String(s.into())))) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMCreatePLData(data: *mut Data) -> *mut PropList { + if data.is_null() { + return ptr::null_mut(); + } + let data = unsafe { &*data }; + data.with_bytes(|b| Box::leak(Box::new(PropList::new(Node::Data(Vec::from(b)))))) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMCreatePLDataWithBytes( + bytes: *const c_uchar, + length: c_uint, + ) -> *mut PropList { + if bytes.is_null() { + return ptr::null_mut(); + } + let bytes = unsafe { &*ptr::slice_from_raw_parts(bytes.cast::(), length as usize) }; + Box::leak(Box::new(PropList::new(Node::Data(Vec::from(bytes))))) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMCreatePLArrayFromSlice( + elems: *mut PropList, + length: c_uint, + ) -> *mut PropList { + if elems.is_null() { + return ptr::null_mut(); + } + let elems = unsafe { &*ptr::slice_from_raw_parts(elems, length as usize) }; + Box::leak(Box::new(PropList::new(Node::Array( + elems.iter().cloned().collect(), + )))) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMCreateEmptyPLArray() -> *mut PropList { + Box::leak(Box::new(PropList::new(Node::Array(Vec::new())))) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMCreatePLDictionary( + key: *mut PropList, + value: *mut PropList, + ) -> *mut PropList { + if key.is_null() || value.is_null() { + return Box::leak(Box::new(PropList::new(Node::Dictionary(HashMap::new())))); + } + let key = unsafe { (*key).clone() }; + let value = unsafe { (*value).clone() }; + Box::leak(Box::new(PropList::new(Node::Dictionary( + [(key, value)].into(), + )))) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMCreateEmptyPLDictionary() -> *mut PropList { + Box::leak(Box::new(PropList::new(Node::Dictionary(HashMap::new())))) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMRetainPropList(plist: *mut PropList) -> *mut PropList { + if plist.is_null() { + return ptr::null_mut(); + } + unsafe { Box::leak(Box::new((*plist).clone())) } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMReleasePropList(plist: *mut PropList) { + if plist.is_null() { + return; + } + let _ = unsafe { ptr::read(plist) }; + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMInsertInPLArray( + plist: *mut PropList, + index: c_int, + item: *mut PropList, + ) { + if plist.is_null() || index < 0 || item.is_null() { + return; + } + let plist = unsafe { &mut *plist }; + if let Node::Array(ref mut items) = *plist.0.borrow_mut() { + let item = unsafe { (*item).clone() }; + items.insert(index as usize, item); + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMAddToPLArray(plist: *mut PropList, item: *mut PropList) { + if plist.is_null() || item.is_null() { + return; + } + let plist = unsafe { &mut *plist }; + if let Node::Array(ref mut items) = *plist.0.borrow_mut() { + let item = unsafe { (*item).clone() }; + items.push(item); + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMDeleteFromPLArray(plist: *mut PropList, index: c_int) { + if plist.is_null() || index < 0 { + return; + } + let plist = unsafe { &mut *plist }; + if let Node::Array(ref mut items) = *plist.0.borrow_mut() { + items.remove(index as usize); + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMRemoveFromPLArray(plist: *mut PropList, item: *mut PropList) { + if plist.is_null() || item.is_null() { + return; + } + let plist = unsafe { &mut *plist }; + let item = unsafe { &*item }; + if let Node::Array(ref mut items) = *plist.0.borrow_mut() { + if let Some((i, _)) = items.iter().enumerate().find(|(_, x)| *x == item) { + items.remove(i); + } + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMPutInPLDictionary( + plist: *mut PropList, + key: *mut PropList, + value: *mut PropList, + ) { + if plist.is_null() || key.is_null() || value.is_null() { + return; + } + let plist = unsafe { &mut *plist }; + if let Node::Dictionary(ref mut items) = *plist.0.borrow_mut() { + let key = unsafe { (*key).clone() }; + let value = unsafe { (*value).clone() }; + items.insert(key, value); + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMRemoveFromPLDictionary(plist: *mut PropList, key: *mut PropList) { + if plist.is_null() || key.is_null() { + return; + } + let plist = unsafe { &mut *plist }; + let key = unsafe { &*key }; + if let Node::Dictionary(ref mut items) = *plist.0.borrow_mut() { + items.remove(key); + } + } + + /// If `dest` and `source` are both dictionaries, overwrites entries in + /// `dest` with corresponding entries in `source`. + /// + /// If `recursive` is non-zero, this is done recursively for values in + /// `dest` and `source` that are both dictionaries. + /// + /// ## Rust rewrite notes + /// + /// This operation is used a few times. It may be worth keeping around + /// longer-term, although it might be hard to express if we do transition + /// away from `PropList`s to statically typed struct trees. + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMMergePLDictionaries( + dest: *mut PropList, + source: *mut PropList, + recursive: c_int, + ) -> *mut PropList { + if dest.is_null() || ptr::eq(dest, source) || source.is_null() { + return dest; + } + + let dest = unsafe { (*dest).clone() }; + let source = unsafe { (*source).clone() }; + + if recursive == 0 { + merge_shallow(dest.clone(), source); + } else { + merge_deep(dest.clone(), source); + } + + return Box::leak(Box::new(dest)); + } + + /// If `dest` and `source` are both dictionaries, removes from `dest` any + /// `(k, v)` pairs where `dest[k] == source[k]`. + /// + /// If `recursive` is non-zero, this is done recursively over subtrees of + /// `dest` and `source` when both `dest` and `source` are dictionaries for + /// keys of `source` that are also keys of `dest`. + /// + /// ## Rust rewrite notes + /// + /// This operation is only used in one place. It may be better to implement + /// this behavior as a one-off closer to where it is used, or with a + /// different API differently (e.g., as a function of a more general + /// proplist diff). + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMSubtractPLDictionaries( + dest: *mut PropList, + source: *mut PropList, + recursive: c_int, + ) -> *mut PropList { + if dest.is_null() { + return ptr::null_mut(); + } + if source.is_null() { + return dest; + } + + let dest = unsafe { (*dest).clone() }; + let source = unsafe { (*source).clone() }; + + if recursive == 0 { + subtract_shallow(dest.clone(), source); + } else { + subtract_deep(dest.clone(), source); + } + + Box::leak(Box::new(dest)) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMGetPropListItemCount(plist: *mut PropList) -> c_int { + if plist.is_null() { + return 0; + } + let plist = unsafe { &*plist }; + match &*plist.0.borrow() { + Node::Array(xs) => xs.len() as c_int, + Node::Dictionary(xs) => xs.len() as c_int, + _ => 0, + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMIsPLString(plist: *mut PropList) -> c_int { + if plist.is_null() { + return 0; + } + let plist = unsafe { &*plist }; + match &*plist.0.borrow() { + Node::String(_) => 1, + _ => 0, + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMIsPLData(plist: *mut PropList) -> c_int { + if plist.is_null() { + return 0; + } + let plist = unsafe { &*plist }; + match &*plist.0.borrow() { + Node::Data(_) => 1, + _ => 0, + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMIsPLArray(plist: *mut PropList) -> c_int { + if plist.is_null() { + return 0; + } + let plist = unsafe { &*plist }; + match &*plist.0.borrow() { + Node::Array(_) => 1, + _ => 0, + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMIsPLDictionary(plist: *mut PropList) -> c_int { + if plist.is_null() { + return 0; + } + let plist = unsafe { &*plist }; + match &*plist.0.borrow() { + Node::Dictionary(_) => 1, + _ => 0, + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMIsPropListEqualTo(a: *mut PropList, b: *mut PropList) -> c_int { + if ptr::eq(a, b) { + return 1; + } + if a.is_null() { + return 0; + } + let a = unsafe { &*a }; + let b = unsafe { &*b }; + if a == b { + 1 + } else { + 0 + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMGetFromPLString(plist: *mut PropList) -> *const c_char { + if plist.is_null() { + return ptr::null_mut(); + } + let plist = unsafe { &*plist }; + if let Node::String(ref s) = *plist.0.borrow() { + s.as_ref().as_ptr().cast::() + } else { + ptr::null() + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMGetFromPLArray(plist: *mut PropList, index: c_int) -> *mut PropList { + if plist.is_null() || index < 0 { + return ptr::null_mut(); + } + let plist = unsafe { &*plist }; + if let Node::Array(ref items) = *plist.0.borrow() { + Box::leak(Box::new(items[index as usize].clone())) + } else { + ptr::null_mut() + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMGetFromPLDictionary(plist: *mut PropList, key: *mut PropList) -> *mut PropList { + if plist.is_null() || key.is_null() { + return ptr::null_mut(); + } + let plist = unsafe { &*plist }; + let key = unsafe { &*key }; + if let Node::Dictionary(ref items) = *plist.0.borrow() { + if let Some(item) = items.get(key) { + return Box::leak(Box::new(item.clone())); + } + } + ptr::null_mut() + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMGetPLDictionaryKeys(plist: *mut PropList) -> *mut PropList { + if plist.is_null() { + return ptr::null_mut(); + } + let plist = unsafe { &*plist }; + + if let Node::Dictionary(ref items) = *plist.0.borrow() { + return Box::leak(Box::new(PropList::new(Node::Array(items.keys().cloned().collect())))); + } + ptr::null_mut() + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMDeepCopyPropList(plist: *mut PropList) -> *mut PropList { + if plist.is_null() { + return ptr::null_mut(); + } + let plist = unsafe { &*plist }; + Box::leak(Box::new(plist.deep_clone())) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMCreatePropListFromDescription(desc: *const c_char) -> *mut PropList { + if desc.is_null() { + return ptr::null_mut(); + } + let desc = unsafe { CStr::from_ptr(desc) }; + let Ok(desc) = desc.to_str() else { + return ptr::null_mut(); + }; + + match parser::from_str(desc) { + Ok(plist) => Box::leak(Box::new(plist)), + Err(_) => ptr::null_mut(), + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMGetPropListDescription( + plist: *mut PropList, + indented: c_int, + ) -> *mut c_char { + use std::io::Write; + + if plist.is_null() { + return ptr::null_mut(); + } + let plist = unsafe { &*plist }; + let mut buf = Vec::new(); + if indented != 0 { + if let Err(_) = write!(&mut buf, "{}", plist.display_indented()) { + return ptr::null_mut(); + } + } else { + if let Err(_) = write!(&mut buf, "{}", plist.display_unindented()) { + return ptr::null_mut(); + } + } + match CString::new(buf) { + Ok(s) => s.into_raw(), + Err(_) => ptr::null_mut(), + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMReadPropListFromFile(path: *const c_char) -> *mut PropList { + if path.is_null() { + return ptr::null_mut(); + } + let path = unsafe { CStr::from_ptr(path) }; + let Ok(path) = path.to_str() else { + return ptr::null_mut(); + }; + match PropList::from_file(path) { + Ok(plist) => Box::leak(Box::new(plist)), + Err(_) => { + // TODO: print error message. + ptr::null_mut() + } + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMReadPropListFromPipe(command: *const c_char) -> *mut PropList { + if command.is_null() { + return ptr::null_mut(); + } + + let command = unsafe { CStr::from_ptr(command) }; + let Ok(command) = command.to_str() else { + return ptr::null_mut(); + }; + let command = OsString::from_str(command).unwrap(); + + let Ok(output) = std::process::Command::new("/bin/sh") + .arg("-c") + .arg(command) + .output() + else { + // TODO: print error message. + return ptr::null_mut(); + }; + let Ok(output) = String::from_utf8(output.stdout) else { + // TODO: print error message. + return ptr::null_mut(); + }; + match parser::from_str(&output) { + Ok(plist) => Box::leak(Box::new(plist)), + Err(_) => { + // TODO: print error message. + ptr::null_mut() + } + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn WMWritePropListToFile( + plist: *mut PropList, + path: *const c_char, + ) -> c_int { + if plist.is_null() || path.is_null() { + return 0; + } + let plist = unsafe { + &*plist + }; + + let path = unsafe { + CStr::from_ptr(path) + }; + let Some(path) = path_from_cstr(path) else { + // TODO: complain. + return 0; + }; + + match plist.write_to_file(&path) { + Ok(_) => return 1, + Err(_) => { + // TODO: complain. + return 0; + } + } + } +} diff --git a/wutil-rs/src/prop_list/parser.rs b/wutil-rs/src/prop_list/parser.rs new file mode 100644 index 00000000..92d37998 --- /dev/null +++ b/wutil-rs/src/prop_list/parser.rs @@ -0,0 +1,1463 @@ +//! Support for reading NeXTSTEP/OPENSTEP/Window Maker property list files. +//! +//! Files are parsed into the WINGs-internal [`PropList`] structure, which does +//! not support GNUstep extensions for special data types (`NSValue`, `NSDate`, +//! etc.) or comments. +//! +//! This is not exactly the same as the original WINGs parser, but it should +//! handle Window Maker configurations just fine. Notable differences include: +//! +//! * A single trailing backslash in quoted strings is not accepted (and a +//! properly escaped backslash must be used). +//! * Strings are UTF-8 (and input must be in UTF-8). Files with strings that +//! contain a zero byte will be rejected because we still use C-style strings +//! internally to support FFI with code that has not yet been ported to Rust. + +use std::{collections::HashMap, ffi::CString}; + +use nom::{ + branch::alt, + character::complete::{char, multispace0, none_of, satisfy}, + combinator::{cut, eof, fail, map, map_res, opt}, + error::context, + multi::{fold_many0, many0, many1, many_m_n, separated_list0}, + sequence::{delimited, preceded, terminated}, + AsChar, Finish, IResult, Input, Parser, +}; +use nom_language::error::{convert_error, VerboseError}; + +use super::{Node, PropList}; + +fn read_dictionary_key>(input: I) -> IResult> { + preceded( + multispace0, + terminated( + alt((read_data, read_string)), + cut(context( + "looking for '=' after dictionary key", + (multispace0, char('='), multispace0), + )), + ), + ) + .parse(input) +} + +fn read_dictionary_value>( + input: I, +) -> IResult> { + terminated( + delimited(multispace0, read_prop_list, multispace0), + cut(context( + "looking for ';' after dictionary value", + (char(';'), multispace0), + )), + ) + .parse(input) +} + +fn read_dictionary>(input: I) -> IResult> { + delimited( + context( + "looking for dictionary to start with '{'", + (multispace0, char('{'), multispace0), + ), + map( + fold_many0( + (read_dictionary_key, read_dictionary_value), + HashMap::new, + |mut items: HashMap, (k, v): (PropList, PropList)| { + items.insert(k, v); + items + }, + ), + |items: HashMap| PropList::new(Node::Dictionary(items)), + ), + cut(context( + "looking for dictionary to end with '}'", + (multispace0, char('}'), multispace0), + )), + ) + .parse(input) +} + +fn read_array_elements>( + input: I, +) -> IResult, VerboseError> { + delimited( + multispace0, + separated_list0( + context("looking for comma between array elements", char(',')), + delimited(multispace0, read_prop_list, multispace0), + ), + multispace0, + ) + .parse(input) +} + +fn read_array>(input: I) -> IResult> { + map( + preceded( + (multispace0, char('(')), + terminated( + read_array_elements, + ( + multispace0, + opt(char(',')), + multispace0, + cut(context("looking for array that ends with ')'", char(')'))), + ), + ), + ), + |items: Vec| PropList::new(Node::Array(items)), + ) + .parse(input) +} + +fn read_data_byte>(input: I) -> IResult> { + map( + delimited( + multispace0, + ( + context( + "looking for first hex value", + satisfy(|c: char| c.is_ascii_hexdigit()), + ), + cut(context( + "looking for second hex value", + satisfy(|c: char| c.is_ascii_hexdigit()), + )), + ), + multispace0, + ), + |(upper, lower)| { + let s = &[upper as u8, lower as u8]; + // Safety: upper and lower are ASCII hexdigits, so they + // should fit into a str without validation and provide + // exactly 8 bits of value. + u8::from_str_radix(unsafe { str::from_utf8_unchecked(s) }, 16).unwrap() + }, + ) + .parse(input) +} + +fn read_data>(input: I) -> IResult> { + map( + delimited( + (multispace0, char('<')), + many0(read_data_byte), + cut(context( + "looking for data to end with '>'", + (multispace0, char('>')), + )), + ), + |bytes: Vec| PropList::new(Node::Data(bytes)), + ) + .parse(input) +} + +fn unescape_character(c: char) -> char { + match c { + '\\' => '\\', + '"' => '"', + 'a' => '\x07', + 'b' => '\x08', + 't' => '\t', + 'n' => '\n', + 'v' => '\x0B', + 'f' => '\x0D', + x @ _ => x, + } +} + +fn unescape_octal(bytes: Vec) -> Result { + let mut result = 0u32; + for b in &bytes { + result <<= 3; + result |= (*b as u32) & 0o7; + } + char::from_u32(result).ok_or_else(|| { + format!( + "unable to convert octal escape sequence '{:?}' to character", + &bytes + ) + }) +} + +fn read_quoted_char>(input: I) -> IResult> { + alt(( + preceded( + char('\\'), + cut(alt(( + map( + context( + "looking for an escaped character", + satisfy(|c: char| !c.is_oct_digit()), + ), + unescape_character, + ), + map_res( + context( + "looking for an octal sequence (0-7, up to 3 times)", + many_m_n(1, 3, satisfy(|c| c.is_oct_digit())), + ), + unescape_octal, + ), + ))), + ), + // Characters that do not need escaping. + none_of(r#""\"#), + )) + .parse(input) +} + +fn read_nullbyte_char>( + input: I, +) -> IResult> { + map_res(read_quoted_char, |c: char| { + let mut bytes = [0u8; 4]; + let s = c.encode_utf8(&mut bytes).as_bytes(); + for b in s { + if *b == 0 { + let len = s.len(); + return Ok((bytes, len)); + } + } + Err(()) + }) + .parse(input) +} + +fn read_char_bytes>( + input: I, +) -> IResult> { + map(read_quoted_char, |c: char| { + let mut bytes = [0u8; 4]; + let s = c.encode_utf8(&mut bytes).as_bytes(); + let len = s.len(); + (bytes, len) + }) + .parse(input) +} + +fn read_quoted_string>(input: I) -> IResult> { + map( + fold_many0( + alt(( + terminated( + // First we try to read a char with a null byte. If we do, + // unconditionally fail. + read_nullbyte_char, + cut(context( + "null bytes are not allowed in strings", + fail::>(), + )), + ), + // Character is valid, so proceed. + read_char_bytes, + )), + Vec::new, + |mut buf: Vec, (bytes, len): ([u8; 4], usize)| { + buf.extend_from_slice(&bytes[0..len]); + buf + }, + ), + |buf: Vec| unsafe { CString::from_vec_unchecked(buf) }, + ) + .parse(input) +} + +pub(crate) fn is_unquoted_char(c: char) -> bool { + c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '/' || c == '+' +} + +fn read_unquoted_string>(input: I) -> IResult> { + map( + many1(map(satisfy(is_unquoted_char), |c: char| c as u8)), + |bytes: Vec| unsafe { CString::from_vec_unchecked(bytes) }, + ) + .parse(input) +} + +fn read_string>(input: I) -> IResult> { + map( + delimited( + multispace0, + alt(( + delimited( + char('"'), + read_quoted_string, + cut(context("looking for closing \" for string", char('"'))), + ), + read_unquoted_string, + )), + multispace0, + ), + |s: CString| PropList::new(Node::String(s)), + ) + .parse(input) +} + +fn read_prop_list>(input: I) -> IResult> { + delimited( + multispace0, + alt((read_array, read_data, read_dictionary, read_string)), + multispace0, + ) + .parse(input) +} + +fn is_context_error(e: &nom_language::error::VerboseErrorKind) -> bool { + if let nom_language::error::VerboseErrorKind::Context(_) = e { + true + } else { + false + } +} + +/// Reads a `PropList` from `s`. Returns a human-readable error message if +/// there's an error. +pub fn from_str(s: &str) -> Result { + terminated( + read_prop_list, + ( + multispace0, + cut(context( + "looking for complete property list at start of input, with no trailing garbage", + eof, + )), + ), + ) + .parse(s) + .finish() + .map(|(_, plist)| plist) + .map_err(|e| { + if let Some(e) = e.errors.iter().filter(|(_, e)| is_context_error(e)).last() { + // Drop all but the deepest context error from the VerboseError trace. + convert_error( + s, + VerboseError { + errors: vec![e.clone()], + }, + ) + } else { + convert_error(s, e) + } + }) +} + +#[cfg(test)] +mod test { + use std::ffi::CString; + + use crate::prop_list::{parser::from_str, Node, PropList}; + + fn pl_array(xs: Vec) -> PropList { + PropList::new(Node::Array(xs)) + } + + fn pl_dict(kvs: Vec<(PropList, PropList)>) -> PropList { + PropList::new(Node::Dictionary(kvs.into_iter().collect())) + } + + fn pl_string(s: &str) -> PropList { + PropList::new(Node::String(CString::new(s.as_bytes()).unwrap())) + } + + #[test] + fn parse_data() { + let plist = from_str("").unwrap(); + assert_eq!( + plist, + PropList::new(Node::Data(vec![0xde, 0xad, 0xbe, 0xef])) + ); + } + + #[test] + fn error_unclosed_data() { + let e = from_str("': +': +, +^ + +"# + ); + } + + #[test] + fn error_incomplete_data_byte() { + let e = from_str("> { + /// Line-breaking strategy. + pub(crate) inline: Inline, + /// Whether to render `node` as if it is the first item on a new line. + pub(crate) clear_left: bool, + /// Indentation level to render `node` at. + pub(crate) indentation: u32, + /// Amount by which `indentation` should increase when rendering children of + /// `node`. + pub(crate) increment: u32, + /// The `Node` to render. + pub(crate) node: N, +} + +/// Quick and dirty single-branch lookup mapping `b` to a hex character. Only +/// valid for `b` in `[0, 15]`. +fn byte_char(b: u8) -> char { + match b { + 0 => '0', + 1 => '1', + 2 => '2', + 3 => '3', + 4 => '4', + 5 => '5', + 6 => '6', + 7 => '7', + 8 => '8', + 9 => '9', + 10 => 'a', + 11 => 'b', + 12 => 'c', + 13 => 'd', + 14 => 'e', + 15 => 'f', + _ => unreachable!(), + } +} + +impl> Display { + /// Writes whitespace to `f` for the current level of indentation. + fn write_indent(&self, f: &mut fmt::Formatter) -> fmt::Result { + for _ in 0..self.indentation { + f.write_char(' ')?; + } + Ok(()) + } +} + +/// Writes a hexadecimal representation of `bytes` to `f`, splitting `bytes` up +/// into space-delimited 32-bit quartets for readability. +fn write_data_bytes(bytes: &[u8], f: &mut fmt::Formatter) -> fmt::Result { + fn write(b: u8, f: &mut fmt::Formatter) -> fmt::Result { + let upper = (b >> 3) as u8; + let lower = (b & 0x0F) as u8; + write!(f, "{}", byte_char(upper))?; + write!(f, "{}", byte_char(lower)) + } + + let mut chunks = bytes.chunks(4); + if let Some(first) = chunks.next() { + for b in first { + write(*b, f)?; + } + } + for seg in chunks { + f.write_char(' ')?; + for b in seg { + write(*b, f)?; + } + } + + Ok(()) +} + +/// Writes `s` to `f`, backslash-escaping special characters as appropriate for +/// a quoted property list string. +fn write_escaped_string(s: &str, f: &mut fmt::Formatter) -> fmt::Result { + for c in s.chars() { + match c { + '\x07' => f.write_str(r#"\a"#)?, + '\x08' => f.write_str(r#"\b"#)?, + '\t' => f.write_str(r#"\t"#)?, + '\n' => f.write_str(r#"\n"#)?, + '\x0B' => f.write_str(r#"\v"#)?, + '\x0D' => f.write_str(r#"\f"#)?, + '\\' => f.write_str(r#"\\"#)?, + '"' => f.write_str(r#"\\""#)?, + c => f.write_char(c)?, + } + } + Ok(()) +} + +impl> fmt::Display for Display { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.clear_left { + writeln!(f, "")?; + self.write_indent(f)?; + } + match &*self.node { + Node::Array(items) if self.inline != Inline::No => { + // Try to fit everything in one line. + let mut buf = String::new(); + buf.push('('); + let mut items = items.iter(); + if let Some(first) = items.next() { + write!( + &mut buf, + "{}", + Display { + inline: self.inline, + clear_left: false, + indentation: self.indentation, + increment: self.increment, + node: first.0.borrow(), + } + )?; + } + for next in items { + write!( + &mut buf, + ", {}", + Display { + inline: self.inline, + clear_left: false, + indentation: self.indentation, + increment: self.increment, + node: next.0.borrow(), + } + )?; + } + buf.push(')'); + if self.inline == Inline::Soft && buf.chars().count() > SOFT_LINEBREAK_WIDTH { + write!( + f, + "{}", + Display { + inline: Inline::No, + clear_left: false, + indentation: self.indentation, + increment: self.increment, + node: &*self.node, + } + )?; + } else { + f.write_str(&buf)?; + } + } + Node::Array(items) if items.is_empty() => f.write_str("()")?, + Node::Array(items) => { + f.write_char('(')?; + let mut items = items.iter(); + if let Some(first) = items.next() { + write!( + f, + "{}", + Display { + inline: Inline::Soft, + clear_left: true, + indentation: self.indentation + self.increment, + increment: self.increment, + node: first.0.borrow(), + } + )?; + } + for next in items { + f.write_char(',')?; + write!( + f, + "{}", + Display { + inline: Inline::Soft, + clear_left: true, + indentation: self.indentation + self.increment, + increment: self.increment, + node: next.0.borrow(), + } + )?; + } + writeln!(f, "")?; + self.write_indent(f)?; + f.write_char(')')?; + } + Node::Data(bytes) => { + write!(f, "<")?; + write_data_bytes(bytes, f)?; + write!(f, ">")?; + } + Node::Dictionary(items) if items.is_empty() => write!(f, "{{}}")?, + Node::Dictionary(items) if self.inline != Inline::No => { + // Try to fit everything in one line. + let mut buf = String::new(); + buf.push('{'); + for (k, v) in items { + write!( + &mut buf, + " {} = {};", + Display { + inline: self.inline, + clear_left: false, + indentation: self.indentation, + increment: self.increment, + node: k.0.borrow(), + }, + Display { + inline: self.inline, + clear_left: false, + indentation: self.indentation, + increment: self.increment, + node: v.0.borrow(), + } + )?; + } + write!(&mut buf, " }}")?; + if self.inline == Inline::Soft && buf.chars().count() > SOFT_LINEBREAK_WIDTH { + write!( + f, + "{}", + Display { + inline: Inline::No, + clear_left: false, + indentation: self.indentation, + increment: self.increment, + node: &*self.node, + } + )?; + } else { + f.write_str(&buf)?; + } + } + Node::Dictionary(items) => { + write!(f, "{{")?; + for (k, v) in items { + write!( + f, + "{} = {};", + Display { + inline: Inline::Soft, + clear_left: true, + indentation: self.indentation + self.increment, + increment: self.increment, + node: k.0.borrow(), + }, + Display { + inline: Inline::Soft, + clear_left: false, + indentation: self.indentation + self.increment, + increment: self.increment, + node: v.0.borrow(), + } + )?; + } + writeln!(f, "")?; + self.write_indent(f)?; + f.write_char('}')?; + } + Node::String(s) => { + let mut unquoted = !s.is_empty(); + for c in s.as_bytes() { + if !parser::is_unquoted_char(*c as char) { + unquoted = false; + break; + } + } + if unquoted { + write!(f, "{}", s.to_string_lossy())?; + } else { + f.write_char('"')?; + write_escaped_string(&s.to_string_lossy(), f)?; + f.write_char('"')?; + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::prop_list::{Node, PropList}; + + use std::ffi::CString; + + fn pl_array(xs: Vec) -> PropList { + PropList::new(Node::Array(xs)) + } + + fn pl_string(s: &str) -> PropList { + PropList::new(Node::String(CString::new(s.as_bytes()).unwrap())) + } + + #[test] + fn write_empty_string() { + let serialized = format!("{:?}", pl_string("")); + assert_eq!(serialized, r#""""#); + } + + #[test] + fn write_unquoted_string() { + let serialized = format!("{:?}", pl_string("hello")); + assert_eq!(serialized, r#"hello"#); + } + + #[test] + fn write_quoted_string() { + let serialized = format!("{:?}", pl_string("hello, world!")); + assert_eq!(serialized, r#""hello, world!""#); + } + + #[test] + fn write_flat_array() { + let serialized = format!( + "{:?}", + pl_array(vec![pl_string("hello"), pl_string("world")]) + ); + assert_eq!(serialized, r#"(hello, world)"#); + } + + #[test] + fn write_empty_array() { + let serialized = format!("{:?}", pl_array(vec![])); + assert_eq!(serialized, r#"()"#); + } + + #[test] + fn write_nested_array() { + let serialized = format!( + "{:?}", + pl_array(vec![ + pl_array(vec![pl_string("hello"), pl_string("world")]), + pl_string("baz"), + pl_string("quux plugh") + ]) + ); + assert_eq!(serialized, r#"((hello, world), baz, "quux plugh")"#); + } + + #[test] + fn write_long_nested_array() { + let serialized = format!( + "{:?}", + pl_array(vec![ + pl_array(vec![pl_string("hello"), pl_string("world")]), + pl_string("baz"), + pl_string("quux plugh"), + pl_string("etc"), + pl_array(vec![ + pl_string("lots"), + pl_string("of"), + pl_string("words"), + pl_string("go"), + pl_array(vec![ + pl_string("here"), + pl_string("and"), + pl_string("stuff"), + pl_string("blah"), + pl_string("blah"), + pl_string("blah"), + ]) + ]) + ]) + ); + assert_eq!( + serialized, + r#"( + (hello, world), + baz, + "quux plugh", + etc, + (lots, of, words, go, (here, and, stuff, blah, blah, blah)) +)"# + ); + } + + #[test] + fn roundtrip_wm_state() { + let original = r#"{ + Dock = { + AutoRaiseLower = No; + Applications = ( + { + Forced = No; + Name = Logo.WMDock; + BuggyApplication = No; + AutoLaunch = No; + Position = "0,0"; + Lock = Yes; + Command = "/usr/bin/WPrefs"; + }, + { + Forced = No; + Name = wmweather.wmweather; + DropCommand = "wmweather -s KBOS %d"; + BuggyApplication = No; + AutoLaunch = Yes; + Position = "0,4"; + Lock = Yes; + PasteCommand = "wmweather -s KBOS %s"; + Command = "wmweather -s KBOS"; + }, + { + Forced = No; + Name = wmclock.WMClock; + DropCommand = "wmclock %d"; + BuggyApplication = No; + AutoLaunch = Yes; + Position = "0,3"; + Lock = Yes; + PasteCommand = "wmclock %s"; + Command = wmclock; + }, + { + Forced = No; + Name = emacs.Emacs; + DropCommand = "emacsclient -c %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "0,2"; + Lock = Yes; + PasteCommand = "emacsclient -c %s"; + Command = "emacsclient -"; + }, + { + Forced = No; + Name = wmfire.wmfire; + DropCommand = "wmfire -m %d"; + BuggyApplication = No; + AutoLaunch = Yes; + Position = "0,5"; + Lock = Yes; + PasteCommand = "wmfire -m %s"; + Command = "wmfire -m"; + }, + { + Forced = No; + Name = wmnet.WMNET; + DropCommand = "wmnet -W enp0s31f6 %d"; + BuggyApplication = No; + AutoLaunch = Yes; + Position = "0,7"; + Lock = Yes; + PasteCommand = "wmnet -W enp0s31f6 %s"; + Command = "wmnet -W enp0s31f6"; + }, + { + Forced = No; + Name = wmtemp.DockApp; + DropCommand = "wmtemp -f %d"; + BuggyApplication = No; + AutoLaunch = Yes; + Position = "0,6"; + Lock = Yes; + PasteCommand = "wmtemp -f %s"; + Command = "wmtemp -f"; + }, + { + Forced = No; + Name = firefox.Firefox; + DropCommand = "firefox %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "0,1"; + Lock = Yes; + PasteCommand = "firefox %s"; + Command = firefox; + }, + { + Forced = No; + Name = "org\\.gnome\\.Weather.org\\.gnome\\.Weather"; + DropCommand = "org.gnome.Weather %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "0,8"; + Lock = Yes; + PasteCommand = "org.gnome.Weather %s"; + Command = "/usr/bin/gnome-weather"; + }, + { + Forced = No; + Name = Zotero.Zotero; + DropCommand = "/home/stu/bin/zotero %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "0,9"; + Lock = Yes; + PasteCommand = "/home/stu/bin/zotero %s"; + Command = "/home/stu/bin/zotero"; + } + ); + Lowered = Yes; + Position = "2496,0"; + Applications1440 = ( + { + Forced = No; + Name = Logo.WMDock; + BuggyApplication = No; + AutoLaunch = No; + Position = "0,0"; + Lock = Yes; + Command = "/usr/bin/WPrefs"; + }, + { + Forced = No; + Name = wmweather.wmweather; + DropCommand = "wmweather -s KBOS %d"; + BuggyApplication = No; + AutoLaunch = Yes; + Position = "0,4"; + Lock = Yes; + PasteCommand = "wmweather -s KBOS %s"; + Command = "wmweather -s KBOS"; + }, + { + Forced = No; + Name = wmclock.WMClock; + DropCommand = "wmclock %d"; + BuggyApplication = No; + AutoLaunch = Yes; + Position = "0,3"; + Lock = Yes; + PasteCommand = "wmclock %s"; + Command = wmclock; + }, + { + Forced = No; + Name = emacs.Emacs; + DropCommand = "emacsclient -c %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "0,2"; + Lock = Yes; + PasteCommand = "emacsclient -c %s"; + Command = "emacsclient -"; + }, + { + Forced = No; + Name = wmfire.wmfire; + DropCommand = "wmfire -m %d"; + BuggyApplication = No; + AutoLaunch = Yes; + Position = "0,5"; + Lock = Yes; + PasteCommand = "wmfire -m %s"; + Command = "wmfire -m"; + }, + { + Forced = No; + Name = wmnet.WMNET; + DropCommand = "wmnet -W enp0s31f6 %d"; + BuggyApplication = No; + AutoLaunch = Yes; + Position = "0,7"; + Lock = Yes; + PasteCommand = "wmnet -W enp0s31f6 %s"; + Command = "wmnet -W enp0s31f6"; + }, + { + Forced = No; + Name = wmtemp.DockApp; + DropCommand = "wmtemp -f %d"; + BuggyApplication = No; + AutoLaunch = Yes; + Position = "0,6"; + Lock = Yes; + PasteCommand = "wmtemp -f %s"; + Command = "wmtemp -f"; + }, + { + Forced = No; + Name = firefox.Firefox; + DropCommand = "firefox %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "0,1"; + Lock = Yes; + PasteCommand = "firefox %s"; + Command = firefox; + }, + { + Forced = No; + Name = "org\\.gnome\\.Weather.org\\.gnome\\.Weather"; + DropCommand = "org.gnome.Weather %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "0,8"; + Lock = Yes; + PasteCommand = "org.gnome.Weather %s"; + Command = "/usr/bin/gnome-weather"; + }, + { + Forced = No; + Name = Zotero.Zotero; + DropCommand = "/home/stu/bin/zotero %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "0,9"; + Lock = Yes; + PasteCommand = "/home/stu/bin/zotero %s"; + Command = "/home/stu/bin/zotero"; + } + ); + }; + Clip = { + Forced = No; + Name = Logo.WMClip; + DropCommand = "wmsetbg -u -t %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "2496,1376"; + Lock = No; + Command = "-"; + }; + Drawers = (); + Workspaces = ( + { + Clip = { + AutoRaiseLower = No; + AutoCollapse = No; + Applications = ( + { + Forced = No; + Name = xsnow.Xsnow; + DropCommand = "xsnow %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "-2,0"; + Lock = No; + PasteCommand = "xsnow %s"; + Command = xsnow; + Omnipresent = No; + }, + { + Forced = No; + Name = "thunderbird.Thunderbird-default"; + DropCommand = "thunderbird %d"; + BuggyApplication = No; + AutoLaunch = No; + Position = "-1,0"; + Lock = No; + PasteCommand = "thunderbird %s"; + Command = thunderbird; + Omnipresent = No; + } + ); + Collapsed = No; + AutoAttractIcons = No; + Lowered = Yes; + }; + Name = Mail; + }, + { + Clip = { + AutoRaiseLower = No; + AutoCollapse = No; + Applications = (); + Collapsed = No; + AutoAttractIcons = No; + Lowered = Yes; + }; + Name = Plans; + }, + { + Clip = { + AutoRaiseLower = No; + AutoCollapse = No; + Applications = (); + Collapsed = No; + AutoAttractIcons = No; + Lowered = Yes; + }; + Name = Misc; + }, + ); +}"#; + + // We do (original serialized) -> plist -> (serialized) -> plist and + // compare the two plists because Rust's hashtable ordering (which is + // reflected in the serialized output) does not match the original + // WUtils hashtable ordering. + let original_plist = super::parser::from_str(original).unwrap(); + let serialized = format!("{}", original_plist.display_indented()); + let deserialized = super::parser::from_str(&serialized).unwrap(); + assert_eq!(original_plist, deserialized); + } +}