001/*
002 * Copyright 2016-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-2018 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   *
202   * @param  in   The input stream to use for standard input.  It may be
203   *              {@code null} if no standard input is needed.
204   * @param  out  The output stream to which standard out should be written.
205   *              It may be {@code null} if standard output should be
206   *              suppressed.
207   * @param  err  The output stream to which standard error should be written.
208   *              It may be {@code null} if standard error should be suppressed.
209   */
210  public Base64Tool(final InputStream in, final OutputStream out,
211                    final OutputStream err)
212  {
213    super(out, err);
214
215    this.in = in;
216
217    parser = null;
218  }
219
220
221
222  /**
223   * Retrieves the name of this tool.  It should be the name of the command used
224   * to invoke this tool.
225   *
226   * @return  The name for this tool.
227   */
228  @Override()
229  public String getToolName()
230  {
231    return "base64";
232  }
233
234
235
236  /**
237   * Retrieves a human-readable description for this tool.
238   *
239   * @return  A human-readable description for this tool.
240   */
241  @Override()
242  public String getToolDescription()
243  {
244    return "Base64 encode raw data, or base64-decode encoded data.  The data " +
245         "to encode or decode may be provided via an argument value, in a " +
246         "file, or read from standard input.  The output may be written to a " +
247         "file or standard output.";
248  }
249
250
251
252  /**
253   * Retrieves a version string for this tool, if available.
254   *
255   * @return  A version string for this tool, or {@code null} if none is
256   *          available.
257   */
258  @Override()
259  public String getToolVersion()
260  {
261    return Version.NUMERIC_VERSION_STRING;
262  }
263
264
265
266  /**
267   * Indicates whether this tool should provide support for an interactive mode,
268   * in which the tool offers a mode in which the arguments can be provided in
269   * a text-driven menu rather than requiring them to be given on the command
270   * line.  If interactive mode is supported, it may be invoked using the
271   * "--interactive" argument.  Alternately, if interactive mode is supported
272   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
273   * interactive mode may be invoked by simply launching the tool without any
274   * arguments.
275   *
276   * @return  {@code true} if this tool supports interactive mode, or
277   *          {@code false} if not.
278   */
279  @Override()
280  public boolean supportsInteractiveMode()
281  {
282    // TODO:  Add support for interactive mode for tools with subcommands.
283    return true;
284  }
285
286
287
288  /**
289   * Indicates whether this tool defaults to launching in interactive mode if
290   * the tool is invoked without any command-line arguments.  This will only be
291   * used if {@link #supportsInteractiveMode()} returns {@code true}.
292   *
293   * @return  {@code true} if this tool defaults to using interactive mode if
294   *          launched without any command-line arguments, or {@code false} if
295   *          not.
296   */
297  @Override()
298  public boolean defaultsToInteractiveMode()
299  {
300    // TODO:  Add support for interactive mode for tools with subcommands.
301    return true;
302  }
303
304
305
306  /**
307   * Indicates whether this tool supports the use of a properties file for
308   * specifying default values for arguments that aren't specified on the
309   * command line.
310   *
311   * @return  {@code true} if this tool supports the use of a properties file
312   *          for specifying default values for arguments that aren't specified
313   *          on the command line, or {@code false} if not.
314   */
315  @Override()
316  public boolean supportsPropertiesFile()
317  {
318    // TODO:  Add support for using a properties file for subcommand-specific
319    // properties.
320    return true;
321  }
322
323
324
325  /**
326   * Indicates whether this tool should provide arguments for redirecting output
327   * to a file.  If this method returns {@code true}, then the tool will offer
328   * an "--outputFile" argument that will specify the path to a file to which
329   * all standard output and standard error content will be written, and it will
330   * also offer a "--teeToStandardOut" argument that can only be used if the
331   * "--outputFile" argument is present and will cause all output to be written
332   * to both the specified output file and to standard output.
333   *
334   * @return  {@code true} if this tool should provide arguments for redirecting
335   *          output to a file, or {@code false} if not.
336   */
337  @Override()
338  protected boolean supportsOutputFile()
339  {
340    // This tool provides its own output file support.
341    return false;
342  }
343
344
345
346  /**
347   * Adds the command-line arguments supported for use with this tool to the
348   * provided argument parser.  The tool may need to retain references to the
349   * arguments (and/or the argument parser, if trailing arguments are allowed)
350   * to it in order to obtain their values for use in later processing.
351   *
352   * @param  parser  The argument parser to which the arguments are to be added.
353   *
354   * @throws  ArgumentException  If a problem occurs while adding any of the
355   *                             tool-specific arguments to the provided
356   *                             argument parser.
357   */
358  @Override()
359  public void addToolArguments(final ArgumentParser parser)
360         throws ArgumentException
361  {
362    this.parser = parser;
363
364
365    // Create the subcommand for encoding data.
366    final ArgumentParser encodeParser =
367         new ArgumentParser("encode", "Base64-encodes raw data.");
368
369    final StringArgument encodeDataArgument = new StringArgument('d',
370         ARG_NAME_DATA, false, 1, "{data}",
371         "The raw data to be encoded.  If neither the --" + ARG_NAME_DATA +
372              " nor the --" + ARG_NAME_INPUT_FILE + " argument is provided, " +
373              "then the data will be read from standard input.");
374    encodeDataArgument.addLongIdentifier("rawData", true);
375    encodeDataArgument.addLongIdentifier("raw-data", true);
376    encodeParser.addArgument(encodeDataArgument);
377
378    final FileArgument encodeDataFileArgument = new FileArgument('f',
379         ARG_NAME_INPUT_FILE, false, 1, null,
380         "The path to a file containing the raw data to be encoded.  If " +
381              "neither the --" + ARG_NAME_DATA + " nor the --" +
382              ARG_NAME_INPUT_FILE + " argument is provided, then the data " +
383              "will be read from standard input.",
384         true, true, true, false);
385    encodeDataFileArgument.addLongIdentifier("rawDataFile", true);
386    encodeDataFileArgument.addLongIdentifier("input-file", true);
387    encodeDataFileArgument.addLongIdentifier("raw-data-file", true);
388    encodeParser.addArgument(encodeDataFileArgument);
389
390    final FileArgument encodeOutputFileArgument = new FileArgument('o',
391         ARG_NAME_OUTPUT_FILE, false, 1, null,
392         "The path to a file to which the encoded data should be written.  " +
393              "If this is not provided, the encoded data will be written to " +
394              "standard output.",
395         false, true, true, false);
396    encodeOutputFileArgument.addLongIdentifier("toEncodedFile", true);
397    encodeOutputFileArgument.addLongIdentifier("output-file", true);
398    encodeOutputFileArgument.addLongIdentifier("to-encoded-file", true);
399    encodeParser.addArgument(encodeOutputFileArgument);
400
401    final BooleanArgument encodeURLArgument = new BooleanArgument(null,
402         ARG_NAME_URL,
403         "Encode the data with the base64url mechanism rather than the " +
404              "standard base64 mechanism.");
405    encodeParser.addArgument(encodeURLArgument);
406
407    final BooleanArgument encodeIgnoreTrailingEOLArgument = new BooleanArgument(
408         null, ARG_NAME_IGNORE_TRAILING_LINE_BREAK,
409         "Ignore any end-of-line marker that may be present at the end of " +
410              "the data to encode.");
411    encodeIgnoreTrailingEOLArgument.addLongIdentifier(
412         "ignore-trailing-line-break", true);
413    encodeParser.addArgument(encodeIgnoreTrailingEOLArgument);
414
415    encodeParser.addExclusiveArgumentSet(encodeDataArgument,
416         encodeDataFileArgument);
417
418    final LinkedHashMap<String[],String> encodeExamples =
419         new LinkedHashMap<String[],String>(3);
420    encodeExamples.put(
421         new String[]
422         {
423           "encode",
424           "--data", "Hello"
425         },
426         "Base64-encodes the string 'Hello' and writes the result to " +
427              "standard output.");
428    encodeExamples.put(
429         new String[]
430         {
431           "encode",
432           "--inputFile", "raw-data.txt",
433           "--outputFile", "encoded-data.txt",
434         },
435         "Base64-encodes the data contained in the 'raw-data.txt' file and " +
436              "writes the result to the 'encoded-data.txt' file.");
437    encodeExamples.put(
438         new String[]
439         {
440           "encode"
441         },
442         "Base64-encodes data read from standard input and writes the result " +
443              "to standard output.");
444
445    final SubCommand encodeSubCommand = new SubCommand(SUBCOMMAND_NAME_ENCODE,
446         "Base64-encodes raw data.", encodeParser, encodeExamples);
447    parser.addSubCommand(encodeSubCommand);
448
449
450    // Create the subcommand for decoding data.
451    final ArgumentParser decodeParser =
452         new ArgumentParser("decode", "Decodes base64-encoded data.");
453
454    final StringArgument decodeDataArgument = new StringArgument('d',
455         ARG_NAME_DATA, false, 1, "{data}",
456         "The base64-encoded data to be decoded.  If neither the --" +
457              ARG_NAME_DATA + " nor the --" + ARG_NAME_INPUT_FILE +
458              " argument is provided, then the data will be read from " +
459              "standard input.");
460    decodeDataArgument.addLongIdentifier("encodedData", true);
461    decodeDataArgument.addLongIdentifier("encoded-data", true);
462    decodeParser.addArgument(decodeDataArgument);
463
464    final FileArgument decodeDataFileArgument = new FileArgument('f',
465         ARG_NAME_INPUT_FILE, false, 1, null,
466         "The path to a file containing the base64-encoded data to be " +
467              "decoded.  If neither the --" + ARG_NAME_DATA + " nor the --" +
468              ARG_NAME_INPUT_FILE + " argument is provided, then the data " +
469              "will be read from standard input.",
470         true, true, true, false);
471    decodeDataFileArgument.addLongIdentifier("encodedDataFile", true);
472    decodeDataFileArgument.addLongIdentifier("input-file", true);
473    decodeDataFileArgument.addLongIdentifier("encoded-data-file", true);
474    decodeParser.addArgument(decodeDataFileArgument);
475
476    final FileArgument decodeOutputFileArgument = new FileArgument('o',
477         ARG_NAME_OUTPUT_FILE, false, 1, null,
478         "The path to a file to which the decoded data should be written.  " +
479              "If this is not provided, the decoded data will be written to " +
480              "standard output.",
481         false, true, true, false);
482    decodeOutputFileArgument.addLongIdentifier("toRawFile", true);
483    decodeOutputFileArgument.addLongIdentifier("output-file", true);
484    decodeOutputFileArgument.addLongIdentifier("to-raw-file", true);
485    decodeParser.addArgument(decodeOutputFileArgument);
486
487    final BooleanArgument decodeURLArgument = new BooleanArgument(null,
488         ARG_NAME_URL,
489         "Decode the data with the base64url mechanism rather than the " +
490              "standard base64 mechanism.");
491    decodeParser.addArgument(decodeURLArgument);
492
493    final BooleanArgument decodeAddTrailingLineBreak = new BooleanArgument(
494         null, ARG_NAME_ADD_TRAILING_LINE_BREAK,
495         "Add a line break to the end of the decoded data.");
496    decodeAddTrailingLineBreak.addLongIdentifier("add-trailing-line-break",
497         true);
498    decodeParser.addArgument(decodeAddTrailingLineBreak);
499
500    decodeParser.addExclusiveArgumentSet(decodeDataArgument,
501         decodeDataFileArgument);
502
503    final LinkedHashMap<String[],String> decodeExamples =
504         new LinkedHashMap<String[],String>(3);
505    decodeExamples.put(
506         new String[]
507         {
508           "decode",
509           "--data", "SGVsbG8="
510         },
511         "Base64-decodes the string 'SGVsbG8=' and writes the result to " +
512              "standard output.");
513    decodeExamples.put(
514         new String[]
515         {
516           "decode",
517           "--inputFile", "encoded-data.txt",
518           "--outputFile", "decoded-data.txt",
519         },
520         "Base64-decodes the data contained in the 'encoded-data.txt' file " +
521              "and writes the result to the 'raw-data.txt' file.");
522    decodeExamples.put(
523         new String[]
524         {
525           "decode"
526         },
527         "Base64-decodes data read from standard input and writes the result " +
528              "to standard output.");
529
530    final SubCommand decodeSubCommand = new SubCommand(SUBCOMMAND_NAME_DECODE,
531         "Decodes base64-encoded data.", decodeParser, decodeExamples);
532    parser.addSubCommand(decodeSubCommand);
533  }
534
535
536
537  /**
538   * Performs the core set of processing for this tool.
539   *
540   * @return  A result code that indicates whether the processing completed
541   *          successfully.
542   */
543  @Override()
544  public ResultCode doToolProcessing()
545  {
546    // Get the subcommand selected by the user.
547    final SubCommand subCommand = parser.getSelectedSubCommand();
548    if (subCommand == null)
549    {
550      // This should never happen.
551      wrapErr(0, WRAP_COLUMN, "No subcommand was selected.");
552      return ResultCode.PARAM_ERROR;
553    }
554
555
556    // Take the appropriate action based on the selected subcommand.
557    if (subCommand.hasName(SUBCOMMAND_NAME_ENCODE))
558    {
559      return doEncode(subCommand.getArgumentParser());
560    }
561    else
562    {
563      return doDecode(subCommand.getArgumentParser());
564    }
565  }
566
567
568
569  /**
570   * Performs the necessary work for base64 encoding.
571   *
572   * @param  p  The argument parser for the encode subcommand.
573   *
574   * @return  A result code that indicates whether the processing completed
575   *          successfully.
576   */
577  private ResultCode doEncode(final ArgumentParser p)
578  {
579    // Get the data to encode.
580    final ByteStringBuffer rawDataBuffer = new ByteStringBuffer();
581    final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA);
582    if ((dataArg != null) && dataArg.isPresent())
583    {
584      rawDataBuffer.append(dataArg.getValue());
585    }
586    else
587    {
588      try
589      {
590        final InputStream inputStream;
591        final FileArgument inputFileArg =
592             p.getFileArgument(ARG_NAME_INPUT_FILE);
593        if ((inputFileArg != null) && inputFileArg.isPresent())
594        {
595          inputStream = new FileInputStream(inputFileArg.getValue());
596        }
597        else
598        {
599          inputStream = in;
600        }
601
602        final byte[] buffer = new byte[8192];
603        while (true)
604        {
605          final int bytesRead = inputStream.read(buffer);
606          if (bytesRead <= 0)
607          {
608            break;
609          }
610
611          rawDataBuffer.append(buffer, 0, bytesRead);
612        }
613
614        inputStream.close();
615      }
616      catch (final Exception e)
617      {
618        Debug.debugException(e);
619        wrapErr(0, WRAP_COLUMN,
620             "An error occurred while attempting to read the data to encode:  ",
621             StaticUtils.getExceptionMessage(e));
622        return ResultCode.LOCAL_ERROR;
623      }
624    }
625
626
627    // If we should ignore any trailing end-of-line markers, then do that now.
628    final BooleanArgument ignoreEOLArg =
629         p.getBooleanArgument(ARG_NAME_IGNORE_TRAILING_LINE_BREAK);
630    if ((ignoreEOLArg != null) && ignoreEOLArg.isPresent())
631    {
632stripEOLLoop:
633      while (rawDataBuffer.length() > 0)
634      {
635        switch (rawDataBuffer.getBackingArray()[rawDataBuffer.length() - 1])
636        {
637          case '\n':
638          case '\r':
639            rawDataBuffer.delete(rawDataBuffer.length() - 1, 1);
640            break;
641          default:
642            break stripEOLLoop;
643        }
644      }
645    }
646
647
648    // Base64-encode the data.
649    final byte[] rawDataArray = rawDataBuffer.toByteArray();
650    final ByteStringBuffer encodedDataBuffer =
651         new ByteStringBuffer(4 * rawDataBuffer.length() / 3 + 3);
652    final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL);
653    if ((urlArg != null) && urlArg.isPresent())
654    {
655      Base64.urlEncode(rawDataArray, 0, rawDataArray.length, encodedDataBuffer,
656           false);
657    }
658    else
659    {
660      Base64.encode(rawDataArray, encodedDataBuffer);
661    }
662
663
664    // Write the encoded data.
665    final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE);
666    if ((outputFileArg != null) && outputFileArg.isPresent())
667    {
668      try
669      {
670        final FileOutputStream outputStream =
671             new FileOutputStream(outputFileArg.getValue(), false);
672        encodedDataBuffer.write(outputStream);
673        outputStream.write(StaticUtils.EOL_BYTES);
674        outputStream.flush();
675        outputStream.close();
676      }
677      catch (final Exception e)
678      {
679        Debug.debugException(e);
680        wrapErr(0, WRAP_COLUMN,
681             "An error occurred while attempting to write the base64-encoded " +
682                  "data to output file ",
683             outputFileArg.getValue().getAbsolutePath(), ":  ",
684             StaticUtils.getExceptionMessage(e));
685        err("Base64-encoded data:");
686        err(encodedDataBuffer.toString());
687        return ResultCode.LOCAL_ERROR;
688      }
689    }
690    else
691    {
692      out(encodedDataBuffer.toString());
693    }
694
695
696    return ResultCode.SUCCESS;
697  }
698
699
700
701  /**
702   * Performs the necessary work for base64 decoding.
703   *
704   * @param  p  The argument parser for the decode subcommand.
705   *
706   * @return  A result code that indicates whether the processing completed
707   *          successfully.
708   */
709  private ResultCode doDecode(final ArgumentParser p)
710  {
711    // Get the data to decode.  We'll always ignore the following:
712    // - Line breaks
713    // - Blank lines
714    // - Lines that start with an octothorpe (#)
715    //
716    // Unless the --url argument was provided, then we'll also ignore lines that
717    // start with a dash (like those used as start and end markers in a
718    // PEM-encoded certificate).  Since dashes are part of the base64url
719    // alphabet, we can't ignore dashes if the --url argument was provided.
720    final ByteStringBuffer encodedDataBuffer = new ByteStringBuffer();
721    final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL);
722    final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA);
723    if ((dataArg != null) && dataArg.isPresent())
724    {
725      encodedDataBuffer.append(dataArg.getValue());
726    }
727    else
728    {
729      try
730      {
731        final BufferedReader reader;
732        final FileArgument inputFileArg =
733             p.getFileArgument(ARG_NAME_INPUT_FILE);
734        if ((inputFileArg != null) && inputFileArg.isPresent())
735        {
736          reader = new BufferedReader(new FileReader(inputFileArg.getValue()));
737        }
738        else
739        {
740          reader = new BufferedReader(new InputStreamReader(in));
741        }
742
743        while (true)
744        {
745          final String line = reader.readLine();
746          if (line == null)
747          {
748            break;
749          }
750
751          if ((line.length() == 0) || line.startsWith("#"))
752          {
753            continue;
754          }
755
756          if (line.startsWith("-") &&
757              ((urlArg == null) || (! urlArg.isPresent())))
758          {
759            continue;
760          }
761
762          encodedDataBuffer.append(line);
763        }
764
765        reader.close();
766      }
767      catch (final Exception e)
768      {
769        Debug.debugException(e);
770        wrapErr(0, WRAP_COLUMN,
771             "An error occurred while attempting to read the data to decode:  ",
772             StaticUtils.getExceptionMessage(e));
773        return ResultCode.LOCAL_ERROR;
774      }
775    }
776
777
778    // Base64-decode the data.
779    final ByteStringBuffer rawDataBuffer = new
780         ByteStringBuffer(encodedDataBuffer.length());
781    if ((urlArg != null) && urlArg.isPresent())
782    {
783      try
784      {
785        rawDataBuffer.append(Base64.urlDecode(encodedDataBuffer.toString()));
786      }
787      catch (final Exception e)
788      {
789        Debug.debugException(e);
790        wrapErr(0, WRAP_COLUMN,
791             "An error occurred while attempting to base64url-decode the " +
792                  "provided data:  " + StaticUtils.getExceptionMessage(e));
793        return ResultCode.LOCAL_ERROR;
794      }
795    }
796    else
797    {
798      try
799      {
800        rawDataBuffer.append(Base64.decode(encodedDataBuffer.toString()));
801      }
802      catch (final Exception e)
803      {
804        Debug.debugException(e);
805        wrapErr(0, WRAP_COLUMN,
806             "An error occurred while attempting to base64-decode the " +
807                  "provided data:  " + StaticUtils.getExceptionMessage(e));
808        return ResultCode.LOCAL_ERROR;
809      }
810    }
811
812
813    // If we should add a newline, then do that now.
814    final BooleanArgument addEOLArg =
815         p.getBooleanArgument(ARG_NAME_ADD_TRAILING_LINE_BREAK);
816    if ((addEOLArg != null) && addEOLArg.isPresent())
817    {
818      rawDataBuffer.append(StaticUtils.EOL_BYTES);
819    }
820
821
822    // Write the decoded data.
823    final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE);
824    if ((outputFileArg != null) && outputFileArg.isPresent())
825    {
826      try
827      {
828        final FileOutputStream outputStream =
829             new FileOutputStream(outputFileArg.getValue(), false);
830        rawDataBuffer.write(outputStream);
831        outputStream.flush();
832        outputStream.close();
833      }
834      catch (final Exception e)
835      {
836        Debug.debugException(e);
837        wrapErr(0, WRAP_COLUMN,
838             "An error occurred while attempting to write the base64-decoded " +
839                  "data to output file ",
840             outputFileArg.getValue().getAbsolutePath(), ":  ",
841             StaticUtils.getExceptionMessage(e));
842        err("Base64-decoded data:");
843        err(encodedDataBuffer.toString());
844        return ResultCode.LOCAL_ERROR;
845      }
846    }
847    else
848    {
849      final byte[] rawDataArray = rawDataBuffer.toByteArray();
850      getOut().write(rawDataArray, 0, rawDataArray.length);
851      getOut().flush();
852    }
853
854
855    return ResultCode.SUCCESS;
856  }
857
858
859
860  /**
861   * Retrieves a set of information that may be used to generate example usage
862   * information.  Each element in the returned map should consist of a map
863   * between an example set of arguments and a string that describes the
864   * behavior of the tool when invoked with that set of arguments.
865   *
866   * @return  A set of information that may be used to generate example usage
867   *          information.  It may be {@code null} or empty if no example usage
868   *          information is available.
869   */
870  @Override()
871  public LinkedHashMap<String[],String> getExampleUsages()
872  {
873    final LinkedHashMap<String[],String> examples =
874         new LinkedHashMap<String[],String>(2);
875
876    examples.put(
877         new String[]
878         {
879           "encode",
880           "--data", "Hello"
881         },
882         "Base64-encodes the string 'Hello' and writes the result to " +
883              "standard output.");
884
885    examples.put(
886         new String[]
887         {
888           "decode",
889           "--inputFile", "encoded-data.txt",
890           "--outputFile", "decoded-data.txt",
891         },
892         "Base64-decodes the data contained in the 'encoded-data.txt' file " +
893              "and writes the result to the 'raw-data.txt' file.");
894
895    return examples;
896  }
897}