/** Property list utility. Copyright (C) 2020 Free Software Foundation, Inc. Written by: Mingye Wang Created: feb 2020 This file is part of the GNUstep Project This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. You should have received a copy of the GNU General Public License along with this program; see the file COPYINGv3. If not, write to the Free Software Foundation, 31 Milk Street #960789 Boston, MA 02196 USA. */ // #import "common.h" #include #import "Foundation/NSArray.h" #import "Foundation/NSAutoreleasePool.h" #import "Foundation/NSData.h" #import "Foundation/NSDictionary.h" #import "Foundation/NSException.h" #import "Foundation/NSFileHandle.h" #import "Foundation/NSProcessInfo.h" #import "Foundation/NSPropertyList.h" #import "Foundation/NSString.h" #import "Foundation/NSUserDefaults.h" #import "Foundation/NSValue.h" #import "NSPropertyList+PLUtil.h" // From NSPropertyList.m GS_EXPORT void GSPropertyListMake(id, NSDictionary *, BOOL, BOOL, unsigned, id *); // We don't have @[] on gcc #define NARRAY(...) [NSArray arrayWithObjects: __VA_ARGS__, nil] // And no @() or @123 #define NINT(Num) [NSNumber numberWithInt: Num] // Unfortunately we have to define @() for @"" too because a macro later #define NSTR(Str) [NSString stringWithCString: Str] /* Bitmap of 'quotable' characters ... those characters which must be * inside a quoted string if written to an old style property list. * Taken from NSPropertyList.m. */ static const unsigned char quotables[32] = { '\xff', '\xff', '\xff', '\xff', '\x85', '\x13', '\x00', '\x78', '\x00', '\x00', '\x00', '\x38', '\x01', '\x00', '\x00', '\xa8', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', }; #define IS_BIT_SET(a, i) ((((a) & (1 << (i)))) > 0) #define GS_IS_QUOTABLE(X) IS_BIT_SET(quotables[(X) / 8], (X) % 8) /** * Indexes a NSArray or a NSDictionary. */ id plIndex(id obj, NSString *key) { const char *ckey; char *endptr = NULL; NSInteger res; if ([obj isKindOfClass: [NSDictionary class]]) { return [(NSDictionary *) obj objectForKey: key]; } else if ([obj isKindOfClass: [NSArray class]]) { ckey = [key cStringUsingEncoding: [NSString defaultCStringEncoding]]; res = strtoll(ckey, &endptr, 10); if (endptr && *endptr != '\0') { [NSException raise: NSInvalidArgumentException format: @"%@ is not a valid integer", key]; } return [(NSArray *) obj objectAtIndex: res]; } else { [NSException raise: NSInvalidArgumentException format: @"%@ is not indexible", obj]; return nil; } } /** * Mutate obj[key] to leaf. * If leaf is nil, remove. * Else if replace is NO, insert. * Otherwise replace. * * Inserting and replacing are the same for NSMutableDictionary. */ void mutate(id obj, NSString *key, id leaf, BOOL replace) { const char *ckey; char *endptr = NULL; NSInteger res; if ([obj isKindOfClass: [NSMutableDictionary class]]) { if (!leaf) [(NSMutableDictionary *) obj removeObjectForKey: key]; else [(NSMutableDictionary *) obj setObject: leaf forKey: key]; } else if ([obj isKindOfClass: [NSMutableArray class]]) { ckey = [key cStringUsingEncoding: [NSString defaultCStringEncoding]]; res = strtoll(ckey, &endptr, 10); if (endptr && *endptr != '\0') { [NSException raise: NSInvalidArgumentException format: @"%@ is not a valid integer", key]; } if (!leaf) [(NSMutableArray *) obj removeObjectAtIndex: res]; else if (replace) [(NSMutableArray *) obj replaceObjectAtIndex: res withObject: leaf]; else [(NSMutableArray *) obj insertObject: leaf atIndex: res]; } else { [NSException raise: NSInvalidArgumentException format: @"%@ is not indexible", obj]; } } #define KEYPATH_SEP '.' NSString * parseQuotedString(const char *, size_t *); /** * Parses a keypath to its component keys. ISO/IEC 14977 EBNF: *
 * keypath = '' | keypath '.' key;
 * key = quotedString | unquotedStringNotAllowingPeriod;
 * 
