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