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