* The definitions of strings follow plist conventions. * The use of quoted strings is a GNUstep extension (I think). */ NSArray * parseKeyPath(const char *keypath) { NSMutableArray *res = [NSMutableArray array]; NSString *key = nil; size_t i; size_t j; for (i = 0; keypath[i]; i++) { switch (keypath[i]) { case KEYPATH_SEP: if (key != nil) { [res addObject: key]; key = nil; } break; case '"': key = parseQuotedString(keypath, &i); break; default: j = i; while (keypath[j] && !GS_IS_QUOTABLE(keypath[j]) && keypath[j] != KEYPATH_SEP) { j++; } key = [NSString stringWithCString: &keypath[i] length: (j - i)]; i = j - 1; break; } } return AUTORELEASE([res copy]); } /** * Parse a quoted string by pretending it is a plist. */ NSString * parseQuotedString(const char *keypath, size_t *i) { const char *begin = &keypath[*i]; const char *end; id parsed; // Select the part of the quoted string that looks like a plist string for (end = begin + 1; *end && *end != '"'; end++) { if (*end == '\\') { if (end[1]) { end += 1; } else { [NSException raise: NSInvalidArgumentException format: @"Premature EOF in keypath"]; } } } if (!*end) { [NSException raise: NSInvalidArgumentException format: @"Premature EOF in keypath"]; } *i = end - keypath + 1; parsed = [[NSString stringWithCString: begin length: end-begin] propertyList]; return (NSString *)parsed; } /** * Index by keypath. */ id plIndexKeypath(id obj, NSString *keypath, int depthOffset) { NSArray *parsedPath = parseKeyPath([keypath cString]); int count = [parsedPath count]; int i; for (i = 0; i < count - depthOffset; i++) obj = plIndex(obj, [parsedPath objectAtIndex: i]); return obj; } /** * Interpret -type value options. * * The -plist type is a GNUStep extension. */ id parseValue(NSString *type, NSString *value) { id result; if ([type isEqual: @"-plist"]) { result = [value propertyList]; } else if ([type isEqual: @"-xml"] || [type isEqual: @"-date"]) { NSData *mydata = [value dataUsingEncoding: [NSString defaultCStringEncoding]]; NSPropertyListFormat aFormat; NSError *anError; result = [NSPropertyListSerialization propertyListWithData: mydata options: NSPropertyListMutableContainersAndLeaves format: &aFormat error: &anError]; if (result == nil) { GSPrintf(stderr, @"Parsing plist %@: %@\n", value, anError); } else if ([type isEqual: @"-xml"] && aFormat != NSPropertyListXMLFormat_v1_0) { GSPrintf(stderr, @"Warning: parsing XML plist %@: Not an XML (fmt %d)\n", value, aFormat); } else if ([type isEqual: @"-date"] && ![result isKindOfClass: [NSDate class]]) { GSPrintf(stderr, @"Warning: parsing date %@: Not a date (got %@)\n", value, result); } } else if ([type isEqual: @"-bool"]) { result = [NSNumber numberWithBool: ([value isEqual: @"YES"] || [value isEqual: @"true"])]; } else if ([type isEqual: @"-integer"]) { result = [NSNumber numberWithLongLong: [value longLongValue]]; } else if ([type isEqual: @"-float"]) { /* * We do a step further than NSPropertyList.m and probably Apple, * since notsupporting inf and nan is a bad look * (No hex literals for now unless someone really wants it) */ result = [NSNumber numberWithDouble: strtod([value cString], 0)]; } else if ([type isEqual: @"-data"]) { result = AUTORELEASE([[NSData alloc] initWithBase64EncodedData: [value dataUsingEncoding: [NSString defaultCStringEncoding]] options: NSDataBase64DecodingIgnoreUnknownCharacters]); } else { GSPrintf(stderr, @"Unrecognized type %@\n", type); result = nil; } return result; } #define SELFMAP(Name) NINT(Name), NSTR(#Name) /** * Translates a string fmt to NSPropertyListFormat. */ NSPropertyListFormat plFormatFromName(NSString *name) { // clang-format off NSDictionary *nameMap = [NSDictionary dictionaryWithObjectsAndKeys: NINT(NSPropertyListXMLFormat_v1_0), @"xml1", NINT(NSPropertyListBinaryFormat_v1_0), @"binary1", NINT(NSPropertyListOpenStepFormat), @"openstep", NINT(NSPropertyListGNUstepFormat), @"gnustep", NINT(NSPropertyListGNUstepBinaryFormat), @"gsbinary", NINT(NSPropertyListJSONFormat), @"json", SELFMAP(NSPropertyListOpenStepFormat), SELFMAP(NSPropertyListXMLFormat_v1_0), SELFMAP(NSPropertyListBinaryFormat_v1_0), SELFMAP(NSPropertyListGNUstepFormat), SELFMAP(NSPropertyListGNUstepBinaryFormat), nil]; id res = [nameMap objectForKey: name]; // clang-format on if (!res) [NSException raise: NSInvalidArgumentException format: @"Invalid fmt %@", name]; return [res intValue]; } /** * Dumps obj to outfile. */ int dumpToFile(id obj, NSPropertyListFormat fmt, NSString *outfile) { NSString *errorString = nil; NSFileHandle *fh; NSData *outdata = [NSPropertyListSerialization dataFromPropertyList: obj format: fmt errorDescription: &errorString]; if (errorString) { GSPrintf(stderr, @"Dumping %@ as format %@ - %@\n", obj, fmt, errorString); return EXIT_FAILURE; } if ([outfile isEqual: @"-"]) fh = [NSFileHandle fileHandleWithStandardOutput]; else fh = [NSFileHandle fileHandleForWritingAtPath: outfile]; [fh writeData: outdata]; [fh synchronizeFile]; return EXIT_SUCCESS; } int plCmdConvert(id obj, NSArray *cmdargs, NSString *outfile) { NSString *fmt = [cmdargs objectAtIndex: 0]; return dumpToFile(obj, plFormatFromName(fmt), outfile); } int plCmdExtract(id obj, NSArray *cmdargs, NSString *outfile) { NSString *keypath = [cmdargs objectAtIndex: 0]; NSString *fmt = [cmdargs objectAtIndex: 1]; obj = plIndexKeypath(obj, keypath, 0); return dumpToFile(obj, plFormatFromName(fmt), outfile); } int plCmdRemove(id obj, NSPropertyListFormat fmt, NSArray *cmdargs, NSString *outfile) { NSString *keypath = [cmdargs objectAtIndex: 0]; NSArray *parsedPath = parseKeyPath([keypath cString]); id leaf = plIndexKeypath(obj, keypath, 1); mutate(leaf, [parsedPath lastObject], nil, false); return dumpToFile(obj, fmt, outfile); } int plCmdInsert(id obj, NSPropertyListFormat fmt, NSArray *cmdargs, NSString *outfile) { NSString *keypath = [cmdargs objectAtIndex: 0]; id newleaf = parseValue([cmdargs objectAtIndex: 1], [cmdargs objectAtIndex: 2]); NSArray *parsedPath = parseKeyPath([keypath cString]); id leaf = plIndexKeypath(obj, keypath, 1); if (!newleaf) return EXIT_FAILURE; mutate(leaf, [parsedPath lastObject], newleaf, false); return dumpToFile(obj, fmt, outfile); } int plCmdReplace(id obj, NSPropertyListFormat fmt, NSArray *cmdargs, NSString *outfile) { NSString *keypath = [cmdargs objectAtIndex: 0]; id newleaf = parseValue([cmdargs objectAtIndex: 1], [cmdargs objectAtIndex: 2]); NSArray *parsedPath = parseKeyPath([keypath cString]); id leaf = plIndexKeypath(obj, keypath, 1); if (!newleaf) return EXIT_FAILURE; mutate(leaf, [parsedPath lastObject], newleaf, true); return dumpToFile(obj, fmt, outfile); } static void print_help(FILE *f) { GSPrintf(f, @"Property list utility\n"); GSPrintf(f, @"Usage: plutil [command] [options] file\n\n"); GSPrintf(f, @"Accepted commands:\n"); GSPrintf( f, @" -p\tPrints the plists in a human-readable form (GNUstep ASCII).\n"); GSPrintf( f, @" -lint\tVerifies the plist can be parsed.\n"); GSPrintf( f, @" -convert FMT\tConverts the plist to another format.\n"); GSPrintf( f, @" -insert PATH KEY VALUE\tInsert KEY=VALUE to the object at PATH.\n"); GSPrintf( f, @" -replace PATH KEY VALUE\tReplace KEY=VALUE for the object at PATH.\n"); GSPrintf( f, @" -remove PATH KEY\tRemove KEY from the object at PATH.\n"); GSPrintf( f, @" -extract PATH KEY\tExtract the KEY from the object at PATH.\n"); GSPrintf(f, @"Accepted options:\n"); GSPrintf( f, @" -s\t(No effect.)\n"); GSPrintf( f, @" -o OUTFILE\tOutput to the file given.\n"); GSPrintf( f, @" -e OUTEXT\tOutput to a file with the given extension.\n"); } typedef enum _Action { ACTION_LINT, ACTION_PRINT, ACTION_CONVERT, ACTION_REPLACE, ACTION_INSERT, ACTION_REMOVE, ACTION_EXTRACT, } Action; /**

Property list utility. Should act like macOS catalina plutil(1). *

*/ int main(int argc, char **argv, char **env) { int status = EXIT_SUCCESS; do { ENTER_POOL NSProcessInfo *proc; NSArray *args; NSString *arg; NSString *inpath; NSArray *cmdargs = nil; NSDictionary *commands = nil; NSArray *command_rhs; NSString *outpath = nil; NSString *outext = nil; Action action = ACTION_LINT; int count = 0; int i = 1; #ifdef GS_PASS_ARGUMENTS GSInitializeProcess(argc, argv, env); #endif proc = [NSProcessInfo processInfo]; if (proc == nil) { NSLog(@"plutil: unable to get process information."); status = EXIT_FAILURE; break; } args = [proc arguments]; if ((count = [args count]) <= 1) { NSLog(@"plutil: no files given."); status = EXIT_FAILURE; break; } // Parse the COMMAND. // Maps number of args to commands. // clang-format off commands = [NSDictionary dictionaryWithObjectsAndKeys: NARRAY(NINT(ACTION_PRINT), NINT(0)), @"-p", NARRAY(NINT(ACTION_LINT), NINT(0)), @"-lint", NARRAY(NINT(ACTION_CONVERT), NINT(1)), @"-convert", NARRAY(NINT(ACTION_INSERT), NINT(3)), @"-insert", NARRAY(NINT(ACTION_REPLACE), NINT(3)), @"-replace", NARRAY(NINT(ACTION_REMOVE), NINT(1)), @"-remove", NARRAY(NINT(ACTION_EXTRACT), NINT(2)), @"-extract", nil]; // clang-format on NS_DURING { NSData *fileData; NSPropertyListFormat aFormat; NSError *anError; id result; NSMutableString *outStr = nil; NSDictionary *locale; arg = [args objectAtIndex: i]; if (![arg hasPrefix: @"-"]) goto parse_file; command_rhs = [commands objectForKey: arg]; if (command_rhs) { int iwant; NSRange argrange; action = [[command_rhs objectAtIndex: 0] intValue]; iwant = [[command_rhs objectAtIndex: 1] intValue]; argrange.location = i + 1; argrange.length = iwant; cmdargs = [args subarrayWithRange: argrange]; i += 1 + iwant; } // Parse options for (; i < count; i++) { arg = [args objectAtIndex: i]; if (![arg hasPrefix: @"-"] || [arg isEqual: @"-"] || [arg isEqual: @"--"]) { goto parse_file; } else if ([arg caseInsensitiveCompare: @"-help"] == NSOrderedSame || [arg caseInsensitiveCompare: @"--help"] == NSOrderedSame || [arg caseInsensitiveCompare: @"-h"] == NSOrderedSame) { print_help(stdout); break; } else if ([arg isEqual: @"-s"]) { /* NOOP: What the heck is being quiet? */; } else if ([arg isEqual: @"-o"]) { outpath = [args objectAtIndex: ++i]; } else if ([arg isEqual: @"-e"]) { outext = [args objectAtIndex: ++i]; } else { GSPrintf(stderr, @"unrecognized option: %@\n", arg); return EXIT_FAILURE; } } parse_file: inpath = [args objectAtIndex: i]; if (!outpath && !outext && action != ACTION_EXTRACT) { outpath = inpath; } else if (outext) { NSRange dot; dot = [inpath rangeOfString: @"." options: NSBackwardsSearch]; if (dot.length == 0) { dot.location = [inpath length]; } outpath = [NSString stringWithFormat: @"%@.%@", [inpath substringToIndex: dot.location], outext]; } // Open, read, do things. if ([inpath isEqual: @"-"]) { NSFileHandle *fh = [NSFileHandle fileHandleWithStandardInput]; fileData = [fh readDataToEndOfFile]; } else fileData = [NSData dataWithContentsOfFile: inpath]; result = [NSPropertyListSerialization propertyListWithData: fileData options: NSPropertyListMutableContainersAndLeaves format: &aFormat error: &anError]; if (result == nil) { GSPrintf(stderr, @"Loading '%@' - %@\n", inpath, anError); status = EXIT_FAILURE; break; } switch (action) { case ACTION_LINT: break; case ACTION_PRINT: // Not using description because we can GS it locale = [[NSUserDefaults standardUserDefaults] dictionaryRepresentation]; GSPropertyListMake(result, locale, NO, NO, 2, &outStr); GSPrintf(stdout, @"%@\n", outStr); break; case ACTION_CONVERT: status = plCmdConvert(result, cmdargs, outpath); break; case ACTION_REPLACE: status = plCmdReplace(result, aFormat, cmdargs, outpath); break; case ACTION_INSERT: status = plCmdInsert(result, aFormat, cmdargs, outpath); break; case ACTION_REMOVE: status = plCmdRemove(result, aFormat, cmdargs, outpath); break; case ACTION_EXTRACT: if (!outpath) outpath = @"-"; status = plCmdExtract(result, cmdargs, outpath); break; } } NS_HANDLER { NSLog(@"Problem: %@", localException); if ([[localException name] isEqual: NSInvalidArgumentException]) print_help(stderr); status = EXIT_FAILURE; break; } NS_ENDHANDLER LEAVE_POOL } while (0); return status; }