001/*
002 * Copyright 2016-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-2019 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.examples;
022
023
024
025import java.io.BufferedReader;
026import java.io.FileInputStream;
027import java.io.FileReader;
028import java.io.FileOutputStream;
029import java.io.InputStream;
030import java.io.InputStreamReader;
031import java.io.OutputStream;
032import java.util.LinkedHashMap;
033
034import com.unboundid.ldap.sdk.ResultCode;
035import com.unboundid.ldap.sdk.Version;
036import com.unboundid.util.Base64;
037import com.unboundid.util.ByteStringBuffer;
038import com.unboundid.util.CommandLineTool;
039import com.unboundid.util.Debug;
040import com.unboundid.util.StaticUtils;
041import com.unboundid.util.ThreadSafety;
042import com.unboundid.util.ThreadSafetyLevel;
043import com.unboundid.util.args.ArgumentException;
044import com.unboundid.util.args.ArgumentParser;
045import com.unboundid.util.args.BooleanArgument;
046import com.unboundid.util.args.FileArgument;
047import com.unboundid.util.args.StringArgument;
048import com.unboundid.util.args.SubCommand;
049
050
051
052/**
053 * This class provides a tool that can be used to perform base64 encoding and
054 * decoding from the command line.  It provides two subcommands:  encode and
055 * decode.  Each of those subcommands offers the following arguments:
056 * <UL>
057 *   <LI>
058 *     "--data {data}" -- specifies the data to be encoded or decoded.
059 *   </LI>
060 *   <LI>
061 *     "--inputFile {data}" -- specifies the path to a file containing the data
062 *     to be encoded or decoded.
063 *   </LI>
064 *   <LI>
065 *     "--outputFile {data}" -- specifies the path to a file to which the
066 *     encoded or decoded data should be written.
067 *   </LI>
068 * </UL>
069 * The "--data" and "--inputFile" arguments are mutually exclusive, and if
070 * neither is provided, the data to encode will be read from standard input.
071 * If the "--outputFile" argument is not provided, then the result will be
072 * written to standard output.
073 */
074@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
075public final class Base64Tool
076       extends CommandLineTool
077{
078  /**
079   * The column at which to wrap long lines of output.
080   */
081  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
082
083
084
085  /**
086   * The name of the argument used to indicate whether to add an end-of-line
087   * marker to the end of the base64-encoded data.
088   */
089  private static final String ARG_NAME_ADD_TRAILING_LINE_BREAK =
090       "addTrailingLineBreak";
091
092
093
094  /**
095   * The name of the argument used to specify the data to encode or decode.
096   */
097  private static final String ARG_NAME_DATA = "data";
098
099
100
101  /**
102   * The name of the argument used to indicate whether to ignore any end-of-line
103   * marker that might be present at the end of the data to encode.
104   */
105  private static final String ARG_NAME_IGNORE_TRAILING_LINE_BREAK =
106       "ignoreTrailingLineBreak";
107
108
109
110  /**
111   * The name of the argument used to specify the path to the input file with
112   * the data to encode or decode.
113   */
114  private static final String ARG_NAME_INPUT_FILE = "inputFile";
115
116
117
118  /**
119   * The name of the argument used to specify the path to the output file into
120   * which to write the encoded or decoded data.
121   */
122  private static final String ARG_NAME_OUTPUT_FILE = "outputFile";
123
124
125
126  /**
127   * The name of the argument used to indicate that the encoding and decoding
128   * should be performed using the base64url alphabet rather than the standard
129   * base64 alphabet.
130   */
131  private static final String ARG_NAME_URL = "url";
132
133
134
135  /**
136   * The name of the subcommand used to decode data.
137   */
138  private static final String SUBCOMMAND_NAME_DECODE = "decode";
139
140
141
142  /**
143   * The name of the subcommand used to encode data.
144   */
145  private static final String SUBCOMMAND_NAME_ENCODE = "encode";
146
147
148
149  // The argument parser for this tool.
150  private volatile ArgumentParser parser;
151
152  // The input stream to use as standard input.
153  private final InputStream in;
154
155
156
157  /**
158   * Runs the tool with the provided set of arguments.
159   *
160   * @param  args  The command line arguments provided to this program.
161   */
162  public static void main(final String... args)
163  {
164    final ResultCode resultCode = main(System.in, System.out, System.err, args);
165    if (resultCode != ResultCode.SUCCESS)
166    {
167      System.exit(resultCode.intValue());
168    }
169  }
170
171
172
173  /**
174   * Runs the tool with the provided information.
175   *
176   * @param  in    The input stream to use for standard input.  It may be
177   *               {@code null} if no standard input is needed.
178   * @param  out   The output stream to which standard out should be written.
179   *               It may be {@code null} if standard output should be
180   *               suppressed.
181   * @param  err   The output stream to which standard error should be written.
182   *               It may be {@code null} if standard error should be
183   *               suppressed.
184   * @param  args  The command line arguments provided to this program.
185   *
186   * @return  The result code obtained from running the tool.  A result code
187   *          other than {@link ResultCode#SUCCESS} will indicate that an error
188   *          occurred.
189   */
190  public static ResultCode main(final InputStream in, final OutputStream out,
191                                final OutputStream err, final String... args)
192  {
193    final Base64Tool tool = new Base64Tool(in, out, err);
194    return tool.runTool(args);
195  }
196
197
198
199  /**
200   * Creates a new instance of this tool with the provided information.
201   * Standard input will not be available.
202   *
203   * @param  out  The output stream to which standard out should be written.
204   *              It may be {@code null} if standard output should be
205   *              suppressed.
206   * @param  err  The output stream to which standard error should be written.
207   *              It may be {@code null} if standard error should be suppressed.
208   */
209  public Base64Tool(final OutputStream out, final OutputStream err)
210  {
211    this(null, out, err);
212  }
213
214
215
216  /**
217   * Creates a new instance of this tool with the provided information.
218   *
219   * @param  in   The input stream to use for standard input.  It may be
220   *              {@code null} if no standard input is needed.
221   * @param  out  The output stream to which standard out should be written.
222   *              It may be {@code null} if standard output should be
223   *              suppressed.
224   * @param  err  The output stream to which standard error should be written.
225   *              It may be {@code null} if standard error should be suppressed.
226   */
227  public Base64Tool(final InputStream in, final OutputStream out,
228                    final OutputStream err)
229  {
230    super(out, err);
231
232    this.in = in;
233
234    parser = null;
235  }
236
237
238
239  /**
240   * Retrieves the name of this tool.  It should be the name of the command used
241   * to invoke this tool.
242   *
243   * @return  The name for this tool.
244   */
245  @Override()
246  public String getToolName()
247  {
248    return "base64";
249  }
250
251
252
253  /**
254   * Retrieves a human-readable description for this tool.
255   *
256   * @return  A human-readable description for this tool.
257   */
258  @Override()
259  public String getToolDescription()
260  {
261    return "Base64 encode raw data, or base64-decode encoded data.  The data " +
262         "to encode or decode may be provided via an argument value, in a " +
263         "file, or read from standard input.  The output may be written to a " +
264         "file or standard output.";
265  }
266
267
268
269  /**
270   * Retrieves a version string for this tool, if available.
271   *
272   * @return  A version string for this tool, or {@code null} if none is
273   *          available.
274   */
275  @Override()
276  public String getToolVersion()
277  {
278    return Version.NUMERIC_VERSION_STRING;
279  }
280
281
282
283  /**
284   * Indicates whether this tool should provide support for an interactive mode,
285   * in which the tool offers a mode in which the arguments can be provided in
286   * a text-driven menu rather than requiring them to be given on the command
287   * line.  If interactive mode is supported, it may be invoked using the
288   * "--interactive" argument.  Alternately, if interactive mode is supported
289   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
290   * interactive mode may be invoked by simply launching the tool without any
291   * arguments.
292   *
293   * @return  {@code true} if this tool supports interactive mode, or
294   *          {@code false} if not.
295   */
296  @Override()
297  public boolean supportsInteractiveMode()
298  {
299    return true;
300  }
301
302
303
304  /**
305   * Indicates whether this tool defaults to launching in interactive mode if
306   * the tool is invoked without any command-line arguments.  This will only be
307   * used if {@link #supportsInteractiveMode()} returns {@code true}.
308   *
309   * @return  {@code true} if this tool defaults to using interactive mode if
310   *          launched without any command-line arguments, or {@code false} if
311   *          not.
312   */
313  @Override()
314  public boolean defaultsToInteractiveMode()
315  {
316    return true;
317  }
318
319
320
321  /**
322   * Indicates whether this tool supports the use of a properties file for
323   * specifying default values for arguments that aren't specified on the
324   * command line.
325   *
326   * @return  {@code true} if this tool supports the use of a properties file
327   *          for specifying default values for arguments that aren't specified
328   *          on the command line, or {@code false} if not.
329   */
330  @Override()
331  public boolean supportsPropertiesFile()
332  {
333    return true;
334  }
335
336
337
338  /**
339   * Indicates whether this tool should provide arguments for redirecting output
340   * to a file.  If this method returns {@code true}, then the tool will offer
341   * an "--outputFile" argument that will specify the path to a file to which
342   * all standard output and standard error content will be written, and it will
343   * also offer a "--teeToStandardOut" argument that can only be used if the
344   * "--outputFile" argument is present and will cause all output to be written
345   * to both the specified output file and to standard output.
346   *
347   * @return  {@code true} if this tool should provide arguments for redirecting
348   *          output to a file, or {@code false} if not.
349   */
350  @Override()
351  protected boolean supportsOutputFile()
352  {
353    // This tool provides its own output file support.
354    return false;
355  }
356
357
358
359  /**
360   * Adds the command-line arguments supported for use with this tool to the
361   * provided argument parser.  The tool may need to retain references to the
362   * arguments (and/or the argument parser, if trailing arguments are allowed)
363   * to it in order to obtain their values for use in later processing.
364   *
365   * @param  parser  The argument parser to which the arguments are to be added.
366   *
367   * @throws  ArgumentException  If a problem occurs while adding any of the
368   *                             tool-specific arguments to the provided
369   *                             argument parser.
370   */
371  @Override()
372  public void addToolArguments(final ArgumentParser parser)
373         throws ArgumentException
374  {
375    this.parser = parser;
376
377
378    // Create the subcommand for encoding data.
379    final ArgumentParser encodeParser =
380         new ArgumentParser("encode", "Base64-encodes raw data.");
381
382    final StringArgument encodeDataArgument = new StringArgument('d',
383         ARG_NAME_DATA, false, 1, "{data}",
384         "The raw data to be encoded.  If neither the --" + ARG_NAME_DATA +
385              " nor the --" + ARG_NAME_INPUT_FILE + " argument is provided, " +
386              "then the data will be read from standard input.");
387    encodeDataArgument.addLongIdentifier("rawData", true);
388    encodeDataArgument.addLongIdentifier("raw-data", true);
389    encodeParser.addArgument(encodeDataArgument);
390
391    final FileArgument encodeDataFileArgument = new FileArgument('f',
392         ARG_NAME_INPUT_FILE, false, 1, null,
393         "The path to a file containing the raw data to be encoded.  If " +
394              "neither the --" + ARG_NAME_DATA + " nor the --" +
395              ARG_NAME_INPUT_FILE + " argument is provided, then the data " +
396              "will be read from standard input.",
397         true, true, true, false);
398    encodeDataFileArgument.addLongIdentifier("rawDataFile", true);
399    encodeDataFileArgument.addLongIdentifier("input-file", true);
400    encodeDataFileArgument.addLongIdentifier("raw-data-file", true);
401    encodeParser.addArgument(encodeDataFileArgument);
402
403    final FileArgument encodeOutputFileArgument = new FileArgument('o',
404         ARG_NAME_OUTPUT_FILE, false, 1, null,
405         "The path to a file to which the encoded data should be written.  " +
406              "If this is not provided, the encoded data will be written to " +
407              "standard output.",
408         false, true, true, false);
409    encodeOutputFileArgument.addLongIdentifier("toEncodedFile", true);
410    encodeOutputFileArgument.addLongIdentifier("output-file", true);
411    encodeOutputFileArgument.addLongIdentifier("to-encoded-file", true);
412    encodeParser.addArgument(encodeOutputFileArgument);
413
414    final BooleanArgument encodeURLArgument = new BooleanArgument(null,
415         ARG_NAME_URL,
416         "Encode the data with the base64url mechanism rather than the " +
417              "standard base64 mechanism.");
418    encodeParser.addArgument(encodeURLArgument);
419
420    final BooleanArgument encodeIgnoreTrailingEOLArgument = new BooleanArgument(
421         null, ARG_NAME_IGNORE_TRAILING_LINE_BREAK,
422         "Ignore any end-of-line marker that may be present at the end of " +
423              "the data to encode.");
424    encodeIgnoreTrailingEOLArgument.addLongIdentifier(
425         "ignore-trailing-line-break", true);
426    encodeParser.addArgument(encodeIgnoreTrailingEOLArgument);
427
428    encodeParser.addExclusiveArgumentSet(encodeDataArgument,
429         encodeDataFileArgument);
430
431    final LinkedHashMap<String[],String> encodeExamples =
432         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
433    encodeExamples.put(
434         new String[]
435         {
436           "encode",
437           "--data", "Hello"
438         },
439         "Base64-encodes the string 'Hello' and writes the result to " +
440              "standard output.");
441    encodeExamples.put(
442         new String[]
443         {
444           "encode",
445           "--inputFile", "raw-data.txt",
446           "--outputFile", "encoded-data.txt",
447         },
448         "Base64-encodes the data contained in the 'raw-data.txt' file and " +
449              "writes the result to the 'encoded-data.txt' file.");
450    encodeExamples.put(
451         new String[]
452         {
453           "encode"
454         },
455         "Base64-encodes data read from standard input and writes the result " +
456              "to standard output.");
457
458    final SubCommand encodeSubCommand = new SubCommand(SUBCOMMAND_NAME_ENCODE,
459         "Base64-encodes raw data.", encodeParser, encodeExamples);
460    parser.addSubCommand(encodeSubCommand);
461
462
463    // Create the subcommand for decoding data.
464    final ArgumentParser decodeParser =
465         new ArgumentParser("decode", "Decodes base64-encoded data.");
466
467    final StringArgument decodeDataArgument = new StringArgument('d',
468         ARG_NAME_DATA, false, 1, "{data}",
469         "The base64-encoded data to be decoded.  If neither the --" +
470              ARG_NAME_DATA + " nor the --" + ARG_NAME_INPUT_FILE +
471              " argument is provided, then the data will be read from " +
472              "standard input.");
473    decodeDataArgument.addLongIdentifier("encodedData", true);
474    decodeDataArgument.addLongIdentifier("encoded-data", true);
475    decodeParser.addArgument(decodeDataArgument);
476
477    final FileArgument decodeDataFileArgument = new FileArgument('f',
478         ARG_NAME_INPUT_FILE, false, 1, null,
479         "The path to a file containing the base64-encoded data to be " +
480              "decoded.  If neither the --" + ARG_NAME_DATA + " nor the --" +
481              ARG_NAME_INPUT_FILE + " argument is provided, then the data " +
482              "will be read from standard input.",
483         true, true, true, false);
484    decodeDataFileArgument.addLongIdentifier("encodedDataFile", true);
485    decodeDataFileArgument.addLongIdentifier("input-file", true);
486    decodeDataFileArgument.addLongIdentifier("encoded-data-file", true);
487    decodeParser.addArgument(decodeDataFileArgument);
488
489    final FileArgument decodeOutputFileArgument = new FileArgument('o',
490         ARG_NAME_OUTPUT_FILE, false, 1, null,
491         "The path to a file to which the decoded data should be written.  " +
492              "If this is not provided, the decoded data will be written to " +
493              "standard output.",
494         false, true, true, false);
495    decodeOutputFileArgument.addLongIdentifier("toRawFile", true);
496    decodeOutputFileArgument.addLongIdentifier("output-file", true);
497    decodeOutputFileArgument.addLongIdentifier("to-raw-file", true);
498    decodeParser.addArgument(decodeOutputFileArgument);
499
500    final BooleanArgument decodeURLArgument = new BooleanArgument(null,
501         ARG_NAME_URL,
502         "Decode the data with the base64url mechanism rather than the " +
503              "standard base64 mechanism.");
504    decodeParser.addArgument(decodeURLArgument);
505
506    final BooleanArgument decodeAddTrailingLineBreak = new BooleanArgument(
507         null, ARG_NAME_ADD_TRAILING_LINE_BREAK,
508         "Add a line break to the end of the decoded data.");
509    decodeAddTrailingLineBreak.addLongIdentifier("add-trailing-line-break",
510         true);
511    decodeParser.addArgument(decodeAddTrailingLineBreak);
512
513    decodeParser.addExclusiveArgumentSet(decodeDataArgument,
514         decodeDataFileArgument);
515
516    final LinkedHashMap<String[],String> decodeExamples =
517         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
518    decodeExamples.put(
519         new String[]
520         {
521           "decode",
522           "--data", "SGVsbG8="
523         },
524         "Base64-decodes the string 'SGVsbG8=' and writes the result to " +
525              "standard output.");
526    decodeExamples.put(
527         new String[]
528         {
529           "decode",
530           "--inputFile", "encoded-data.txt",
531           "--outputFile", "decoded-data.txt",
532         },
533         "Base64-decodes the data contained in the 'encoded-data.txt' file " +
534              "and writes the result to the 'raw-data.txt' file.");
535    decodeExamples.put(
536         new String[]
537         {
538           "decode"
539         },
540         "Base64-decodes data read from standard input and writes the result " +
541              "to standard output.");
542
543    final SubCommand decodeSubCommand = new SubCommand(SUBCOMMAND_NAME_DECODE,
544         "Decodes base64-encoded data.", decodeParser, decodeExamples);
545    parser.addSubCommand(decodeSubCommand);
546  }
547
548
549
550  /**
551   * Performs the core set of processing for this tool.
552   *
553   * @return  A result code that indicates whether the processing completed
554   *          successfully.
555   */
556  @Override()
557  public ResultCode doToolProcessing()
558  {
559    // Get the subcommand selected by the user.
560    final SubCommand subCommand = parser.getSelectedSubCommand();
561    if (subCommand == null)
562    {
563      // This should never happen.
564      wrapErr(0, WRAP_COLUMN, "No subcommand was selected.");
565      return ResultCode.PARAM_ERROR;
566    }
567
568
569    // Take the appropriate action based on the selected subcommand.
570    if (subCommand.hasName(SUBCOMMAND_NAME_ENCODE))
571    {
572      return doEncode(subCommand.getArgumentParser());
573    }
574    else
575    {
576      return doDecode(subCommand.getArgumentParser());
577    }
578  }
579
580
581
582  /**
583   * Performs the necessary work for base64 encoding.
584   *
585   * @param  p  The argument parser for the encode subcommand.
586   *
587   * @return  A result code that indicates whether the processing completed
588   *          successfully.
589   */
590  private ResultCode doEncode(final ArgumentParser p)
591  {
592    // Get the data to encode.
593    final ByteStringBuffer rawDataBuffer = new ByteStringBuffer();
594    final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA);
595    if ((dataArg != null) && dataArg.isPresent())
596    {
597      rawDataBuffer.append(dataArg.getValue());
598    }
599    else
600    {
601      try
602      {
603        final InputStream inputStream;
604        final FileArgument inputFileArg =
605             p.getFileArgument(ARG_NAME_INPUT_FILE);
606        if ((inputFileArg != null) && inputFileArg.isPresent())
607        {
608          inputStream = new FileInputStream(inputFileArg.getValue());
609        }
610        else
611        {
612          inputStream = in;
613        }
614
615        final byte[] buffer = new byte[8192];
616        while (true)
617        {
618          final int bytesRead = inputStream.read(buffer);
619          if (bytesRead <= 0)
620          {
621            break;
622          }
623
624          rawDataBuffer.append(buffer, 0, bytesRead);
625        }
626
627        inputStream.close();
628      }
629      catch (final Exception e)
630      {
631        Debug.debugException(e);
632        wrapErr(0, WRAP_COLUMN,
633             "An error occurred while attempting to read the data to encode:  ",
634             StaticUtils.getExceptionMessage(e));
635        return ResultCode.LOCAL_ERROR;
636      }
637    }
638
639
640    // If we should ignore any trailing end-of-line markers, then do that now.
641    final BooleanArgument ignoreEOLArg =
642         p.getBooleanArgument(ARG_NAME_IGNORE_TRAILING_LINE_BREAK);
643    if ((ignoreEOLArg != null) && ignoreEOLArg.isPresent())
644    {
645stripEOLLoop:
646      while (rawDataBuffer.length() > 0)
647      {
648        switch (rawDataBuffer.getBackingArray()[rawDataBuffer.length() - 1])
649        {
650          case '\n':
651          case '\r':
652            rawDataBuffer.delete(rawDataBuffer.length() - 1, 1);
653            break;
654          default:
655            break stripEOLLoop;
656        }
657      }
658    }
659
660
661    // Base64-encode the data.
662    final byte[] rawDataArray = rawDataBuffer.toByteArray();
663    final ByteStringBuffer encodedDataBuffer =
664         new ByteStringBuffer(4 * rawDataBuffer.length() / 3 + 3);
665    final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL);
666    if ((urlArg != null) && urlArg.isPresent())
667    {
668      Base64.urlEncode(rawDataArray, 0, rawDataArray.length, encodedDataBuffer,
669           false);
670    }
671    else
672    {
673      Base64.encode(rawDataArray, encodedDataBuffer);
674    }
675
676
677    // Write the encoded data.
678    final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE);
679    if ((outputFileArg != null) && outputFileArg.isPresent())
680    {
681      try
682      {
683        final FileOutputStream outputStream =
684             new FileOutputStream(outputFileArg.getValue(), false);
685        encodedDataBuffer.write(outputStream);
686        outputStream.write(StaticUtils.EOL_BYTES);
687        outputStream.flush();
688        outputStream.close();
689      }
690      catch (final Exception e)
691      {
692        Debug.debugException(e);
693        wrapErr(0, WRAP_COLUMN,
694             "An error occurred while attempting to write the base64-encoded " +
695                  "data to output file ",
696             outputFileArg.getValue().getAbsolutePath(), ":  ",
697             StaticUtils.getExceptionMessage(e));
698        err("Base64-encoded data:");
699        err(encodedDataBuffer.toString());
700        return ResultCode.LOCAL_ERROR;
701      }
702    }
703    else
704    {
705      out(encodedDataBuffer.toString());
706    }
707
708
709    return ResultCode.SUCCESS;
710  }
711
712
713
714  /**
715   * Performs the necessary work for base64 decoding.
716   *
717   * @param  p  The argument parser for the decode subcommand.
718   *
719   * @return  A result code that indicates whether the processing completed
720   *          successfully.
721   */
722  private ResultCode doDecode(final ArgumentParser p)
723  {
724    // Get the data to decode.  We'll always ignore the following:
725    // - Line breaks
726    // - Blank lines
727    // - Lines that start with an octothorpe (#)
728    //
729    // Unless the --url argument was provided, then we'll also ignore lines that
730    // start with a dash (like those used as start and end markers in a
731    // PEM-encoded certificate).  Since dashes are part of the base64url
732    // alphabet, we can't ignore dashes if the --url argument was provided.
733    final ByteStringBuffer encodedDataBuffer = new ByteStringBuffer();
734    final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL);
735    final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA);
736    if ((dataArg != null) && dataArg.isPresent())
737    {
738      encodedDataBuffer.append(dataArg.getValue());
739    }
740    else
741    {
742      try
743      {
744        final BufferedReader reader;
745        final FileArgument inputFileArg =
746             p.getFileArgument(ARG_NAME_INPUT_FILE);
747        if ((inputFileArg != null) && inputFileArg.isPresent())
748        {
749          reader = new BufferedReader(new FileReader(inputFileArg.getValue()));
750        }
751        else
752        {
753          reader = new BufferedReader(new InputStreamReader(in));
754        }
755
756        while (true)
757        {
758          final String line = reader.readLine();
759          if (line == null)
760          {
761            break;
762          }
763
764          if ((line.length() == 0) || line.startsWith("#"))
765          {
766            continue;
767          }
768
769          if (line.startsWith("-") &&
770              ((urlArg == null) || (! urlArg.isPresent())))
771          {
772            continue;
773          }
774
775          encodedDataBuffer.append(line);
776        }
777
778        reader.close();
779      }
780      catch (final Exception e)
781      {
782        Debug.debugException(e);
783        wrapErr(0, WRAP_COLUMN,
784             "An error occurred while attempting to read the data to decode:  ",
785             StaticUtils.getExceptionMessage(e));
786        return ResultCode.LOCAL_ERROR;
787      }
788    }
789
790
791    // Base64-decode the data.
792    final ByteStringBuffer rawDataBuffer = new
793         ByteStringBuffer(encodedDataBuffer.length());
794    if ((urlArg != null) && urlArg.isPresent())
795    {
796      try
797      {
798        rawDataBuffer.append(Base64.urlDecode(encodedDataBuffer.toString()));
799      }
800      catch (final Exception e)
801      {
802        Debug.debugException(e);
803        wrapErr(0, WRAP_COLUMN,
804             "An error occurred while attempting to base64url-decode the " +
805                  "provided data:  " + StaticUtils.getExceptionMessage(e));
806        return ResultCode.LOCAL_ERROR;
807      }
808    }
809    else
810    {
811      try
812      {
813        rawDataBuffer.append(Base64.decode(encodedDataBuffer.toString()));
814      }
815      catch (final Exception e)
816      {
817        Debug.debugException(e);
818        wrapErr(0, WRAP_COLUMN,
819             "An error occurred while attempting to base64-decode the " +
820                  "provided data:  " + StaticUtils.getExceptionMessage(e));
821        return ResultCode.LOCAL_ERROR;
822      }
823    }
824
825
826    // If we should add a newline, then do that now.
827    final BooleanArgument addEOLArg =
828         p.getBooleanArgument(ARG_NAME_ADD_TRAILING_LINE_BREAK);
829    if ((addEOLArg != null) && addEOLArg.isPresent())
830    {
831      rawDataBuffer.append(StaticUtils.EOL_BYTES);
832    }
833
834
835    // Write the decoded data.
836    final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE);
837    if ((outputFileArg != null) && outputFileArg.isPresent())
838    {
839      try
840      {
841        final FileOutputStream outputStream =
842             new FileOutputStream(outputFileArg.getValue(), false);
843        rawDataBuffer.write(outputStream);
844        outputStream.flush();
845        outputStream.close();
846      }
847      catch (final Exception e)
848      {
849        Debug.debugException(e);
850        wrapErr(0, WRAP_COLUMN,
851             "An error occurred while attempting to write the base64-decoded " +
852                  "data to output file ",
853             outputFileArg.getValue().getAbsolutePath(), ":  ",
854             StaticUtils.getExceptionMessage(e));
855        err("Base64-decoded data:");
856        err(encodedDataBuffer.toString());
857        return ResultCode.LOCAL_ERROR;
858      }
859    }
860    else
861    {
862      final byte[] rawDataArray = rawDataBuffer.toByteArray();
863      getOut().write(rawDataArray, 0, rawDataArray.length);
864      getOut().flush();
865    }
866
867
868    return ResultCode.SUCCESS;
869  }
870
871
872
873  /**
874   * Retrieves a set of information that may be used to generate example usage
875   * information.  Each element in the returned map should consist of a map
876   * between an example set of arguments and a string that describes the
877   * behavior of the tool when invoked with that set of arguments.
878   *
879   * @return  A set of information that may be used to generate example usage
880   *          information.  It may be {@code null} or empty if no example usage
881   *          information is available.
882   */
883  @Override()
884  public LinkedHashMap<String[],String> getExampleUsages()
885  {
886    final LinkedHashMap<String[],String> examples =
887         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
888
889    examples.put(
890         new String[]
891         {
892           "encode",
893           "--data", "Hello"
894         },
895         "Base64-encodes the string 'Hello' and writes the result to " +
896              "standard output.");
897
898    examples.put(
899         new String[]
900         {
901           "decode",
902           "--inputFile", "encoded-data.txt",
903           "--outputFile", "decoded-data.txt",
904         },
905         "Base64-decodes the data contained in the 'encoded-data.txt' file " +
906              "and writes the result to the 'raw-data.txt' file.");
907
908    return examples;
909  }
910}