Line data Source code
1 0 : /*
2 :
3 : OOLegacyScriptWhitelist.m
4 :
5 :
6 : Oolite
7 : Copyright (C) 2004-2013 Giles C Williams and contributors
8 :
9 : This program is free software; you can redistribute it and/or
10 : modify it under the terms of the GNU General Public License
11 : as published by the Free Software Foundation; either version 2
12 : of the License, or (at your option) any later version.
13 :
14 : This program is distributed in the hope that it will be useful,
15 : but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : GNU General Public License for more details.
18 :
19 : You should have received a copy of the GNU General Public License
20 : along with this program; if not, write to the Free Software
21 : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
22 : MA 02110-1301, USA.
23 :
24 : */
25 :
26 : #import "OOCocoa.h"
27 : #import "OOLegacyScriptWhitelist.h"
28 : #import "OOStringParsing.h"
29 : #import "ResourceManager.h"
30 : #import "OOCollectionExtractors.h"
31 : #import "PlayerEntityLegacyScriptEngine.h"
32 : #import "NSDictionaryOOExtensions.h"
33 : #import "OODeepCopy.h"
34 :
35 :
36 0 : #define INCLUDE_RAW_STRING OOLITE_DEBUG // If nonzero, raw condition strings are included; if zero, a placeholder is used.
37 :
38 :
39 0 : typedef struct SanStackElement SanStackElement;
40 0 : struct SanStackElement
41 : {
42 0 : SanStackElement *back;
43 0 : NSString *key; // Dictionary key; nil for arrays.
44 0 : NSUInteger index; // Array index if key is nil.
45 : };
46 :
47 :
48 : static NSArray *OOSanitizeLegacyScriptInternal(NSArray *script, SanStackElement *stack, BOOL allowAIMethods);
49 : static NSArray *OOSanitizeLegacyScriptConditionsInternal(NSArray *conditions, SanStackElement *stack);
50 :
51 : static NSArray *SanitizeCondition(NSString *condition, SanStackElement *stack);
52 : static NSArray *SanitizeConditionalStatement(NSDictionary *statement, SanStackElement *stack, BOOL allowAIMethods);
53 : static NSArray *SanitizeActionStatement(NSString *statement, SanStackElement *stack, BOOL allowAIMethods);
54 : static OOOperationType ClassifyLHSConditionSelector(NSString *selectorString, NSString **outSanitizedMethod, SanStackElement *stack);
55 : static NSString *SanitizeQueryMethod(NSString *selectorString); // Checks aliases and whitelist, returns nil if whitelist fails.
56 : static NSString *SanitizeActionMethod(NSString *selectorString, BOOL allowAIMethods); // Checks aliases and whitelist, returns nil if whitelist fails.
57 : static NSArray *AlwaysFalseConditions(void);
58 : static BOOL IsAlwaysFalseConditions(NSArray *conditions);
59 :
60 : static NSString *StringFromStack(SanStackElement *topOfStack);
61 :
62 :
63 0 : NSArray *OOSanitizeLegacyScript(NSArray *script, NSString *context, BOOL allowAIMethods)
64 : {
65 : SanStackElement stackRoot = { NULL, context, 0 };
66 : NSArray *result = OOSanitizeLegacyScriptInternal(script, &stackRoot, allowAIMethods);
67 : return [OODeepCopy(result) autorelease];
68 : }
69 :
70 :
71 0 : static NSArray *OOSanitizeLegacyScriptInternal(NSArray *script, SanStackElement *stack, BOOL allowAIMethods)
72 : {
73 : NSAutoreleasePool *pool = nil;
74 : NSMutableArray *result = nil;
75 : NSEnumerator *statementEnum = nil;
76 : id statement = nil;
77 : NSUInteger index = 0;
78 :
79 : pool = [[NSAutoreleasePool alloc] init];
80 :
81 : result = [NSMutableArray arrayWithCapacity:[script count]];
82 :
83 : for (statementEnum = [script objectEnumerator]; (statement = [statementEnum nextObject]); )
84 : {
85 : SanStackElement subStack =
86 : {
87 : stack, nil, index++
88 : };
89 :
90 : if ([statement isKindOfClass:[NSDictionary class]])
91 : {
92 : statement = SanitizeConditionalStatement(statement, &subStack, allowAIMethods);
93 : }
94 : else if ([statement isKindOfClass:[NSString class]])
95 : {
96 : statement = SanitizeActionStatement(statement, &subStack, allowAIMethods);
97 : }
98 : else
99 : {
100 : OOLog(@"script.syntax.statement.invalidType", @"***** SCRIPT ERROR: in %@, statement is of invalid type - expected string or dictionary, got %@.", StringFromStack(stack), [statement class]);
101 : statement = nil;
102 : }
103 :
104 : if (statement != nil)
105 : {
106 : [result addObject:statement];
107 : }
108 : }
109 :
110 : [result retain];
111 : [pool release];
112 :
113 : return [result autorelease];
114 : }
115 :
116 :
117 0 : NSArray *OOSanitizeLegacyScriptConditions(NSArray *conditions, NSString *context)
118 : {
119 : if (context == nil) context = @"<anonymous conditions>";
120 : SanStackElement stackRoot = { NULL, context, 0 };
121 : NSArray *result = OOSanitizeLegacyScriptConditionsInternal(conditions, &stackRoot);
122 : return [OODeepCopy(result) autorelease];
123 : }
124 :
125 :
126 0 : static NSArray *OOSanitizeLegacyScriptConditionsInternal(NSArray *conditions, SanStackElement *stack)
127 : {
128 : NSEnumerator *conditionEnum = nil;
129 : NSString *condition = nil;
130 : NSMutableArray *result = nil;
131 : NSArray *tokens = nil;
132 : BOOL OK = YES;
133 : NSUInteger index = 0;
134 :
135 : if (OOLegacyConditionsAreSanitized(conditions) || conditions == nil) return conditions;
136 :
137 : result = [NSMutableArray arrayWithCapacity:[conditions count]];
138 :
139 : for (conditionEnum = [conditions objectEnumerator]; (condition = [conditionEnum nextObject]); )
140 : {
141 : SanStackElement subStack =
142 : {
143 : stack, nil, index++
144 : };
145 :
146 : if (![condition isKindOfClass:[NSString class]])
147 : {
148 : OOLog(@"script.syntax.condition.notString", @"***** SCRIPT ERROR: in %@, bad condition - expected string, got %@; ignoring.", StringFromStack(stack), [condition class]);
149 : OK = NO;
150 : break;
151 : }
152 :
153 : tokens = SanitizeCondition(condition, &subStack);
154 : if (tokens != nil)
155 : {
156 : [result addObject:tokens];
157 : }
158 : else
159 : {
160 : OK = NO;
161 : break;
162 : }
163 : }
164 :
165 : if (OK) return result;
166 : else return AlwaysFalseConditions();
167 : }
168 :
169 :
170 0 : BOOL OOLegacyConditionsAreSanitized(NSArray *conditions)
171 : {
172 : if ([conditions count] == 0) return YES; // Empty array is safe.
173 : return [[conditions objectAtIndex:0] isKindOfClass:[NSArray class]];
174 : }
175 :
176 :
177 0 : static NSArray *SanitizeCondition(NSString *condition, SanStackElement *stack)
178 : {
179 : NSArray *tokens = nil;
180 : NSUInteger i, tokenCount;
181 : OOOperationType opType;
182 : NSString *selectorString = nil;
183 : NSString *sanitizedSelectorString = nil;
184 : NSString *comparatorString = nil;
185 : OOComparisonType comparatorValue;
186 : NSMutableArray *rhs = nil;
187 : NSString *rhsItem = nil;
188 : NSString *rhsSelector = nil;
189 : NSArray *sanitizedRHSItem = nil;
190 : NSString *stringSegment = nil;
191 :
192 : tokens = ScanTokensFromString(condition);
193 : tokenCount = [tokens count];
194 :
195 : if (tokenCount < 1)
196 : {
197 : OOLog(@"script.debug.syntax.scriptCondition.noneSpecified", @"***** SCRIPT ERROR: in %@, empty script condition.", StringFromStack(stack));
198 : return nil;
199 : }
200 :
201 : // Parse left-hand side.
202 : selectorString = [tokens oo_stringAtIndex:0];
203 : opType = ClassifyLHSConditionSelector(selectorString, &sanitizedSelectorString, stack);
204 : if (opType >= OP_INVALID)
205 : {
206 : OOLog(@"script.unpermittedMethod", @"***** SCRIPT ERROR: in %@ (\"%@\"), method \"%@\" not allowed.", StringFromStack(stack), condition, selectorString);
207 : return nil;
208 : }
209 :
210 : // Parse operator.
211 : if (tokenCount > 1)
212 : {
213 : comparatorString = [tokens oo_stringAtIndex:1];
214 : if ([comparatorString isEqualToString:@"equal"]) comparatorValue = COMPARISON_EQUAL;
215 : else if ([comparatorString isEqualToString:@"notequal"]) comparatorValue = COMPARISON_NOTEQUAL;
216 : else if ([comparatorString isEqualToString:@"lessthan"]) comparatorValue = COMPARISON_LESSTHAN;
217 : else if ([comparatorString isEqualToString:@"greaterthan"]) comparatorValue = COMPARISON_GREATERTHAN;
218 : else if ([comparatorString isEqualToString:@"morethan"]) comparatorValue = COMPARISON_GREATERTHAN;
219 : else if ([comparatorString isEqualToString:@"oneof"]) comparatorValue = COMPARISON_ONEOF;
220 : else if ([comparatorString isEqualToString:@"undefined"]) comparatorValue = COMPARISON_UNDEFINED;
221 : else
222 : {
223 : OOLog(@"script.debug.syntax.badComparison", @"***** SCRIPT ERROR: in %@ (\"%@\"), unknown comparison operator \"%@\", will return NO.", StringFromStack(stack), condition, comparatorString);
224 : return nil;
225 : }
226 : }
227 : else
228 : {
229 : /* In the direct interpreter, having no operator resulted in an
230 : implicit COMPARISON_NO operator, which always evaluated to false.
231 : Returning NO here causes AlwaysFalseConditions() to be used, which
232 : has the same effect.
233 : */
234 : OOLog(@"script.debug.syntax.noOperator", @"----- WARNING: SCRIPT in %@ -- No operator in expression \"%@\", will always evaluate as false.", StringFromStack(stack), condition);
235 : return nil;
236 : }
237 :
238 : // Check for invalid opType/comparator combinations.
239 : if (opType == OP_NUMBER && comparatorValue == COMPARISON_UNDEFINED)
240 : {
241 : OOLog(@"script.debug.syntax.invalidOperator", @"***** SCRIPT ERROR: in %@ (\"%@\"), comparison operator \"%@\" is not valid for %@.", StringFromStack(stack), condition, @"undefined", @"numbers");
242 : return nil;
243 : }
244 : else if (opType == OP_BOOL)
245 : {
246 : switch (comparatorValue)
247 : {
248 : // Valid comparators
249 : case COMPARISON_EQUAL:
250 : case COMPARISON_NOTEQUAL:
251 : break;
252 :
253 : default:
254 : OOLog(@"script.debug.syntax.invalidOperator", @"***** SCRIPT ERROR: in %@ (\"%@\"), comparison operator \"%@\" is not valid for %@.", StringFromStack(stack), condition, OOComparisonTypeToString(comparatorValue), @"booleans");
255 : return nil;
256 :
257 : }
258 : }
259 :
260 : /* Parse right-hand side. Each token is converted to an array of the
261 : token and a boolean indicating whether it's a selector.
262 :
263 : This also coalesces non-selector tokens, i.e. whitespace-separated
264 : string segments.
265 : */
266 : if (tokenCount > 2)
267 : {
268 : rhs = [NSMutableArray arrayWithCapacity:tokenCount - 2];
269 : for (i = 2; i < tokenCount; i++)
270 : {
271 : rhsItem = [tokens oo_stringAtIndex:i];
272 : rhsSelector = SanitizeQueryMethod(rhsItem);
273 : if (rhsSelector != nil)
274 : {
275 : // Method
276 : if (stringSegment != nil)
277 : {
278 : // Add stringSegment as a literal token.
279 : sanitizedRHSItem = [NSArray arrayWithObjects:[NSNumber numberWithBool:NO], stringSegment, nil];
280 : [rhs addObject:sanitizedRHSItem];
281 : stringSegment = nil;
282 : }
283 :
284 : sanitizedRHSItem = [NSArray arrayWithObjects:[NSNumber numberWithBool:YES], rhsSelector, nil];
285 : [rhs addObject:sanitizedRHSItem];
286 : }
287 : else
288 : {
289 : // String; append to stringSegment
290 : if (stringSegment == nil) stringSegment = rhsItem;
291 : else stringSegment = [NSString stringWithFormat:@"%@ %@", stringSegment, rhsItem];
292 : }
293 : }
294 :
295 : if (stringSegment != nil)
296 : {
297 : sanitizedRHSItem = [NSArray arrayWithObjects:[NSNumber numberWithBool:NO], stringSegment, nil];
298 : [rhs addObject:sanitizedRHSItem];
299 : }
300 : }
301 : else
302 : {
303 : rhs = [NSMutableArray array];
304 : }
305 :
306 : NSString *rawString = nil;
307 : #if INCLUDE_RAW_STRING
308 : rawString = condition;
309 : #else
310 : rawString = @"<condition>";
311 : #endif
312 :
313 : return [NSArray arrayWithObjects:
314 : [NSNumber numberWithUnsignedInt:opType],
315 : rawString,
316 : sanitizedSelectorString,
317 : [NSNumber numberWithUnsignedInt:comparatorValue],
318 : rhs,
319 : nil];
320 : }
321 :
322 :
323 0 : static NSArray *SanitizeConditionalStatement(NSDictionary *statement, SanStackElement *stack, BOOL allowAIMethods)
324 : {
325 : NSArray *conditions = nil;
326 : NSArray *doActions = nil;
327 : NSArray *elseActions = nil;
328 :
329 : conditions = [statement oo_arrayForKey:@"conditions"];
330 : if (conditions == nil)
331 : {
332 : OOLog(@"script.syntax.noConditions", @"***** SCRIPT ERROR: in %@, conditions array contains no \"conditions\" entry, ignoring.", StringFromStack(stack));
333 : return nil;
334 : }
335 :
336 : // Sanitize conditions.
337 : SanStackElement subStack = { stack, @"conditions", 0 };
338 : conditions = OOSanitizeLegacyScriptConditionsInternal(conditions, &subStack);
339 : if (conditions == nil)
340 : {
341 : return nil;
342 : }
343 :
344 : // Sanitize do and else.
345 : if (!IsAlwaysFalseConditions(conditions)) doActions = [statement oo_arrayForKey:@"do"];
346 : if (doActions != nil)
347 : {
348 : subStack.key = @"do";
349 : doActions = OOSanitizeLegacyScriptInternal(doActions, &subStack, allowAIMethods);
350 : }
351 :
352 : elseActions = [statement oo_arrayForKey:@"else"];
353 : if (elseActions != nil)
354 : {
355 : subStack.key = @"else";
356 : elseActions = OOSanitizeLegacyScriptInternal(elseActions, &subStack, allowAIMethods);
357 : }
358 :
359 : // If neither does anything, the statment has no effect.
360 : if ([doActions count] == 0 && [elseActions count] == 0)
361 : {
362 : return nil;
363 : }
364 :
365 : if (doActions == nil) doActions = [NSArray array];
366 : if (elseActions == nil) elseActions = [NSArray array];
367 :
368 : return [NSArray arrayWithObjects:[NSNumber numberWithBool:YES], conditions, doActions, elseActions, nil];
369 : }
370 :
371 :
372 0 : static NSArray *SanitizeActionStatement(NSString *statement, SanStackElement *stack, BOOL allowAIMethods)
373 : {
374 : NSMutableArray *tokens = nil;
375 : NSUInteger tokenCount;
376 : NSString *rawSelectorString = nil;
377 : NSString *selectorString = nil;
378 : NSString *argument = nil;
379 :
380 : tokens = ScanTokensFromString(statement);
381 : tokenCount = [tokens count];
382 : if (tokenCount == 0) return nil;
383 :
384 : rawSelectorString = [tokens objectAtIndex:0];
385 : selectorString = SanitizeActionMethod(rawSelectorString, allowAIMethods);
386 : if (selectorString == nil)
387 : {
388 : OOLog(@"script.unpermittedMethod", @"***** SCRIPT ERROR: in %@ (\"%@\"), method \"%@\" not allowed.", StringFromStack(stack), statement, rawSelectorString);
389 : return nil;
390 : }
391 :
392 : if ([selectorString isEqualToString:@"doNothing"])
393 : {
394 : return nil;
395 : }
396 :
397 : if ([selectorString hasSuffix:@":"])
398 : {
399 : // Expects an argument
400 : if (tokenCount == 2)
401 : {
402 : argument = [tokens objectAtIndex:1];
403 : }
404 : else
405 : {
406 : [tokens removeObjectAtIndex:0];
407 : argument = [tokens componentsJoinedByString:@" "];
408 : }
409 :
410 : argument = [argument stringByReplacingOccurrencesOfString:@"[credits_number]" withString:@"[_oo_legacy_credits_number]"];
411 : }
412 :
413 : return [NSArray arrayWithObjects:[NSNumber numberWithBool:NO], selectorString, argument, nil];
414 : }
415 :
416 :
417 0 : static OOOperationType ClassifyLHSConditionSelector(NSString *selectorString, NSString **outSanitizedSelector, SanStackElement *stack)
418 : {
419 : assert(outSanitizedSelector != NULL);
420 :
421 : *outSanitizedSelector = selectorString;
422 :
423 : // Allow arbitrary mission_foo or local_foo pseudo-selectors.
424 : if ([selectorString hasPrefix:@"mission_"]) return OP_MISSION_VAR;
425 : if ([selectorString hasPrefix:@"local_"]) return OP_LOCAL_VAR;
426 :
427 : // If it's a real method, check against whitelist.
428 : *outSanitizedSelector = SanitizeQueryMethod(selectorString);
429 : if (*outSanitizedSelector == nil)
430 : {
431 : return OP_INVALID;
432 : }
433 :
434 : // If it's a real method, and in the whitelist, classify by suffix.
435 : if ([selectorString hasSuffix:@"_string"]) return OP_STRING;
436 : if ([selectorString hasSuffix:@"_number"]) return OP_NUMBER;
437 : if ([selectorString hasSuffix:@"_bool"]) return OP_BOOL;
438 :
439 : // If we got here, something's wrong.
440 : OOLog(@"script.sanitize.unclassifiedSelector", @"***** ERROR: Whitelisted query method \"%@\" has no type suffix, treating as invalid.", selectorString);
441 : return OP_INVALID;
442 : }
443 :
444 :
445 0 : static NSString *SanitizeQueryMethod(NSString *selectorString)
446 : {
447 : static NSSet *whitelist = nil;
448 : static NSDictionary *aliases = nil;
449 : NSString *aliasedSelector = nil;
450 :
451 : if (whitelist == nil)
452 : {
453 : whitelist = [[NSSet alloc] initWithArray:[[ResourceManager whitelistDictionary] oo_arrayForKey:@"query_methods"]];
454 : aliases = [[[ResourceManager whitelistDictionary] oo_dictionaryForKey:@"query_method_aliases"] retain];
455 : }
456 :
457 : aliasedSelector = [aliases oo_stringForKey:selectorString];
458 : if (aliasedSelector != nil) selectorString = aliasedSelector;
459 :
460 : if (![whitelist containsObject:selectorString]) selectorString = nil;
461 :
462 : return selectorString;
463 : }
464 :
465 :
466 0 : static NSString *SanitizeActionMethod(NSString *selectorString, BOOL allowAIMethods)
467 : {
468 : static NSSet *whitelist = nil;
469 : static NSSet *whitelistWithAI = nil;
470 : static NSDictionary *aliases = nil;
471 : static NSDictionary *aliasesWithAI = nil;
472 : NSString *aliasedSelector = nil;
473 :
474 : if (whitelist == nil)
475 : {
476 : NSArray *actionMethods = nil;
477 : NSArray *aiMethods = nil;
478 : NSArray *aiAndActionMethods = nil;
479 :
480 : actionMethods = [[ResourceManager whitelistDictionary] oo_arrayForKey:@"action_methods"];
481 : aiMethods = [[ResourceManager whitelistDictionary] oo_arrayForKey:@"ai_methods"];
482 : aiAndActionMethods = [[ResourceManager whitelistDictionary] oo_arrayForKey:@"ai_and_action_methods"];
483 :
484 : if (actionMethods == nil) actionMethods = [NSArray array];
485 : if (aiMethods == nil) aiMethods = [NSArray array];
486 :
487 : if (aiAndActionMethods != nil) actionMethods = [actionMethods arrayByAddingObjectsFromArray:aiAndActionMethods];
488 :
489 : whitelist = [[NSSet alloc] initWithArray:actionMethods];
490 : whitelistWithAI = [[NSSet alloc] initWithArray:[aiMethods arrayByAddingObjectsFromArray:actionMethods]];
491 :
492 : aliases = [[[ResourceManager whitelistDictionary] oo_dictionaryForKey:@"action_method_aliases"] retain];
493 :
494 : aliasesWithAI = [[ResourceManager whitelistDictionary] oo_dictionaryForKey:@"ai_method_aliases"];
495 : if (aliasesWithAI != nil)
496 : {
497 : aliasesWithAI = [[aliasesWithAI dictionaryByAddingEntriesFromDictionary:aliases] copy];
498 : }
499 : else
500 : {
501 : aliasesWithAI = [aliases copy];
502 : }
503 : }
504 :
505 : aliasedSelector = [(allowAIMethods ? aliasesWithAI : aliases) oo_stringForKey:selectorString];
506 : if (aliasedSelector != nil) selectorString = aliasedSelector;
507 :
508 : if (![(allowAIMethods ? whitelistWithAI : whitelist) containsObject:selectorString]) selectorString = nil;
509 :
510 : return selectorString;
511 : }
512 :
513 :
514 : // Return a conditions array that always evaluates as false.
515 0 : static NSArray *AlwaysFalseConditions(void)
516 : {
517 : static NSArray *alwaysFalse = nil;
518 : if (alwaysFalse != nil)
519 : {
520 : alwaysFalse = [NSArray arrayWithObject:[NSArray arrayWithObject:[NSNumber numberWithUnsignedInt:OP_FALSE]]];
521 : [alwaysFalse retain];
522 : }
523 :
524 : return alwaysFalse;
525 : }
526 :
527 :
528 0 : static BOOL IsAlwaysFalseConditions(NSArray *conditions)
529 : {
530 : return [[conditions oo_arrayAtIndex:0] oo_unsignedIntAtIndex:0] == OP_FALSE;
531 : }
532 :
533 :
534 0 : static NSMutableString *StringFromStackInternal(SanStackElement *topOfStack)
535 : {
536 : if (topOfStack == NULL) return nil;
537 :
538 : NSMutableString *base = StringFromStackInternal(topOfStack->back);
539 : if (base == nil) base = [NSMutableString string];
540 :
541 : NSString *string = topOfStack->key;
542 : if (string == nil) string = [NSString stringWithFormat:@"%lu", (unsigned long)topOfStack->index];
543 : if ([base length] > 0) [base appendString:@"."];
544 :
545 : [base appendString:string];
546 :
547 : return base;
548 : }
549 :
550 :
551 0 : static NSString *StringFromStack(SanStackElement *topOfStack)
552 : {
553 : return StringFromStackInternal(topOfStack);
554 : }
|