Create plutil utility

This is supposedly an expansion of the macOS plutil(1). See #101.
This commit is contained in:
Mingye Wang 2020-02-18 22:27:20 +08:00
parent af38713702
commit b8d83158e8
2 changed files with 606 additions and 1 deletions

View file

@ -72,7 +72,7 @@ MAN8_PAGES = gdomap.8
ifeq ($(add),yes)
TOOL_NAME = autogsdoc cvtenc plmerge sfparse xmlparse
else
TOOL_NAME = autogsdoc cvtenc gdnc gspath defaults pl plmerge \
TOOL_NAME = autogsdoc cvtenc gdnc gspath defaults pl plmerge plutil \
plparse sfparse pldes plget plser pl2link xmlparse HTMLLinker
CTOOL_NAME = gdomap
@ -97,6 +97,7 @@ plget_OBJC_FILES = plget.m
plser_OBJC_FILES = plser.m
plmerge_OBJC_FILES = plmerge.m
plparse_OBJC_FILES = plparse.m
plutil_OBJC_FILES = plutil.m
sfparse_OBJC_FILES = sfparse.m
pl2link_OBJC_FILES = pl2link.m
locale_alias_OBJC_FILES = locale_alias.m

604
Tools/plutil.m Normal file
View file

@ -0,0 +1,604 @@
/** 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,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// #import "common.h"
#include <string.h>
#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"
// 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:
* <pre>
* keypath = '' | keypath '.' key;
* key = quotedString | unquotedStringNotAllowingPeriod;
* </pre>
* 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 alloc] init];
NSString * key = nil;
size_t i;
size_t j;
for (i = 0; keypath[i]; i++)
switch (keypath[i])
{
case KEYPATH_SEP:
[res addObject:key];
key = nil;
break;
case '"':
key = parseQuotedString(keypath, &i);
break;
default:
for (j = i; 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 [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)
{
if ([type isEqual:@"-plist"])
return [value propertyList];
else if ([type isEqual:@"-xml"] || [type isEqual:@"-date"])
{
NSData *mydata =
[value dataUsingEncoding:[NSString defaultCStringEncoding]];
NSPropertyListFormat aFormat;
NSError * anError;
id result = [NSPropertyListSerialization
propertyListWithData:mydata
options:NSPropertyListMutableContainersAndLeaves
format:&aFormat
error:&anError];
if (result == nil)
{
GSPrintf(stderr, @"Parsing plist %@: %@\n", value, anError);
return nil;
}
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);
return result;
}
else if ([type isEqual:@"-bool"])
return [NSNumber
numberWithBool:([value isEqual:@"YES"] || [value isEqual:@"true"])];
else if ([type isEqual:@"-integer"])
return [[NSNumber alloc] initWithLongLong:[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)
return [[NSNumber alloc] initWithDouble:strtod([value cString], 0)];
else if ([type isEqual:@"-data"])
return [[NSData alloc]
initWithBase64EncodedData:[value
dataUsingEncoding:[NSString
defaultCStringEncoding]]
options:NSDataBase64DecodingIgnoreUnknownCharacters];
else
GSPrintf(stderr, @"Unrecognized type %@\n", type);
return nil;
}
#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",
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)
{
NSError * anError;
NSFileHandle *fh;
NSData * outdata = [NSPropertyListSerialization dataWithPropertyList:obj
format:fmt
options:0
error:&anError];
if (anError)
{
GSPrintf(stderr, @"Dumping %@ as format %@ - %@\n", obj, fmt, anError);
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.");
GSPrintf(f, "Accepted options:\n");
}
typedef enum _Action
{
ACTION_LINT,
ACTION_PRINT,
ACTION_CONVERT,
ACTION_REPLACE,
ACTION_INSERT,
ACTION_REMOVE,
ACTION_EXTRACT,
} Action;
/** <p> Property list utility. Should act like macOS catalina plutil(1).
* </p>
*/
int
main(int argc, char **argv, char **env)
{
int status = EXIT_SUCCESS;
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.
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(2)),
@"-insert",
NARRAY(NINT(ACTION_REPLACE), NINT(2)),
@"-replace",
NARRAY(NINT(ACTION_REMOVE), NINT(1)),
@"-remove",
NARRAY(NINT(ACTION_EXTRACT), NINT(2)),
@"-extract", nil];
NS_DURING
{
NSData * fileData;
NSPropertyListFormat aFormat;
NSError * anError;
id result;
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;
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 isEqual:@"-help"])
{
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
[NSException raise:NSInvalidArgumentException
format:@"Invalid option %@", arg];
}
parse_file:
inpath = [args objectAtIndex:i];
if (!outpath && !outext && action != ACTION_EXTRACT)
outpath = inpath;
else if (outext)
{
NSRange dot = [inpath rangeOfString:@"." options:NSBackwardsSearch];
if (dot.length == 0)
dot.location = [inpath length];
outpath = [NSString
stringWithFormat:@"%s.%s", [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:
// Why use an ad-hoc format when description gives pretty plists?
GSPrintf(stdout, @"%@\n",
[result
descriptionWithLocale:[[NSUserDefaults standardUserDefaults]
dictionaryRepresentation]
indent:0]);
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
return status;
}