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}