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