• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2015 The Bazel Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //    http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 package com.google.devtools.common.options;
15 
16 import com.google.common.base.Joiner;
17 import com.google.common.base.Verify;
18 import com.google.common.collect.ArrayListMultimap;
19 import com.google.common.collect.ImmutableList;
20 import com.google.common.collect.ImmutableSet;
21 import com.google.common.collect.Multimap;
22 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.AllowValues;
23 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.DisallowValues;
24 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.FlagPolicy;
25 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.FlagPolicy.OperationCase;
26 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
27 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.SetValue;
28 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.UseDefault;
29 import com.google.devtools.common.options.OptionPriority.PriorityCategory;
30 import com.google.devtools.common.options.OptionsParser.OptionDescription;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Set;
38 import java.util.logging.Level;
39 import java.util.logging.Logger;
40 import java.util.stream.Collectors;
41 import javax.annotation.Nullable;
42 
43 /**
44  * Enforces the {@link FlagPolicy}s (from an {@link InvocationPolicy} proto) on an {@link
45  * OptionsParser} by validating and changing the flag values in the given {@link OptionsParser}.
46  *
47  * <p>"Flag" and "Option" are used interchangeably in this file.
48  */
49 public final class InvocationPolicyEnforcer {
50 
51   private static final Logger logger = Logger.getLogger(InvocationPolicyEnforcer.class.getName());
52 
53   private static final String INVOCATION_POLICY_SOURCE = "Invocation policy";
54   @Nullable private final InvocationPolicy invocationPolicy;
55   private final Level loglevel;
56 
57   /**
58    * Creates an InvocationPolicyEnforcer that enforces the given policy.
59    *
60    * @param invocationPolicy the policy to enforce. A null policy means this enforcer will do
61    *     nothing in calls to enforce().
62    */
InvocationPolicyEnforcer(@ullable InvocationPolicy invocationPolicy)63   public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy) {
64     this(invocationPolicy, Level.FINE);
65   }
66 
67   /**
68    * Creates an InvocationPolicyEnforcer that enforces the given policy.
69    *
70    * @param invocationPolicy the policy to enforce. A null policy means this enforcer will do
71    *     nothing in calls to enforce().
72    * @param loglevel the level at which to log informational statements. Warnings and errors will
73    *     still be logged at the appropriate level.
74    */
InvocationPolicyEnforcer(@ullable InvocationPolicy invocationPolicy, Level loglevel)75   public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy, Level loglevel) {
76     this.invocationPolicy = invocationPolicy;
77     this.loglevel = loglevel;
78   }
79 
80   private static final class FlagPolicyWithContext {
81     private final FlagPolicy policy;
82     private final OptionDescription description;
83     private final OptionInstanceOrigin origin;
84 
FlagPolicyWithContext( FlagPolicy policy, OptionDescription description, OptionInstanceOrigin origin)85     public FlagPolicyWithContext(
86         FlagPolicy policy, OptionDescription description, OptionInstanceOrigin origin) {
87       this.policy = policy;
88       this.description = description;
89       this.origin = origin;
90     }
91   }
92 
getInvocationPolicy()93   public InvocationPolicy getInvocationPolicy() {
94     return invocationPolicy;
95   }
96 
97   /**
98    * Applies this OptionsPolicyEnforcer's policy to the given OptionsParser for all blaze commands.
99    *
100    * @param parser The OptionsParser to enforce policy on.
101    * @throws OptionsParsingException if any flag policy is invalid.
102    */
enforce(OptionsParser parser)103   public void enforce(OptionsParser parser) throws OptionsParsingException {
104     enforce(parser, null);
105   }
106 
107   /**
108    * Applies this OptionsPolicyEnforcer's policy to the given OptionsParser.
109    *
110    * @param parser The OptionsParser to enforce policy on.
111    * @param command The current blaze command, for flag policies that apply to only specific
112    *     commands. Such policies will be enforced only if they contain this command or a command
113    *     they inherit from
114    * @throws OptionsParsingException if any flag policy is invalid.
115    */
enforce(OptionsParser parser, @Nullable String command)116   public void enforce(OptionsParser parser, @Nullable String command)
117       throws OptionsParsingException {
118     if (invocationPolicy == null || invocationPolicy.getFlagPoliciesCount() == 0) {
119       return;
120     }
121 
122     // The effective policy returned is expanded, filtered for applicable commands, and cleaned of
123     // redundancies and conflicts.
124     List<FlagPolicyWithContext> effectivePolicies =
125         getEffectivePolicies(invocationPolicy, parser, command, loglevel);
126 
127     for (FlagPolicyWithContext flagPolicy : effectivePolicies) {
128       String flagName = flagPolicy.policy.getFlagName();
129 
130       OptionValueDescription valueDescription;
131       try {
132         valueDescription = parser.getOptionValueDescription(flagName);
133       } catch (IllegalArgumentException e) {
134         // This flag doesn't exist. We are deliberately lenient if the flag policy has a flag
135         // we don't know about. This is for better future proofing so that as new flags are added,
136         // new policies can use the new flags without worrying about older versions of Bazel.
137         logger.log(
138             loglevel,
139             String.format("Flag '%s' specified by invocation policy does not exist", flagName));
140         continue;
141       }
142 
143       // getOptionDescription() will return null if the option does not exist, however
144       // getOptionValueDescription() above would have thrown an IllegalArgumentException if that
145       // were the case.
146       Verify.verifyNotNull(flagPolicy.description);
147 
148       switch (flagPolicy.policy.getOperationCase()) {
149         case SET_VALUE:
150           applySetValueOperation(parser, flagPolicy, valueDescription, loglevel);
151           break;
152 
153         case USE_DEFAULT:
154           applyUseDefaultOperation(
155               parser, "UseDefault", flagPolicy.description.getOptionDefinition(), loglevel);
156           break;
157 
158         case ALLOW_VALUES:
159           AllowValues allowValues = flagPolicy.policy.getAllowValues();
160           FilterValueOperation.AllowValueOperation allowValueOperation =
161               new FilterValueOperation.AllowValueOperation(loglevel);
162           allowValueOperation.apply(
163               parser,
164               flagPolicy.origin,
165               allowValues.getAllowedValuesList(),
166               allowValues.hasNewValue() ? allowValues.getNewValue() : null,
167               allowValues.hasUseDefault(),
168               valueDescription,
169               flagPolicy.description);
170           break;
171 
172         case DISALLOW_VALUES:
173           DisallowValues disallowValues = flagPolicy.policy.getDisallowValues();
174           FilterValueOperation.DisallowValueOperation disallowValueOperation =
175               new FilterValueOperation.DisallowValueOperation(loglevel);
176           disallowValueOperation.apply(
177               parser,
178               flagPolicy.origin,
179               disallowValues.getDisallowedValuesList(),
180               disallowValues.hasNewValue() ? disallowValues.getNewValue() : null,
181               disallowValues.hasUseDefault(),
182               valueDescription,
183               flagPolicy.description);
184           break;
185 
186         case OPERATION_NOT_SET:
187           throw new PolicyOperationNotSetException(flagName);
188 
189         default:
190           logger.warning(
191               String.format(
192                   "Unknown operation '%s' from invocation policy for flag '%s'",
193                   flagPolicy.policy.getOperationCase(), flagName));
194           break;
195       }
196     }
197   }
198 
199   private static class PolicyOperationNotSetException extends OptionsParsingException {
PolicyOperationNotSetException(String flagName)200     PolicyOperationNotSetException(String flagName) {
201       super(String.format("Flag policy for flag '%s' does not " + "have an operation", flagName));
202     }
203   }
204 
policyApplies(FlagPolicy policy, ImmutableSet<String> applicableCommands)205   private static boolean policyApplies(FlagPolicy policy, ImmutableSet<String> applicableCommands) {
206     // Skip the flag policy if it doesn't apply to this command. If the commands list is empty,
207     // then the policy applies to all commands.
208     if (policy.getCommandsList().isEmpty() || applicableCommands.isEmpty()) {
209       return true;
210     }
211 
212     return !Collections.disjoint(policy.getCommandsList(), applicableCommands);
213   }
214 
215   /** Returns the expanded and filtered policy that would be enforced for the given command. */
getEffectiveInvocationPolicy( InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel)216   public static InvocationPolicy getEffectiveInvocationPolicy(
217       InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel)
218       throws OptionsParsingException {
219     ImmutableList<FlagPolicyWithContext> effectivePolicies =
220         getEffectivePolicies(invocationPolicy, parser, command, loglevel);
221 
222     InvocationPolicy.Builder builder = InvocationPolicy.newBuilder();
223     for (FlagPolicyWithContext policyWithContext : effectivePolicies) {
224       builder.addFlagPolicies(policyWithContext.policy);
225     }
226     return builder.build();
227   }
228 
229   /**
230    * Takes the provided policy and processes it to the form that can be used on the user options.
231    *
232    * <p>Expands any policies on expansion flags.
233    */
getEffectivePolicies( InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel)234   private static ImmutableList<FlagPolicyWithContext> getEffectivePolicies(
235       InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel)
236       throws OptionsParsingException {
237     if (invocationPolicy == null) {
238       return ImmutableList.of();
239     }
240 
241     ImmutableSet<String> commandAndParentCommands =
242         command == null
243             ? ImmutableSet.of()
244             : CommandNameCache.CommandNameCacheInstance.INSTANCE.get(command);
245 
246     // Expand all policies to transfer policies on expansion flags to policies on the child flags.
247     List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>();
248     OptionPriority nextPriority =
249         OptionPriority.lowestOptionPriorityAtCategory(PriorityCategory.INVOCATION_POLICY);
250     for (FlagPolicy policy : invocationPolicy.getFlagPoliciesList()) {
251       // Explicitly disallow --config in invocation policy.
252       if (policy.getFlagName().equals("config")) {
253         throw new OptionsParsingException(
254             "Invocation policy is applied after --config expansion, changing config values now "
255                 + "would have no effect and is disallowed to prevent confusion. Please remove the "
256                 + "following policy : "
257                 + policy);
258       }
259 
260       // These policies are high-level, before expansion, and so are not the implicitDependents or
261       // expansions of any other flag, other than in an obtuse sense from --invocation_policy.
262       OptionPriority currentPriority = nextPriority;
263       OptionInstanceOrigin origin =
264           new OptionInstanceOrigin(currentPriority, INVOCATION_POLICY_SOURCE, null, null);
265       nextPriority = OptionPriority.nextOptionPriority(currentPriority);
266       if (!policyApplies(policy, commandAndParentCommands)) {
267         // Only keep and expand policies that are applicable to the current command.
268         continue;
269       }
270 
271       OptionDescription optionDescription = parser.getOptionDescription(policy.getFlagName());
272       if (optionDescription == null) {
273         // InvocationPolicy ignores policy on non-existing flags by design, for version
274         // compatibility.
275         logger.log(
276             loglevel,
277             String.format(
278                 "Flag '%s' specified by invocation policy does not exist, and will be ignored",
279                 policy.getFlagName()));
280         continue;
281       }
282       FlagPolicyWithContext policyWithContext =
283           new FlagPolicyWithContext(policy, optionDescription, origin);
284       List<FlagPolicyWithContext> policies = expandPolicy(policyWithContext, parser, loglevel);
285       expandedPolicies.addAll(policies);
286     }
287 
288     // Only keep that last policy for each flag.
289     Map<String, FlagPolicyWithContext> effectivePolicy = new HashMap<>();
290     for (FlagPolicyWithContext expandedPolicy : expandedPolicies) {
291       String flagName = expandedPolicy.policy.getFlagName();
292       effectivePolicy.put(flagName, expandedPolicy);
293     }
294 
295     return ImmutableList.copyOf(effectivePolicy.values());
296   }
297 
throwAllowValuesOnExpansionFlagException(String flagName)298   private static void throwAllowValuesOnExpansionFlagException(String flagName)
299       throws OptionsParsingException {
300     throw new OptionsParsingException(
301         String.format("Allow_Values on expansion flags like %s is not allowed.", flagName));
302   }
303 
throwDisallowValuesOnExpansionFlagException(String flagName)304   private static void throwDisallowValuesOnExpansionFlagException(String flagName)
305       throws OptionsParsingException {
306     throw new OptionsParsingException(
307         String.format("Disallow_Values on expansion flags like %s is not allowed.", flagName));
308   }
309 
310   /**
311    * Expand a single policy. If the policy is not about an expansion flag, this will simply return a
312    * list with a single element, oneself. If the policy is for an expansion flag, the policy will
313    * get split into multiple policies applying to each flag the original flag expands to.
314    *
315    * <p>None of the flagPolicies returned should be on expansion flags.
316    */
expandPolicy( FlagPolicyWithContext originalPolicy, OptionsParser parser, Level loglevel)317   private static List<FlagPolicyWithContext> expandPolicy(
318       FlagPolicyWithContext originalPolicy, OptionsParser parser, Level loglevel)
319       throws OptionsParsingException {
320     List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>();
321 
322     boolean isExpansion = originalPolicy.description.isExpansion();
323     ImmutableList<ParsedOptionDescription> subflags =
324         parser.getExpansionValueDescriptions(
325             originalPolicy.description.getOptionDefinition(), originalPolicy.origin);
326 
327     // If we have nothing to expand to, no need to do any further work.
328     if (subflags.isEmpty()) {
329       return ImmutableList.of(originalPolicy);
330     }
331 
332     if (logger.isLoggable(loglevel)) {
333       // Log the expansion. This is only really useful for understanding the invocation policy
334       // itself.
335       List<String> subflagNames = new ArrayList<>(subflags.size());
336       for (ParsedOptionDescription subflag : subflags) {
337         subflagNames.add("--" + subflag.getOptionDefinition().getOptionName());
338       }
339 
340       logger.logp(
341           loglevel,
342           "InvocationPolicyEnforcer",
343           "expandPolicy",
344           String.format(
345               "Expanding %s on option %s to its %s: %s.",
346               originalPolicy.policy.getOperationCase(),
347               originalPolicy.policy.getFlagName(),
348               isExpansion ? "expansions" : "implied flags",
349               Joiner.on("; ").join(subflagNames)));
350     }
351 
352     // Repeated flags are special, and could set multiple times in an expansion, with the user
353     // expecting both values to be valid. Collect these separately.
354     Multimap<OptionDescription, ParsedOptionDescription> repeatableSubflagsInSetValues =
355         ArrayListMultimap.create();
356 
357     // Create a flag policy for the child that looks like the parent's policy "transferred" to its
358     // child. Note that this only makes sense for SetValue, when setting an expansion flag, or
359     // UseDefault, when preventing it from being set.
360     for (ParsedOptionDescription currentSubflag : subflags) {
361       OptionDescription subflagOptionDescription =
362           parser.getOptionDescription(currentSubflag.getOptionDefinition().getOptionName());
363 
364       if (currentSubflag.getOptionDefinition().allowsMultiple()
365           && originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE)) {
366         repeatableSubflagsInSetValues.put(subflagOptionDescription, currentSubflag);
367       } else {
368         FlagPolicyWithContext subflagAsPolicy =
369             getSingleValueSubflagAsPolicy(
370                 subflagOptionDescription, currentSubflag, originalPolicy, isExpansion);
371         // In case any of the expanded flags are themselves expansions, recurse.
372         expandedPolicies.addAll(expandPolicy(subflagAsPolicy, parser, loglevel));
373       }
374     }
375 
376     // If there are any repeatable flag SetValues, deal with them together now.
377     // Note that expansion flags have no value, and so cannot have multiple values either.
378     // Skipping the recursion above is fine.
379     for (OptionDescription repeatableFlag : repeatableSubflagsInSetValues.keySet()) {
380       int numValues = repeatableSubflagsInSetValues.get(repeatableFlag).size();
381       ArrayList<String> newValues = new ArrayList<>(numValues);
382       ArrayList<OptionInstanceOrigin> origins = new ArrayList<>(numValues);
383       for (ParsedOptionDescription setValue : repeatableSubflagsInSetValues.get(repeatableFlag)) {
384         newValues.add(setValue.getUnconvertedValue());
385         origins.add(setValue.getOrigin());
386       }
387       // These options come from expanding a single policy, so they have effectively the same
388       // priority. They could have come from different expansions or implicit requirements in the
389       // recursive resolving of the option list, so just pick the first one. Do collapse the source
390       // strings though, in case there are different sources.
391       OptionInstanceOrigin arbitraryFirstOptionOrigin = origins.get(0);
392       OptionInstanceOrigin originOfSubflags =
393           new OptionInstanceOrigin(
394               arbitraryFirstOptionOrigin.getPriority(),
395               origins
396                   .stream()
397                   .map(OptionInstanceOrigin::getSource)
398                   .distinct()
399                   .collect(Collectors.joining(", ")),
400               arbitraryFirstOptionOrigin.getImplicitDependent(),
401               arbitraryFirstOptionOrigin.getExpandedFrom());
402       expandedPolicies.add(
403           getSetValueSubflagAsPolicy(repeatableFlag, newValues, originOfSubflags, originalPolicy));
404     }
405 
406     // Don't add the original policy if it was an expansion flag, which have no value, but do add
407     // it if there was either no expansion or if it was a valued flag with implicit requirements.
408     if (!isExpansion) {
409       expandedPolicies.add(originalPolicy);
410     }
411 
412     return expandedPolicies;
413   }
414 
415   /**
416    * Expand a SetValue flag policy on a repeatable flag. SetValue operations are the only flag
417    * policies that set the flag, and so interact with repeatable flags, flags that can be set
418    * multiple times, in subtle ways.
419    *
420    * @param subflagDesc, the description of the flag the SetValue'd expansion flag expands to.
421    * @param subflagValue, the values that the SetValue'd expansion flag expands to for this flag.
422    * @param originalPolicy, the original policy on the expansion flag.
423    * @return the flag policy for the subflag given, this will be part of the expanded form of the
424    *     SetValue policy on the original flag.
425    */
getSetValueSubflagAsPolicy( OptionDescription subflagDesc, List<String> subflagValue, OptionInstanceOrigin subflagOrigin, FlagPolicyWithContext originalPolicy)426   private static FlagPolicyWithContext getSetValueSubflagAsPolicy(
427       OptionDescription subflagDesc,
428       List<String> subflagValue,
429       OptionInstanceOrigin subflagOrigin,
430       FlagPolicyWithContext originalPolicy) {
431     // Some sanity checks.
432     OptionDefinition subflag = subflagDesc.getOptionDefinition();
433     Verify.verify(originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE));
434     if (!subflag.allowsMultiple()) {
435       Verify.verify(subflagValue.size() <= 1);
436     }
437 
438     // Flag value from the expansion, overridability from the original policy, unless the flag is
439     // repeatable, in which case we care about appendability, not overridability.
440     SetValue.Builder setValueExpansion = SetValue.newBuilder();
441     for (String value : subflagValue) {
442       setValueExpansion.addFlagValue(value);
443     }
444     if (subflag.allowsMultiple()) {
445       setValueExpansion.setAppend(originalPolicy.policy.getSetValue().getOverridable());
446     } else {
447       setValueExpansion.setOverridable(originalPolicy.policy.getSetValue().getOverridable());
448     }
449 
450     // Commands from the original policy, flag name of the expansion
451     return new FlagPolicyWithContext(
452         FlagPolicy.newBuilder()
453             .addAllCommands(originalPolicy.policy.getCommandsList())
454             .setFlagName(subflag.getOptionName())
455             .setSetValue(setValueExpansion)
456             .build(),
457         subflagDesc,
458         subflagOrigin);
459   }
460 
461   /**
462    * For an expansion flag in an invocation policy, each flag it expands to must be given a
463    * corresponding policy.
464    */
getSingleValueSubflagAsPolicy( OptionDescription subflagContext, ParsedOptionDescription currentSubflag, FlagPolicyWithContext originalPolicy, boolean isExpansion)465   private static FlagPolicyWithContext getSingleValueSubflagAsPolicy(
466       OptionDescription subflagContext,
467       ParsedOptionDescription currentSubflag,
468       FlagPolicyWithContext originalPolicy,
469       boolean isExpansion)
470       throws OptionsParsingException {
471     FlagPolicyWithContext subflagAsPolicy = null;
472     switch (originalPolicy.policy.getOperationCase()) {
473       case SET_VALUE:
474         if (currentSubflag.getOptionDefinition().allowsMultiple()) {
475           throw new AssertionError(
476               "SetValue subflags with allowMultiple should have been dealt with separately and "
477                   + "accumulated into a single FlagPolicy.");
478         }
479         // Accept null originalValueStrings, they are expected when the subflag is also an expansion
480         // flag.
481         List<String> subflagValue;
482         if (currentSubflag.getUnconvertedValue() == null) {
483           subflagValue = ImmutableList.of();
484         } else {
485           subflagValue = ImmutableList.of(currentSubflag.getUnconvertedValue());
486         }
487         subflagAsPolicy =
488             getSetValueSubflagAsPolicy(
489                 subflagContext, subflagValue, currentSubflag.getOrigin(), originalPolicy);
490         break;
491 
492       case USE_DEFAULT:
493         // Commands from the original policy, flag name of the expansion
494         subflagAsPolicy =
495             new FlagPolicyWithContext(
496                 FlagPolicy.newBuilder()
497                     .addAllCommands(originalPolicy.policy.getCommandsList())
498                     .setFlagName(currentSubflag.getOptionDefinition().getOptionName())
499                     .setUseDefault(UseDefault.getDefaultInstance())
500                     .build(),
501                 subflagContext,
502                 currentSubflag.getOrigin());
503         break;
504 
505       case ALLOW_VALUES:
506         if (isExpansion) {
507           throwAllowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName());
508         }
509         // If this flag is an implicitRequirement, and some values for the parent flag are
510         // allowed, nothing needs to happen on the implicitRequirement that is set for all
511         // values of the flag.
512         break;
513 
514       case DISALLOW_VALUES:
515         if (isExpansion) {
516           throwDisallowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName());
517         }
518         // If this flag is an implicitRequirement, and some values for the parent flag are
519         // disallowed, that implies that all others are allowed, so nothing needs to happen
520         // on the implicitRequirement that is set for all values of the parent flag.
521         break;
522 
523       case OPERATION_NOT_SET:
524         throw new PolicyOperationNotSetException(originalPolicy.policy.getFlagName());
525 
526       default:
527         return null;
528     }
529     return subflagAsPolicy;
530   }
531 
logInApplySetValueOperation( Level loglevel, String formattingString, Object... objects)532   private static void logInApplySetValueOperation(
533       Level loglevel, String formattingString, Object... objects) {
534     // Finding the caller here is relatively expensive and shows up in profiling, so provide it
535     // manually.
536     logger.logp(
537         loglevel,
538         "InvocationPolicyEnforcer",
539         "applySetValueOperation",
540         String.format(formattingString, objects));
541   }
542 
applySetValueOperation( OptionsParser parser, FlagPolicyWithContext flagPolicy, OptionValueDescription valueDescription, Level loglevel)543   private static void applySetValueOperation(
544       OptionsParser parser,
545       FlagPolicyWithContext flagPolicy,
546       OptionValueDescription valueDescription,
547       Level loglevel)
548       throws OptionsParsingException {
549     SetValue setValue = flagPolicy.policy.getSetValue();
550     OptionDefinition optionDefinition = flagPolicy.description.getOptionDefinition();
551 
552     // SetValue.flag_value must have at least 1 value.
553     if (setValue.getFlagValueCount() == 0) {
554       throw new OptionsParsingException(
555           String.format(
556               "SetValue operation from invocation policy for %s does not have a value",
557               optionDefinition));
558     }
559 
560     // Flag must allow multiple values if multiple values are specified by the policy.
561     if (setValue.getFlagValueCount() > 1
562         && !flagPolicy.description.getOptionDefinition().allowsMultiple()) {
563       throw new OptionsParsingException(
564           String.format(
565               "SetValue operation from invocation policy sets multiple values for %s which "
566                   + "does not allow multiple values",
567               optionDefinition));
568     }
569 
570     if (setValue.getOverridable() && valueDescription != null) {
571       // The user set the value for the flag but the flag policy is overridable, so keep the user's
572       // value.
573       logInApplySetValueOperation(
574           loglevel,
575           "Keeping value '%s' from source '%s' for %s because the invocation policy specifying "
576               + "the value(s) '%s' is overridable",
577           valueDescription.getValue(),
578           valueDescription.getSourceString(),
579           optionDefinition,
580           setValue.getFlagValueList());
581     } else {
582 
583       if (!setValue.getAppend()) {
584         // Clear the value in case the flag is a repeated flag so that values don't accumulate.
585         parser.clearValue(flagPolicy.description.getOptionDefinition());
586       }
587 
588       // Set all the flag values from the policy.
589       for (String flagValue : setValue.getFlagValueList()) {
590         if (valueDescription == null) {
591           logInApplySetValueOperation(
592               loglevel,
593               "Setting value for %s from invocation policy to '%s', overriding the default value "
594                   + "'%s'",
595               optionDefinition,
596               flagValue,
597               optionDefinition.getDefaultValue());
598         } else {
599           logInApplySetValueOperation(
600               loglevel,
601               "Setting value for %s from invocation policy to '%s', overriding value '%s' from "
602                   + "'%s'",
603               optionDefinition,
604               flagValue,
605               valueDescription.getValue(),
606               valueDescription.getSourceString());
607         }
608 
609         parser.addOptionValueAtSpecificPriority(flagPolicy.origin, optionDefinition, flagValue);
610       }
611     }
612   }
613 
applyUseDefaultOperation( OptionsParser parser, String policyType, OptionDefinition option, Level loglevel)614   private static void applyUseDefaultOperation(
615       OptionsParser parser, String policyType, OptionDefinition option, Level loglevel)
616       throws OptionsParsingException {
617     OptionValueDescription clearedValueDescription = parser.clearValue(option);
618     if (clearedValueDescription != null) {
619       // Log the removed value.
620       String clearedFlagName = clearedValueDescription.getOptionDefinition().getOptionName();
621       Object clearedFlagDefaultValue =
622           clearedValueDescription.getOptionDefinition().getDefaultValue();
623       logger.log(
624           loglevel,
625           String.format(
626               "Using default value '%s' for flag '%s' as specified by %s invocation policy, "
627                   + "overriding original value '%s' from '%s'",
628               clearedFlagDefaultValue,
629               clearedFlagName,
630               policyType,
631               clearedValueDescription.getValue(),
632               clearedValueDescription.getSourceString()));
633     }
634   }
635 
636   /** Checks the user's flag values against a filtering function. */
637   private abstract static class FilterValueOperation {
638 
639     private static final class AllowValueOperation extends FilterValueOperation {
AllowValueOperation(Level loglevel)640       AllowValueOperation(Level loglevel) {
641         super("Allow", loglevel);
642       }
643 
644       @Override
isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value)645       boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) {
646         return convertedPolicyValues.contains(value);
647       }
648     }
649 
650     private static final class DisallowValueOperation extends FilterValueOperation {
DisallowValueOperation(Level loglevel)651       DisallowValueOperation(Level loglevel) {
652         super("Disalllow", loglevel);
653       }
654 
655       @Override
isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value)656       boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) {
657         // In a disallow operation, the values that the flag policy specifies are not allowed,
658         // so the value is allowed if the set of policy values does not contain the current
659         // flag value.
660         return !convertedPolicyValues.contains(value);
661       }
662     }
663 
664     private final String policyType;
665     private final Level loglevel;
666 
FilterValueOperation(String policyType, Level loglevel)667     FilterValueOperation(String policyType, Level loglevel) {
668       this.policyType = policyType;
669       this.loglevel = loglevel;
670     }
671 
672     /**
673      * Determines if the given value is allowed.
674      *
675      * @param convertedPolicyValues The values given from the FlagPolicy, converted to real objects.
676      * @param value The user value of the flag.
677      * @return True if the value should be allowed, false if it should not.
678      */
isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value)679     abstract boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value);
680 
apply( OptionsParser parser, OptionInstanceOrigin origin, List<String> policyValues, String newValue, boolean useDefault, OptionValueDescription valueDescription, OptionDescription optionDescription)681     void apply(
682         OptionsParser parser,
683         OptionInstanceOrigin origin,
684         List<String> policyValues,
685         String newValue,
686         boolean useDefault,
687         OptionValueDescription valueDescription,
688         OptionDescription optionDescription)
689         throws OptionsParsingException {
690       OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
691       // Convert all the allowed values from strings to real objects using the options'
692       // converters so that they can be checked for equality using real .equals() instead
693       // of string comparison. For example, "--foo=0", "--foo=false", "--nofoo", and "-f-"
694       // (if the option has an abbreviation) are all equal for boolean flags. Plus converters
695       // can be arbitrarily complex.
696       Set<Object> convertedPolicyValues = new HashSet<>();
697       for (String value : policyValues) {
698         Object convertedValue = optionDefinition.getConverter().convert(value);
699         // Some converters return lists, and if the flag is a repeatable flag, the items in the
700         // list from the converter should be added, and not the list itself. Otherwise the items
701         // from invocation policy will be compared to lists, which will never work.
702         // See OptionsParserImpl.ParsedOptionEntry.addValue.
703         if (optionDefinition.allowsMultiple() && convertedValue instanceof List<?>) {
704           convertedPolicyValues.addAll((List<?>) convertedValue);
705         } else {
706           convertedPolicyValues.add(optionDefinition.getConverter().convert(value));
707         }
708       }
709 
710       // Check that if the default value of the flag is disallowed by the policy, that the policy
711       // does not also set use_default. Otherwise the default value would still be set if the
712       // user uses a disallowed value. This doesn't apply to repeatable flags since the default
713       // value for repeatable flags is always the empty list. It also doesn't apply to flags that
714       // are null by default, since these flags' default value is not parsed by the converter, so
715       // there is no guarantee that there exists an accepted user-input value that would also set
716       // the value to NULL. In these cases, we assume that "unset" is a distinct value that is
717       // always allowed.
718       if (!optionDescription.getOptionDefinition().allowsMultiple()
719           && !optionDescription.getOptionDefinition().isSpecialNullDefault()) {
720         boolean defaultValueAllowed =
721             isFlagValueAllowed(
722                 convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue());
723         if (!defaultValueAllowed && useDefault) {
724           throw new OptionsParsingException(
725               String.format(
726                   "%sValues policy disallows the default value '%s' for %s but also specifies to "
727                       + "use the default value",
728                   policyType, optionDefinition.getDefaultValue(), optionDefinition));
729         }
730       }
731 
732       if (valueDescription == null) {
733         // Nothing has set the value yet, so check that the default value from the flag's
734         // definition is allowed. The else case below (i.e. valueDescription is not null) checks for
735         // the flag allowing multiple values, however, flags that allow multiple values cannot have
736         // default values, and their value is always the empty list if they haven't been specified,
737         // which is why new_default_value is not a repeated field.
738         checkDefaultValue(
739             parser, origin, optionDescription, policyValues, newValue, convertedPolicyValues);
740       } else {
741         checkUserValue(
742             parser,
743             origin,
744             optionDescription,
745             valueDescription,
746             policyValues,
747             newValue,
748             useDefault,
749             convertedPolicyValues);
750       }
751     }
752 
checkDefaultValue( OptionsParser parser, OptionInstanceOrigin origin, OptionDescription optionDescription, List<String> policyValues, String newValue, Set<Object> convertedPolicyValues)753     void checkDefaultValue(
754         OptionsParser parser,
755         OptionInstanceOrigin origin,
756         OptionDescription optionDescription,
757         List<String> policyValues,
758         String newValue,
759         Set<Object> convertedPolicyValues)
760         throws OptionsParsingException {
761 
762       OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
763       if (optionDefinition.isSpecialNullDefault()) {
764         // Do nothing, the unset value by definition cannot be set. In option filtering operations,
765         // the value is being filtered, but the value that is `no value` passes any filter.
766         // Otherwise, there is no way to "usedefault" on one of these options that has no value by
767         // default.
768       } else if (!isFlagValueAllowed(convertedPolicyValues, optionDefinition.getDefaultValue())) {
769         if (newValue != null) {
770           // Use the default value from the policy, since the original default is not allowed
771           logger.log(
772               loglevel,
773               String.format(
774                   "Overriding default value '%s' for %s with value '%s' specified by invocation "
775                       + "policy. %sed values are: %s",
776                   optionDefinition.getDefaultValue(),
777                   optionDefinition,
778                   newValue,
779                   policyType,
780                   policyValues));
781           parser.clearValue(optionDefinition);
782           parser.addOptionValueAtSpecificPriority(origin, optionDefinition, newValue);
783         } else {
784           // The operation disallows the default value, but doesn't supply a new value.
785           throw new OptionsParsingException(
786               String.format(
787                   "Default flag value '%s' for %s is not allowed by invocation policy, but "
788                       + "the policy does not provide a new value. %sed values are: %s",
789                   optionDescription.getOptionDefinition().getDefaultValue(),
790                   optionDefinition,
791                   policyType,
792                   policyValues));
793         }
794       }
795     }
796 
checkUserValue( OptionsParser parser, OptionInstanceOrigin origin, OptionDescription optionDescription, OptionValueDescription valueDescription, List<String> policyValues, String newValue, boolean useDefault, Set<Object> convertedPolicyValues)797     void checkUserValue(
798         OptionsParser parser,
799         OptionInstanceOrigin origin,
800         OptionDescription optionDescription,
801         OptionValueDescription valueDescription,
802         List<String> policyValues,
803         String newValue,
804         boolean useDefault,
805         Set<Object> convertedPolicyValues)
806         throws OptionsParsingException {
807       OptionDefinition option = optionDescription.getOptionDefinition();
808       if (optionDescription.getOptionDefinition().allowsMultiple()) {
809         // allowMultiple requires that the type of the option be List<T>, so cast from Object
810         // to List<?>.
811         List<?> optionValues = (List<?>) valueDescription.getValue();
812         for (Object value : optionValues) {
813           if (!isFlagValueAllowed(convertedPolicyValues, value)) {
814             if (useDefault) {
815               applyUseDefaultOperation(parser, policyType + "Values", option, loglevel);
816             } else {
817               throw new OptionsParsingException(
818                   String.format(
819                       "Flag value '%s' for %s is not allowed by invocation policy. %sed values "
820                           + "are: %s",
821                       value, option, policyType, policyValues));
822             }
823           }
824         }
825 
826       } else {
827 
828         if (!isFlagValueAllowed(convertedPolicyValues, valueDescription.getValue())) {
829           if (newValue != null) {
830             logger.log(
831                 loglevel,
832                 String.format(
833                     "Overriding disallowed value '%s' for %s with value '%s' "
834                         + "specified by invocation policy. %sed values are: %s",
835                     valueDescription.getValue(), option, newValue, policyType, policyValues));
836             parser.clearValue(option);
837             parser.addOptionValueAtSpecificPriority(origin, option, newValue);
838           } else if (useDefault) {
839             applyUseDefaultOperation(parser, policyType + "Values", option, loglevel);
840           } else {
841             throw new OptionsParsingException(
842                 String.format(
843                     "Flag value '%s' for %s is not allowed by invocation policy and the "
844                         + "policy does not specify a new value. %sed values are: %s",
845                     valueDescription.getValue(), option, policyType, policyValues));
846           }
847         }
848       }
849     }
850   }
851 }
852