diff --git a/.github/workflows/CI_build.yml b/.github/workflows/CI_build.yml index 37753ad..07d87d0 100644 --- a/.github/workflows/CI_build.yml +++ b/.github/workflows/CI_build.yml @@ -4,19 +4,19 @@ on: [push, pull_request] jobs: build: - runs-on: windows-latest strategy: + fail-fast: false matrix: build_configuration: [Release, Debug] build_platform: [x64, Win32, ARM64] - + steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1 + uses: microsoft/setup-msbuild@v3 - name: MSBuild of plugin dll working-directory: vs.proj\ @@ -24,21 +24,21 @@ jobs: - name: Archive artifacts for x64 if: matrix.build_platform == 'x64' && matrix.build_configuration == 'Release' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: plugin_dll_x64 - path: bin64\mimeTools.dll + name: plugin_dll_x64 + path: bin64\mimeTools.dll - name: Archive artifacts for Win32 if: matrix.build_platform == 'Win32' && matrix.build_configuration == 'Release' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: plugin_dll_x86 - path: bin\mimeTools.dll + name: plugin_dll_x86 + path: bin\mimeTools.dll - name: Archive artifacts for ARM64 if: matrix.build_platform == 'ARM64' && matrix.build_configuration == 'Release' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: plugin_dll_arm64 - path: arm64\mimeTools.dll + name: plugin_dll_arm64 + path: arm64\mimeTools.dll diff --git a/.gitignore b/.gitignore index 77a807e..f3a86b9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,9 @@ vs.proj/.vs/ vs.proj/ARM64/ vs.proj/mimeTools.opensdf vs.proj/mimeTools.exp -vs.proj/mimeTools.lib \ No newline at end of file +vs.proj/mimeTools.lib +vs.proj/mimeTools/ +CppProperties.json +RCa25572 +.vscode +.vs \ No newline at end of file diff --git a/readme.txt b/readme.txt index 4bb5294..1ee4a14 100644 --- a/readme.txt +++ b/readme.txt @@ -1,8 +1,9 @@ MIME plugin for Notepad++ implements several main functionalities defined in MIME (Multipurpose Internet Mail Extensions) : 1. Base64 Encoding/Decoding 2. Quoted-printable Encoding/Decoding -3. URL Encoding/Decoding -4. SAML Decoding (though it's not part of MIME) +3. RFC 2047 encoded-word MIME header decoding +4. URL Encoding/Decoding +5. SAML Encoding/Decoding (HTTP-Redirect binding; though it's not part of MIME) This plugin is under GPL. Don Ho \ No newline at end of file diff --git a/src/Scintilla.h b/src/Scintilla.h index 95fcf9b..178d93a 100644 --- a/src/Scintilla.h +++ b/src/Scintilla.h @@ -15,11 +15,15 @@ extern "C" { #endif -#if defined(_WIN32) -/* Return false on failure: */ -int Scintilla_RegisterClasses(void *hInstance); -int Scintilla_ReleaseResources(void); -#endif +/* +* MIME Tools uses the Scintilla control already owned by Notepad++. +* It does not statically link or initialize Scintilla itself. We don't need the following lines. +*#if defined(_WIN32) +* // Return false on failure: +* int Scintilla_RegisterClasses(void *hInstance); +* int Scintilla_ReleaseResources(void); +*#endif +*/ #ifdef __cplusplus } diff --git a/src/b64.cpp b/src/b64.cpp index 9d9dbde..de7b63d 100644 --- a/src/b64.cpp +++ b/src/b64.cpp @@ -1,213 +1,329 @@ // This file is part of Notepad++ plugin MIME Tools project // Copyright (C)2023 Don HO - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// at your option any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - // Enhance Base64 features, and rewrite Base64 encode/decode implementation // Copyright 2019 by Paul Nankervis +// Copyright 2024 by ExSlam -#include "PluginInterface.h" -#include "mimeTools.h" #include "b64.h" -#include "qp.h" -#include "url.h" -#include "saml.h" - -// Base64 encoding decoding - where 8 bit ascii is re-represented using just 64 ascii characters (plus optional padding '='). -// -// This code includes options to encode to base64 in multiple ways. For example the text lines:- -// -// If you can keep your head when all about you -// Are losing theirs and blaming it on you; -// -// Using "Encode with Unix EOL" would produce a single base64 string with line breaks after each 64 characters:- -// -// SWYgeW91IGNhbiBrZWVwIHlvdXIgaGVhZCB3aGVuIGFsbCBhYm91dCB5b3UNCkFy -// ZSBsb3NpbmcgdGhlaXJzIGFuZCBibGFtaW5nIGl0IG9uIHlvdTs= -// -// That would be decoded using a single base64 decode which ignored whitespace characters (the line breaks). -// -// Alternatively the same lines could be encoded using a "by line" option to encode each line of input as -// its own separate base64 string:- -// -// SWYgeW91IGNhbiBrZWVwIHlvdXIgaGVhZCB3aGVuIGFsbCBhYm91dCB5b3U -// QXJlIGxvc2luZyB0aGVpcnMgYW5kIGJsYW1pbmcgaXQgb24geW91Ow -// -// Each of these output lines could be decoded separately, or multiple lines decoded using "reset on whitespace" -// to cause base64 decoding to restart on each line - - -char base64CharSet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; -int base64CharMap[] = { // base64 values or: -1 for illegal character, -2 to ignore character, and -3 for pad ('=') - -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -1, -1, -2, -1, -1, // & are ignored - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, // is ignored - 52, 53, 54, 55 ,56, 57, 58, 59, 60, 61, -1, -1, -1, -3, -1, -1, // '=' is the pad character - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1 ,-1, - -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, - 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 -}; - -// base64Encode simply converts ascii to base64 with appropriate wrapping and padding. Encoding is done by loading -// three ascii characters at a time into a bitField, and then extracting them as four base64 values. -// returnString is assumed to be large enough to contain the result (which is typically 4 / 3 the input size -// plus line breaks), and the function return is the length of the result -// wrapLength sets the length at which to wrap the encoded test at (not valid with byLineFlag) -// padFlag controls whether the one or two '=' pad characters are included at the end of encoding -// byLineFlag causes each input line to be encoded as a separate base64 string - -int base64Encode(char *resultString, const char *asciiString, size_t asciiStringLength, size_t wrapLength, bool padFlag, bool byLineFlag) + +#include +#include + +namespace { + +constexpr char kBase64Alphabet[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +constexpr char kBase64UrlAlphabet[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +constexpr int kIllegal = -1; +constexpr int kWhitespace = -2; +constexpr int kPadding = -3; + +inline int checkedLength(std::size_t length) +{ + return length > static_cast(INT_MAX) ? -1 : static_cast(length); +} + +inline int decodeValue(unsigned char c, bool urlSafe) +{ + // Preserve the legacy decoder's 7-bit lookup semantics for non-ASCII bytes. + c &= 0x7f; + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+' || (urlSafe && c == '-')) return 62; + if (c == '/' || (urlSafe && c == '_')) return 63; + if (c == '=') return kPadding; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') return kWhitespace; + return kIllegal; +} + +inline std::size_t encodeBlock(char* out, const unsigned char* in, std::size_t length, + const char* alphabet, bool pad) +{ + std::size_t input = 0; + std::size_t output = 0; + + // Hot path: three input bytes become four output bytes with no branches. + while (input + 3 <= length) + { + const std::uint32_t bits = + (static_cast(in[input]) << 16) | + (static_cast(in[input + 1]) << 8) | + static_cast(in[input + 2]); + input += 3; + + out[output++] = alphabet[(bits >> 18) & 0x3f]; + out[output++] = alphabet[(bits >> 12) & 0x3f]; + out[output++] = alphabet[(bits >> 6) & 0x3f]; + out[output++] = alphabet[bits & 0x3f]; + } + + const std::size_t remaining = length - input; + if (remaining == 1) + { + const std::uint32_t bits = static_cast(in[input]) << 16; + out[output++] = alphabet[(bits >> 18) & 0x3f]; + out[output++] = alphabet[(bits >> 12) & 0x3f]; + if (pad) + { + out[output++] = '='; + out[output++] = '='; + } + } + else if (remaining == 2) + { + const std::uint32_t bits = + (static_cast(in[input]) << 16) | + (static_cast(in[input + 1]) << 8); + out[output++] = alphabet[(bits >> 18) & 0x3f]; + out[output++] = alphabet[(bits >> 12) & 0x3f]; + out[output++] = alphabet[(bits >> 6) & 0x3f]; + if (pad) + out[output++] = '='; + } + + return output; +} + +int encodeWrapped(char* resultString, const char* asciiString, std::size_t asciiStringLength, + std::size_t wrapLength, bool padFlag) +{ + const auto* input = reinterpret_cast(asciiString); + std::size_t inputIndex = 0; + std::size_t resultLength = 0; + std::size_t lineLength = 0; + + auto put = [&](char c) { + if (wrapLength > 0 && lineLength >= wrapLength) + { + resultString[resultLength++] = '\n'; + lineLength = 0; + } + resultString[resultLength++] = c; + ++lineLength; + }; + + while (inputIndex + 3 <= asciiStringLength) + { + const std::uint32_t bits = + (static_cast(input[inputIndex]) << 16) | + (static_cast(input[inputIndex + 1]) << 8) | + static_cast(input[inputIndex + 2]); + inputIndex += 3; + + put(kBase64Alphabet[(bits >> 18) & 0x3f]); + put(kBase64Alphabet[(bits >> 12) & 0x3f]); + put(kBase64Alphabet[(bits >> 6) & 0x3f]); + put(kBase64Alphabet[bits & 0x3f]); + } + + const std::size_t remaining = asciiStringLength - inputIndex; + if (remaining == 1) + { + const std::uint32_t bits = static_cast(input[inputIndex]) << 16; + put(kBase64Alphabet[(bits >> 18) & 0x3f]); + put(kBase64Alphabet[(bits >> 12) & 0x3f]); + if (padFlag) + { + put('='); + put('='); + } + } + else if (remaining == 2) + { + const std::uint32_t bits = + (static_cast(input[inputIndex]) << 16) | + (static_cast(input[inputIndex + 1]) << 8); + put(kBase64Alphabet[(bits >> 18) & 0x3f]); + put(kBase64Alphabet[(bits >> 12) & 0x3f]); + put(kBase64Alphabet[(bits >> 6) & 0x3f]); + if (padFlag) + put('='); + } + + return checkedLength(resultLength); +} + +int encodeByLine(char* resultString, const char* asciiString, std::size_t asciiStringLength, + bool padFlag) { - size_t index; // input string index - size_t lineLength = 0; // current line length - int resultLength = 0, // result string length - bitField, // assembled bit field (up to 3 ascii characters at a time) - bitOffset = -1, // offset into bit field (8 bit input: 16, 8, 0 -> 6 bit output: 18, 12, 6, 0) - endOffset, // end offset index value - charValue; // character value - - for (index = 0; index < asciiStringLength; ) - { - bitField = 0; - for (bitOffset = 16; bitOffset >= 0 && index < asciiStringLength; bitOffset -= 8) - { - charValue = (UCHAR)asciiString[index]; - if (byLineFlag && (charValue == '\n' || charValue == '\r')) - { - break; - } - index++; - bitField |= charValue << bitOffset; - } - endOffset = bitOffset + 3; // end indicator - for (bitOffset = 18; bitOffset > endOffset; bitOffset -= 6) - { - if (wrapLength > 0 && lineLength++ >= wrapLength && !byLineFlag) - { - resultString[resultLength++] = '\n'; - lineLength = 1; - } - resultString[resultLength++] = base64CharSet[(bitField >> bitOffset) & 0x3f]; - } - if (byLineFlag) - { - while (index < asciiStringLength && (asciiString[index] == '\n' || asciiString[index] == '\r')) - { - resultString[resultLength++] = asciiString[index++]; - } - } - } - if (padFlag && !byLineFlag) - { - for (; bitOffset >= 0; bitOffset -= 6) - { - if (wrapLength > 0 && lineLength++ >= wrapLength) - { - resultString[resultLength++] = '\n'; - lineLength = 1; - } - resultString[resultLength++] = '='; - } - } - return resultLength; + std::size_t inputIndex = 0; + std::size_t outputIndex = 0; + + while (inputIndex < asciiStringLength) + { + const std::size_t lineStart = inputIndex; + while (inputIndex < asciiStringLength && + asciiString[inputIndex] != '\r' && asciiString[inputIndex] != '\n') + { + ++inputIndex; + } + + outputIndex += encodeBlock( + resultString + outputIndex, + reinterpret_cast(asciiString + lineStart), + inputIndex - lineStart, + kBase64Alphabet, + padFlag); + + // Preserve the exact EOL byte sequence, including CRLF and consecutive blank lines. + while (inputIndex < asciiStringLength && + (asciiString[inputIndex] == '\r' || asciiString[inputIndex] == '\n')) + { + resultString[outputIndex++] = asciiString[inputIndex++]; + } + } + + return checkedLength(outputIndex); } -// base64Decode converts base64 to ascii. But there are choices about what to do with illegal characters or -// malformed strings. In this version there is a strict flag to indicate that the input must be a single -// valid base64 string with no illegal characters, no extra padding, and no short segments. Otherwise -// there is best effort to decode around illegal characters which ARE preserved in the output. -// So "TWFyeQ==.aGFk.YQ.bGl0dGxl.bGFtYg==" decodes to "Mary.had.a.little.lamb" with five seperate -// base64 strings decoded, each separated by the illegal character dot. In strict mode the first dot -// would trigger a fatal error. Some other implementations choose to ignore illegal characters which -// of course has it's own issues. -// The four whitespace characters and are silently ignored unless noWhitespaceFlag -// is set. In this case whitespace is treated similar to illegal characters and base64 decoding operates -// around the white space. So "TWFyeQ== aGFk YQ bGl0dGxl bGFtYg==" would decode as "Mary had a little lamb". -// Decoding is done by loading four base64 characters at a time into a bitField, and then extracting them as -// three ascii characters. -// returnString is assumed to be large enough to contain the result (which could be the same size as the input), -// and the function return is the length of the result, or a negative value in case of an error - -int base64Decode(char *resultString, const char *encodedString, size_t encodedStringLength, bool strictFlag, bool whitespaceReset) +int decodeImpl(char* resultString, const char* encodedString, std::size_t encodedStringLength, + bool strictFlag, bool whitespaceReset, bool urlSafe) { - size_t index; // input string index - - int resultLength = 0, // result string length - bitField, // assembled bit field (up to 3 ascii characters at a time) - bitOffset, // offset into bit field (6 bit intput: 18, 12, 6, 0 -> 8 bit output: 16, 8, 0) - endOffset, // end offset index value - charValue = 0, // character value - charIndex = 0, // character index - padLength = 0; // pad characters seen - - for (index = 0; index < encodedStringLength; ) - { - bitField = 0; - for (bitOffset = 18; bitOffset >= 0 && index < encodedStringLength; ) - { - charValue = (UCHAR)encodedString[index++]; - charIndex = base64CharMap[charValue & 0x7f]; - if (charIndex >= 0) - { - if (padLength > 0 && strictFlag) - { - return -1; // **ERROR** Data after pad character - } - bitField |= charIndex << bitOffset; - bitOffset -= 6; - } - else - { - if (charIndex == -3) // -3 is Pad character '=' - { - padLength++; - if (strictFlag && bitOffset > 6) - { - return -2; // **ERROR** Pad character in wrong place - } - } - else // either -1 for illegal character or -2 for whitespace (ignored) - { - if (charIndex == -1 || whitespaceReset) - { - charIndex = -1; // Remember it as an illegal character for copy below - break; // exit loop to deal with illegal character - } - } - } - } - - if (strictFlag && bitOffset == 12) - { - return -3; // **ERROR** Single symbol block not valid - } - endOffset = bitOffset + 3; // end indicator - - for (bitOffset = 16; bitOffset > endOffset; bitOffset -= 8) - { - resultString[resultLength++] = (bitField >> bitOffset) & 0xff; - } - - if (charIndex == -1) // Was there an illegal character? - { - if (strictFlag) - { - return -4; // **ERROR** Bad character in input string - } - resultString[resultLength++] = (char)charValue; - } - } - return resultLength; -} \ No newline at end of file + std::size_t index = 0; + std::size_t resultLength = 0; + int padLength = 0; + + while (index < encodedStringLength) + { + std::uint32_t bitField = 0; + int bitOffset = 18; + int charValue = 0; + int charIndex = 0; + + while (bitOffset >= 0 && index < encodedStringLength) + { + charValue = static_cast(encodedString[index++]); + charIndex = decodeValue(static_cast(charValue), urlSafe); + + if (charIndex >= 0) + { + if (padLength > 0 && strictFlag) + return -1; // Data after pad character. + bitField |= static_cast(charIndex) << bitOffset; + bitOffset -= 6; + } + else if (charIndex == kPadding) + { + ++padLength; + if (strictFlag && bitOffset > 6) + return -2; // Pad character in wrong place. + } + else if (charIndex == kIllegal || whitespaceReset) + { + charIndex = kIllegal; + break; + } + // Whitespace is otherwise ignored. + } + + if (strictFlag && bitOffset == 12) + return -3; // Single-symbol block is invalid. + + const int endOffset = bitOffset + 3; + for (int outputOffset = 16; outputOffset > endOffset; outputOffset -= 8) + resultString[resultLength++] = static_cast((bitField >> outputOffset) & 0xff); + + if (charIndex == kIllegal) + { + if (strictFlag) + return -4; + resultString[resultLength++] = static_cast(charValue); + } + } + + return checkedLength(resultLength); +} + +} // namespace + +int base64Encode(char* resultString, const char* asciiString, std::size_t asciiStringLength, + std::size_t wrapLength, bool padFlag, bool byLineFlag) +{ + if (byLineFlag) + return encodeByLine(resultString, asciiString, asciiStringLength, false); + + if (wrapLength == 0) + { + const std::size_t resultLength = encodeBlock( + resultString, + reinterpret_cast(asciiString), + asciiStringLength, + kBase64Alphabet, + padFlag); + return checkedLength(resultLength); + } + + return encodeWrapped(resultString, asciiString, asciiStringLength, wrapLength, padFlag); +} + +int base64Decode(char* resultString, const char* encodedString, std::size_t encodedStringLength, + bool strictFlag, bool whitespaceReset) +{ + return decodeImpl(resultString, encodedString, encodedStringLength, + strictFlag, whitespaceReset, false); +} + +int base64EncodeWithPaddingByLine(std::string& resultString, const char* asciiString, + std::size_t asciiStringLength) +{ + // Worst case is 4/3 expansion plus the original line endings. + resultString.clear(); + resultString.reserve(((asciiStringLength + 2) / 3) * 4 + asciiStringLength / 32 + 4); + + std::size_t inputIndex = 0; + char encoded[4]; + + while (inputIndex < asciiStringLength) + { + const std::size_t lineStart = inputIndex; + while (inputIndex < asciiStringLength && + asciiString[inputIndex] != '\r' && asciiString[inputIndex] != '\n') + { + ++inputIndex; + } + + std::size_t lineIndex = lineStart; + const std::size_t lineEnd = inputIndex; + while (lineIndex < lineEnd) + { + const std::size_t chunk = (lineEnd - lineIndex >= 3) ? 3 : (lineEnd - lineIndex); + const std::size_t produced = encodeBlock( + encoded, + reinterpret_cast(asciiString + lineIndex), + chunk, + kBase64Alphabet, + true); + resultString.append(encoded, produced); + lineIndex += chunk; + } + + while (inputIndex < asciiStringLength && + (asciiString[inputIndex] == '\r' || asciiString[inputIndex] == '\n')) + { + resultString.push_back(asciiString[inputIndex++]); + } + } + + return checkedLength(resultString.size()); +} + +int base64UrlEncode(char* resultString, const char* asciiString, std::size_t asciiStringLength) +{ + const std::size_t resultLength = encodeBlock( + resultString, + reinterpret_cast(asciiString), + asciiStringLength, + kBase64UrlAlphabet, + false); + return checkedLength(resultLength); +} + +int base64UrlDecode(char* resultString, const char* encodedString, std::size_t encodedStringLength, + bool strictFlag, bool whitespaceReset) +{ + return decodeImpl(resultString, encodedString, encodedStringLength, + strictFlag, whitespaceReset, true); +} diff --git a/src/b64.h b/src/b64.h index f6a45a9..fd1c180 100644 --- a/src/b64.h +++ b/src/b64.h @@ -1,23 +1,21 @@ // This file is part of Notepad++ plugin MIME Tools project // Copyright (C)2023 Don HO -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// at your option any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - - #pragma once -#include +#include +#include + +int base64Encode(char *resultString, const char *asciiString, std::size_t asciiStringLength, + std::size_t wrapLength, bool padFlag, bool byLineFlag); +int base64Decode(char *resultString, const char *encodedString, std::size_t encodedStringLength, + bool strictFlag, bool whitespaceReset); +int base64EncodeWithPaddingByLine(std::string& resultString, const char* asciiString, + std::size_t asciiStringLength); -int base64Encode(char *resultString, const char *asciiString, size_t asciiStringLength, size_t wrapLength, bool padFlag, bool byLineFlag); -int base64Decode(char *resultString, const char *encodedString, size_t encodedStringLength, bool strictFlag, bool whitespaceReset); +// RFC 4648 base64url helpers. Encoding is unpadded and uses '-'/'_' directly. +// Decoding accepts both URL-safe and standard Base64 alphabet symbols, matching +// the previous MIME Tools Base64URL wrapper's permissive behavior. +int base64UrlEncode(char *resultString, const char *asciiString, std::size_t asciiStringLength); +int base64UrlDecode(char *resultString, const char *encodedString, std::size_t encodedStringLength, + bool strictFlag, bool whitespaceReset); diff --git a/src/mimeTools.cpp b/src/mimeTools.cpp index faf6dda..f17875d 100644 --- a/src/mimeTools.cpp +++ b/src/mimeTools.cpp @@ -1,4 +1,4 @@ -// This file is part of Notepad++ plugin MIME Tools project +// This file is part of Notepad++ plugin MIME Tools project // Copyright (C)2023 Don HO // This program is free software: you can redistribute it and/or modify @@ -17,18 +17,21 @@ // Enhance Base64 features, and rewrite Base64 encode/decode implementation // Copyright 2019 by Paul Nankervis +#include +#include + #include "PluginInterface.h" #include "mimeTools.h" #include "b64.h" #include "qp.h" #include "url.h" #include "saml.h" - +#include "rfc2047.h" const TCHAR PLUGIN_NAME[] = TEXT("MIME Tools"); -const int nbFunc = 22; +const int nbFunc = 31; -HINSTANCE g_hInst = nullptr;; +HINSTANCE g_hInst = nullptr; NppData nppData; FuncItem funcItem[nbFunc]; HWND g_hAboutDlg = nullptr; @@ -40,102 +43,74 @@ BOOL APIENTRY DllMain(HANDLE hModule, DWORD reasonForCall, LPVOID /*lpReserved*/ case DLL_PROCESS_ATTACH: { g_hInst = (HINSTANCE)hModule; + funcItem[0]._pFunc = convertToBase64FromAscii; funcItem[1]._pFunc = convertToBase64FromAscii_pad; - funcItem[2]._pFunc = convertToBase64FromAscii_B64Format; - funcItem[3]._pFunc = convertToBase64FromAscii_byline; - funcItem[4]._pFunc = convertToAsciiFromBase64; - funcItem[5]._pFunc = convertToAsciiFromBase64_strict; - funcItem[6]._pFunc = convertToAsciiFromBase64_whitespaceReset; - - funcItem[7]._pFunc = NULL; - funcItem[8]._pFunc = convertToQuotedPrintable; - funcItem[9]._pFunc = convertToAsciiFromQuotedPrintable; - + funcItem[2]._pFunc = convertToBase64FromAscii_pad_byline; + funcItem[3]._pFunc = convertToBase64FromAscii_B64Format; + funcItem[4]._pFunc = convertToBase64FromAscii_byline; + funcItem[5]._pFunc = convertToAsciiFromBase64; + funcItem[6]._pFunc = convertToAsciiFromBase64_strict; + funcItem[7]._pFunc = convertToAsciiFromBase64_whitespaceReset; + funcItem[8]._pFunc = convertToBase64FromUtf16Le; + funcItem[9]._pFunc = convertToUtf16LeFromBase64; funcItem[10]._pFunc = NULL; - funcItem[11]._pFunc = convertURLMinEncode; - funcItem[12]._pFunc = convertURLMinEncodeByLine; - funcItem[13]._pFunc = convertURLEncodeExtended; - funcItem[14]._pFunc = convertURLEncodeExtendedByLine; - funcItem[15]._pFunc = convertURLFullEncode; - funcItem[16]._pFunc = convertURLFullEncodeByLine; - funcItem[17]._pFunc = convertURLDecode; - - funcItem[18]._pFunc = NULL; - funcItem[19]._pFunc = convertSamlDecode; - - funcItem[20]._pFunc = NULL; - funcItem[21]._pFunc = about; - + funcItem[11]._pFunc = convertToQuotedPrintable; + funcItem[12]._pFunc = convertToAsciiFromQuotedPrintable; + funcItem[13]._pFunc = NULL; + funcItem[14]._pFunc = convertMimeHeaderDecode; + funcItem[15]._pFunc = NULL; + funcItem[16]._pFunc = convertURLMinEncode; + funcItem[17]._pFunc = convertURLMinEncodeByLine; + funcItem[18]._pFunc = convertURLEncodeExtended; + funcItem[19]._pFunc = convertURLEncodeExtendedByLine; + funcItem[20]._pFunc = convertURLFullEncode; + funcItem[21]._pFunc = convertURLFullEncodeByLine; + funcItem[22]._pFunc = convertURLDecode; + funcItem[23]._pFunc = NULL; + funcItem[24]._pFunc = urlconvertToBase64FromAscii; + funcItem[25]._pFunc = urlconvertToAsciiFromBase64; + funcItem[26]._pFunc = NULL; + funcItem[27]._pFunc = convertSamlEncode; + funcItem[28]._pFunc = convertSamlDecode; + funcItem[29]._pFunc = NULL; + funcItem[30]._pFunc = about; lstrcpy(funcItem[0]._itemName, TEXT("Base64 Encode")); lstrcpy(funcItem[1]._itemName, TEXT("Base64 Encode with padding")); - lstrcpy(funcItem[2]._itemName, TEXT("Base64 Encode with Unix EOL")); - lstrcpy(funcItem[3]._itemName, TEXT("Base64 Encode by line")); - lstrcpy(funcItem[4]._itemName, TEXT("Base64 Decode")); - lstrcpy(funcItem[5]._itemName, TEXT("Base64 Decode strict")); - lstrcpy(funcItem[6]._itemName, TEXT("Base64 Decode by line")); - - lstrcpy(funcItem[7]._itemName, TEXT("-SEPARATOR-")); - - lstrcpy(funcItem[8]._itemName, TEXT("Quoted-printable Encode")); - lstrcpy(funcItem[9]._itemName, TEXT("Quoted-printable Decode")); - + lstrcpy(funcItem[2]._itemName, TEXT("Base64 Encode with padding by line")); + lstrcpy(funcItem[3]._itemName, TEXT("Base64 Encode with Unix EOL")); + lstrcpy(funcItem[4]._itemName, TEXT("Base64 Encode by line")); + lstrcpy(funcItem[5]._itemName, TEXT("Base64 Decode")); + lstrcpy(funcItem[6]._itemName, TEXT("Base64 Decode strict")); + lstrcpy(funcItem[7]._itemName, TEXT("Base64 Decode by line (padded/unpadded)")); + lstrcpy(funcItem[8]._itemName, TEXT("Base64 Encode UTF-16LE")); + lstrcpy(funcItem[9]._itemName, TEXT("Base64 Decode UTF-16LE")); lstrcpy(funcItem[10]._itemName, TEXT("-SEPARATOR-")); - - lstrcpy(funcItem[11]._itemName, TEXT("URL Encode (RFC1738)")); - lstrcpy(funcItem[12]._itemName, TEXT("URL Encode (RFC1738) by line")); - lstrcpy(funcItem[13]._itemName, TEXT("URL Encode (Extended)")); - lstrcpy(funcItem[14]._itemName, TEXT("URL Encode (Extended) by line")); - lstrcpy(funcItem[15]._itemName, TEXT("URL Encode (Full)")); - lstrcpy(funcItem[16]._itemName, TEXT("URL Encode (Full) by line")); - lstrcpy(funcItem[17]._itemName, TEXT("URL Decode")); - - lstrcpy(funcItem[18]._itemName, TEXT("-SEPARATOR-")); - - lstrcpy(funcItem[19]._itemName, TEXT("SAML Decode")); - - lstrcpy(funcItem[20]._itemName, TEXT("-SEPARATOR-")); - - lstrcpy(funcItem[21]._itemName, TEXT("About")); - - funcItem[0]._init2Check = false; - funcItem[1]._init2Check = false; - funcItem[2]._init2Check = false; - funcItem[3]._init2Check = false; - funcItem[4]._init2Check = false; - funcItem[5]._init2Check = false; - funcItem[6]._init2Check = false; - funcItem[7]._init2Check = false; - funcItem[8]._init2Check = false; - funcItem[9]._init2Check = false; - funcItem[10]._init2Check = false; - funcItem[11]._init2Check = false; - funcItem[12]._init2Check = false; - funcItem[13]._init2Check = false; - funcItem[14]._init2Check = false; - funcItem[15]._init2Check = false; - funcItem[16]._init2Check = false; - funcItem[17]._init2Check = false; - - // If you don't need the shortcut, you have to make it NULL - funcItem[0]._pShKey = NULL; - funcItem[1]._pShKey = NULL; - funcItem[2]._pShKey = NULL; - funcItem[3]._pShKey = NULL; - funcItem[4]._pShKey = NULL; - funcItem[5]._pShKey = NULL; - funcItem[6]._pShKey = NULL; - funcItem[7]._pShKey = NULL; - funcItem[8]._pShKey = NULL; - funcItem[9]._pShKey = NULL; - funcItem[10]._pShKey = NULL; - funcItem[11]._pShKey = NULL; - funcItem[12]._pShKey = NULL; - funcItem[13]._pShKey = NULL; - funcItem[14]._pShKey = NULL; - funcItem[15]._pShKey = NULL; - funcItem[16]._pShKey = NULL; - funcItem[17]._pShKey = NULL; + lstrcpy(funcItem[11]._itemName, TEXT("Quoted-printable Encode")); + lstrcpy(funcItem[12]._itemName, TEXT("Quoted-printable Decode")); + lstrcpy(funcItem[13]._itemName, TEXT("-SEPARATOR-")); + lstrcpy(funcItem[14]._itemName, TEXT("MIME Header Decode (RFC 2047)")); + lstrcpy(funcItem[15]._itemName, TEXT("-SEPARATOR-")); + lstrcpy(funcItem[16]._itemName, TEXT("URL Encode (RFC1738)")); + lstrcpy(funcItem[17]._itemName, TEXT("URL Encode (RFC1738) by line")); + lstrcpy(funcItem[18]._itemName, TEXT("URL Encode (Extended)")); + lstrcpy(funcItem[19]._itemName, TEXT("URL Encode (Extended) by line")); + lstrcpy(funcItem[20]._itemName, TEXT("URL Encode (Full)")); + lstrcpy(funcItem[21]._itemName, TEXT("URL Encode (Full) by line")); + lstrcpy(funcItem[22]._itemName, TEXT("URL Decode")); + lstrcpy(funcItem[23]._itemName, TEXT("-SEPARATOR-")); + lstrcpy(funcItem[24]._itemName, TEXT("URL Base64 Encode")); + lstrcpy(funcItem[25]._itemName, TEXT("URL Base64 Decode")); + lstrcpy(funcItem[26]._itemName, TEXT("-SEPARATOR-")); + lstrcpy(funcItem[27]._itemName, TEXT("SAML Encode")); + lstrcpy(funcItem[28]._itemName, TEXT("SAML Decode")); + lstrcpy(funcItem[29]._itemName, TEXT("-SEPARATOR-")); + lstrcpy(funcItem[30]._itemName, TEXT("About")); + for (int i = 0; i < nbFunc; i++) + { + funcItem[i]._init2Check = false; + funcItem[i]._pShKey = NULL; + } } break; @@ -207,50 +182,509 @@ HWND getCurrentScintillaHandle() return (currentEdit == 0)?nppData._scintillaMainHandle:nppData._scintillaSecondHandle; }; +namespace +{ + void showUtf16Base64Error(const TCHAR* message) + { + ::MessageBox(nppData._nppHandle, message, TEXT("Base64 UTF-16LE"), MB_OK | MB_ICONERROR); + } + + UINT getDocumentCodePage(HWND hScintilla) + { + const UINT codePage = static_cast( + ::SendMessage(hScintilla, SCI_GETCODEPAGE, 0, 0)); + return codePage == 0 ? CP_ACP : codePage; + } + + bool getSingleSelection(HWND hScintilla, size_t& start, size_t& end, std::vector& selectedText) + { + const size_t nbSelections = static_cast( + ::SendMessage(hScintilla, SCI_GETSELECTIONS, 0, 0)); + if (nbSelections > 1) + return false; + + start = static_cast(::SendMessage(hScintilla, SCI_GETSELECTIONSTART, 0, 0)); + end = static_cast(::SendMessage(hScintilla, SCI_GETSELECTIONEND, 0, 0)); + if (end < start) + { + const size_t tmp = start; + start = end; + end = tmp; + } + + const size_t selectedLength = end - start; + if (selectedLength == 0) + return false; + + selectedText.resize(selectedLength + 1); + ::SendMessage(hScintilla, SCI_SETTARGETSTART, start, 0); + ::SendMessage(hScintilla, SCI_SETTARGETEND, end, 0); + ::SendMessage(hScintilla, SCI_GETTARGETTEXT, 0, + reinterpret_cast(selectedText.data())); + return true; + } + + bool documentBytesToUtf16(HWND hScintilla, const char* text, size_t length, + std::vector& utf16) + { + if (length > static_cast(INT_MAX)) + return false; + if (length == 0) + { + utf16.clear(); + return true; + } + + const UINT codePage = getDocumentCodePage(hScintilla); + const DWORD flags = codePage == CP_UTF8 ? MB_ERR_INVALID_CHARS : 0; + const int inputLength = static_cast(length); + const int utf16Length = ::MultiByteToWideChar( + codePage, flags, text, inputLength, nullptr, 0); + if (utf16Length <= 0) + return false; + + utf16.resize(static_cast(utf16Length)); + return ::MultiByteToWideChar( + codePage, flags, text, inputLength, utf16.data(), utf16Length) == utf16Length; + } + + bool isValidUtf16(const wchar_t* text, size_t length) + { + for (size_t i = 0; i < length; ++i) + { + const unsigned int unit = static_cast(text[i]); + if (unit >= 0xD800 && unit <= 0xDBFF) + { + if (i + 1 >= length) + return false; + const unsigned int next = static_cast(text[++i]); + if (next < 0xDC00 || next > 0xDFFF) + return false; + } + else if (unit >= 0xDC00 && unit <= 0xDFFF) + { + return false; + } + } + return true; + } + + bool utf16ToDocumentBytes(HWND hScintilla, const wchar_t* text, size_t length, + std::vector& output) + { + if (length > static_cast(INT_MAX)) + return false; + if (length == 0) + { + output.clear(); + return true; + } + + const UINT codePage = getDocumentCodePage(hScintilla); + DWORD flags = 0; + BOOL usedDefaultChar = FALSE; + LPBOOL usedDefaultCharPtr = nullptr; + + if (codePage == CP_UTF8) + { + flags = WC_ERR_INVALID_CHARS; + } + else if (codePage != CP_UTF7 && codePage != 54936) + { + flags = WC_NO_BEST_FIT_CHARS; + usedDefaultCharPtr = &usedDefaultChar; + } + + const int inputLength = static_cast(length); + const int outputLength = ::WideCharToMultiByte( + codePage, flags, text, inputLength, nullptr, 0, nullptr, usedDefaultCharPtr); + if (outputLength <= 0 || usedDefaultChar) + return false; + + output.resize(static_cast(outputLength)); + usedDefaultChar = FALSE; + const int convertedLength = ::WideCharToMultiByte( + codePage, flags, text, inputLength, output.data(), outputLength, + nullptr, usedDefaultCharPtr); + return convertedLength == outputLength && !usedDefaultChar; + } + + void replaceSelection(HWND hScintilla, size_t start, size_t end, + const char* text, size_t length) + { + ::SendMessage(hScintilla, SCI_SETTARGETSTART, start, 0); + ::SendMessage(hScintilla, SCI_SETTARGETEND, end, 0); + ::SendMessage(hScintilla, SCI_REPLACETARGET, static_cast(length), + reinterpret_cast(text)); + ::SendMessage(hScintilla, SCI_SETSEL, start, start + length); + } +} + +void convertToBase64FromUtf16Le() +{ + static_assert(sizeof(wchar_t) == 2, "UTF-16LE Base64 requires 16-bit wchar_t on Windows."); + + HWND hCurrScintilla = getCurrentScintillaHandle(); + size_t start = 0; + size_t end = 0; + std::vector selectedText; + if (!getSingleSelection(hCurrScintilla, start, end, selectedText)) + return; + + const size_t selectedLength = end - start; + std::vector utf16; + if (!documentBytesToUtf16(hCurrScintilla, selectedText.data(), selectedLength, utf16)) + { + showUtf16Base64Error(TEXT("The selected text cannot be converted to UTF-16 without decoding errors.")); + return; + } + + if (utf16.size() > static_cast(INT_MAX) / sizeof(wchar_t)) + { + showUtf16Base64Error(TEXT("The selected text is too large to Base64 encode as UTF-16LE.")); + return; + } + const size_t utf16ByteLength = utf16.size() * sizeof(wchar_t); + const size_t fullGroups = utf16ByteLength / 3; + const size_t tailLength = (utf16ByteLength % 3) == 0 ? 0 : 4; + if (fullGroups > (static_cast(INT_MAX) - tailLength) / 4) + { + showUtf16Base64Error(TEXT("The selected text is too large to Base64 encode as UTF-16LE.")); + return; + } + const size_t encodedLength = fullGroups * 4 + tailLength; + std::vector encodedText(encodedLength); + + const int len = base64Encode( + encodedText.data(), + reinterpret_cast(utf16.data()), + utf16ByteLength, + 0, + true, + false); + if (len < 0 || static_cast(len) != encodedLength) + { + showUtf16Base64Error(TEXT("Base64 UTF-16LE encoding failed.")); + return; + } + + replaceSelection(hCurrScintilla, start, end, encodedText.data(), encodedLength); +} + +void convertToUtf16LeFromBase64() +{ + static_assert(sizeof(wchar_t) == 2, "UTF-16LE Base64 requires 16-bit wchar_t on Windows."); + + HWND hCurrScintilla = getCurrentScintillaHandle(); + size_t start = 0; + size_t end = 0; + std::vector selectedText; + if (!getSingleSelection(hCurrScintilla, start, end, selectedText)) + return; + + const size_t selectedLength = end - start; + if (selectedLength > static_cast(INT_MAX)) + { + showUtf16Base64Error(TEXT("The selected Base64 text is too large to decode.")); + return; + } + + // Base64 decoding never produces more bytes than its input. Allocate aligned + // UTF-16 storage directly so no decoded-byte -> wchar_t copy is required. + std::vector utf16((selectedLength + sizeof(wchar_t) - 1) / sizeof(wchar_t)); + const int decodedByteLength = base64Decode( + reinterpret_cast(utf16.data()), + selectedText.data(), + selectedLength, + true, + false); + if (decodedByteLength < 0) + { + showUtf16Base64Error(TEXT("The selection is not valid Base64.")); + return; + } + if ((decodedByteLength % static_cast(sizeof(wchar_t))) != 0) + { + showUtf16Base64Error(TEXT("Decoded Base64 has an odd byte count and is not valid UTF-16LE.")); + return; + } + + size_t utf16Length = static_cast(decodedByteLength) / sizeof(wchar_t); + size_t offset = 0; + if (utf16Length > 0) + { + if (utf16[0] == static_cast(0xFEFF)) + { + offset = 1; // Optional UTF-16LE BOM. + } + else if (utf16[0] == static_cast(0xFFFE)) + { + showUtf16Base64Error(TEXT("Decoded data has a UTF-16BE BOM. This command expects UTF-16LE.")); + return; + } + } + + const wchar_t* utf16Text = utf16.data() + offset; + utf16Length -= offset; + if (!isValidUtf16(utf16Text, utf16Length)) + { + showUtf16Base64Error(TEXT("Decoded Base64 contains malformed UTF-16 surrogate pairs.")); + return; + } + std::vector documentText; + if (!utf16ToDocumentBytes(hCurrScintilla, utf16Text, utf16Length, documentText)) + { + showUtf16Base64Error(TEXT("Decoded UTF-16LE text cannot be represented in the current document encoding.")); + return; + } + + replaceSelection(hCurrScintilla, start, end, + documentText.empty() ? "" : documentText.data(), documentText.size()); +} void convertAsciiToBase64(size_t wrapLength, bool padFlag, bool byLineFlag) { HWND hCurrScintilla = getCurrentScintillaHandle(); - size_t nbSelections = ::SendMessage(hCurrScintilla, SCI_GETSELECTIONS, 0, 0); - if (nbSelections > 1) return; - size_t selectedLength = ::SendMessage(hCurrScintilla, SCI_GETSELTEXT, 0, 0); - if (selectedLength == 0) return; + size_t nbSelections = + ::SendMessage(hCurrScintilla, SCI_GETSELECTIONS, 0, 0); + + if (nbSelections > 1) + return; + + size_t selectedLength = + ::SendMessage(hCurrScintilla, SCI_GETSELTEXT, 0, 0); + + if (selectedLength == 0) + return; + + std::vector selectedText(selectedLength + 1); - char *selectedText = new char[selectedLength + 1]; ::SendMessage(hCurrScintilla, SCI_TARGETFROMSELECTION, 0, 0); - ::SendMessage(hCurrScintilla, SCI_GETTARGETTEXT, 0, (LPARAM)selectedText); + ::SendMessage( + hCurrScintilla, + SCI_GETTARGETTEXT, + 0, + reinterpret_cast(selectedText.data()) + ); + + /* + * Padded-per-line already uses the std::string implementation. + * Do not allocate/copy another temporary output buffer. + */ + if (padFlag && byLineFlag) + { + std::string encodedString; + + int len = base64EncodeWithPaddingByLine( + encodedString, + selectedText.data(), + selectedLength + ); + + if (len < 0) + { + ::MessageBox( + nppData._nppHandle, + TEXT("Input is too large to encode."), + TEXT("Base64"), + MB_OK | MB_ICONERROR + ); + return; + } + + ::SendMessage(hCurrScintilla, SCI_TARGETFROMSELECTION, 0, 0); + ::SendMessage( + hCurrScintilla, + SCI_REPLACETARGET, + len, + reinterpret_cast(encodedString.data()) + ); + + return; + } + + size_t bufferLength; - size_t bufferLength = (selectedLength + 2) / 3 * 4 + 1; - if (wrapLength > 0) + if (byLineFlag) { - bufferLength += bufferLength / wrapLength; + /* + * Unpadded Base64 encoding of a one-byte line produces + * two output bytes. CR/LF bytes are copied unchanged. + * + * Therefore 2 * input length is a safe upper bound. + */ + bufferLength = selectedLength * 2 + 1; } - char *encodedText = new char[bufferLength + 1]; + else + { + /* + * Standard padded Base64 upper bound. + */ + const size_t baseLength = + ((selectedLength + 2) / 3) * 4; - int len = base64Encode(encodedText, selectedText, selectedLength, wrapLength, padFlag, byLineFlag); - encodedText[len] = '\0'; - - ::SendMessage(hCurrScintilla, SCI_TARGETFROMSELECTION, 0, 0); - ::SendMessage(hCurrScintilla, SCI_REPLACETARGET, len, (LPARAM)encodedText); + size_t lineBreaks = 0; - delete[] selectedText; - delete[] encodedText; + if (wrapLength > 0 && baseLength > 0) + lineBreaks = (baseLength - 1) / wrapLength; + + bufferLength = + baseLength + + lineBreaks + + 1; + } + + std::vector encodedText(bufferLength); + int len = base64Encode( + encodedText.data(), + selectedText.data(), + selectedLength, + wrapLength, + padFlag, + byLineFlag + ); + + if (len < 0 || + static_cast(len) > encodedText.size()) + { + ::MessageBox( + nppData._nppHandle, + TEXT("Base64 output exceeded the allocated buffer."), + TEXT("Base64"), + MB_OK | MB_ICONERROR + ); + return; + } + + ::SendMessage(hCurrScintilla, SCI_TARGETFROMSELECTION, 0, 0); + ::SendMessage( + hCurrScintilla, + SCI_REPLACETARGET, + len, + reinterpret_cast(encodedText.data()) + ); } +void urlconvertAsciiToBase64(size_t /*wrapLength*/, bool /*padFlag*/, bool /*byLineFlag*/) +{ + HWND hCurrScintilla = getCurrentScintillaHandle(); + + const size_t nbSelections = + ::SendMessage(hCurrScintilla, SCI_GETSELECTIONS, 0, 0); + + if (nbSelections > 1) + return; + + const size_t selectedLength = + ::SendMessage(hCurrScintilla, SCI_GETSELTEXT, 0, 0); + + if (selectedLength == 0) + return; + + // SCI_GETSELTEXT appends a terminating NUL, so allocate one extra byte. + std::vector selectedText(selectedLength + 1); + + ::SendMessage( + hCurrScintilla, + SCI_GETSELTEXT, + 0, + reinterpret_cast(selectedText.data()) + ); + + /* + * Unpadded Base64URL output length: + * + * 3 input bytes -> 4 output bytes + * 1 remaining byte -> 2 output bytes + * 2 remaining bytes -> 3 output bytes + */ + const size_t fullGroups = selectedLength / 3; + const size_t remainder = selectedLength % 3; + const size_t tailLength = + remainder == 0 ? 0 : remainder + 1; + + /* + * base64UrlEncode() returns int, so reject an input whose encoded + * result cannot be represented by that API. + * + * This check is performed before multiplying by 4, so the size_t + * calculation cannot overflow. + */ + if (fullGroups > + (static_cast(INT_MAX) - tailLength) / 4) + { + ::MessageBox( + nppData._nppHandle, + TEXT("The selected text is too large to Base64URL encode."), + TEXT("Base64URL"), + MB_OK | MB_ICONERROR + ); + + return; + } + + const size_t encodedLength = + fullGroups * 4 + tailLength; + + std::vector encodedText(encodedLength); + + const int len = base64UrlEncode( + encodedText.data(), + selectedText.data(), + selectedLength + ); + + if (len < 0 || + static_cast(len) != encodedLength) + { + ::MessageBox( + nppData._nppHandle, + TEXT("Base64URL encoding failed."), + TEXT("Base64URL"), + MB_OK | MB_ICONERROR + ); + + return; + } + + ::SendMessage( + hCurrScintilla, + SCI_TARGETFROMSELECTION, + 0, + 0 + ); + + ::SendMessage( + hCurrScintilla, + SCI_REPLACETARGET, + static_cast(len), + reinterpret_cast(encodedText.data()) + ); +} void convertToBase64FromAscii() { convertAsciiToBase64(0, false, false); } +void urlconvertToBase64FromAscii() +{ + urlconvertAsciiToBase64(0, false, false); +} + void convertToBase64FromAscii_pad() { convertAsciiToBase64(0, true, false); } +void convertToBase64FromAscii_pad_byline() +{ + convertAsciiToBase64(0, true, true); +} + void convertToBase64FromAscii_B64Format() { convertAsciiToBase64(64, true, false); @@ -263,35 +697,128 @@ void convertToBase64FromAscii_byline() void convertBase64ToAscii(bool strictFlag, bool whitespaceReset) +{ + HWND hCurrScintilla = getCurrentScintillaHandle(); + + size_t nbSelections = ::SendMessage( + hCurrScintilla, + SCI_GETSELECTIONS, + 0, + 0 + ); + + if (nbSelections > 1) + return; + + size_t selectedLength = ::SendMessage( + hCurrScintilla, + SCI_GETSELTEXT, + 0, + 0 + ); + + if (selectedLength == 0) + return; + + std::vector selectedText(selectedLength + 1); + std::vector decodedText(selectedLength + 1); + + ::SendMessage( + hCurrScintilla, + SCI_TARGETFROMSELECTION, + 0, + 0 + ); + + ::SendMessage( + hCurrScintilla, + SCI_GETTARGETTEXT, + 0, + reinterpret_cast(selectedText.data()) + ); + + int len = base64Decode( + decodedText.data(), + selectedText.data(), + selectedLength, + strictFlag, + whitespaceReset + ); + + if (len < 0) + { + ::MessageBox( + nppData._nppHandle, + TEXT("Problem!"), + TEXT("Base64"), + MB_OK | MB_ICONERROR + ); + + return; + } + + ::SendMessage( + hCurrScintilla, + SCI_TARGETFROMSELECTION, + 0, + 0 + ); + + ::SendMessage( + hCurrScintilla, + SCI_REPLACETARGET, + len, + reinterpret_cast(decodedText.data()) + ); +} + + +void urlconvertBase64ToAscii(bool strictFlag, bool whitespaceReset) { HWND hCurrScintilla = getCurrentScintillaHandle(); size_t nbSelections = ::SendMessage(hCurrScintilla, SCI_GETSELECTIONS, 0, 0); if (nbSelections > 1) return; + size_t selectedLength = ::SendMessage(hCurrScintilla, SCI_GETSELTEXT, 0, 0); if (selectedLength == 0) return; - char *selectedText = new char[selectedLength + 1]; - ::SendMessage(hCurrScintilla, SCI_TARGETFROMSELECTION, 0, 0); - ::SendMessage(hCurrScintilla, SCI_GETTARGETTEXT, 0, (LPARAM)selectedText); - - char *decodedText = new char[selectedLength]; + std::vector selectedText(selectedLength + 1); + std::vector decodedText(selectedLength + 1); - int len = base64Decode(decodedText, selectedText, selectedLength, strictFlag, whitespaceReset); + ::SendMessage(hCurrScintilla, SCI_TARGETFROMSELECTION, 0, 0); + ::SendMessage( + hCurrScintilla, + SCI_GETTARGETTEXT, + 0, + reinterpret_cast(selectedText.data()) + ); + + int len = base64UrlDecode( + decodedText.data(), + selectedText.data(), + selectedLength, + strictFlag, + whitespaceReset + ); if (len < 0) { - ::MessageBox(nppData._nppHandle, TEXT("Problem!"), TEXT("Base64"), MB_OK); - } - else - { - decodedText[len] = '\0'; - ::SendMessage(hCurrScintilla, SCI_TARGETFROMSELECTION, 0, 0); - ::SendMessage(hCurrScintilla, SCI_REPLACETARGET, len, (LPARAM)decodedText); + ::MessageBox( + nppData._nppHandle, + TEXT("Problem!"), + TEXT("Base64URL"), + MB_OK + ); + return; } - delete[] selectedText; - delete[] decodedText; - + ::SendMessage(hCurrScintilla, SCI_TARGETFROMSELECTION, 0, 0); + ::SendMessage( + hCurrScintilla, + SCI_REPLACETARGET, + len, + reinterpret_cast(decodedText.data()) + ); } void convertToAsciiFromBase64() @@ -299,6 +826,11 @@ void convertToAsciiFromBase64() convertBase64ToAscii(false, false); } +void urlconvertToAsciiFromBase64() +{ + urlconvertBase64ToAscii(false, false); +} + void convertToAsciiFromBase64_strict() { convertBase64ToAscii(true, false); @@ -309,6 +841,60 @@ void convertToAsciiFromBase64_whitespaceReset() convertBase64ToAscii(false, true); } + +void convertMimeHeaderDecode() +{ + HWND hCurrScintilla = getCurrentScintillaHandle(); + const size_t nbSelections = ::SendMessage(hCurrScintilla, SCI_GETSELECTIONS, 0, 0); + if (nbSelections > 1) return; + + size_t start = ::SendMessage(hCurrScintilla, SCI_GETSELECTIONSTART, 0, 0); + size_t end = ::SendMessage(hCurrScintilla, SCI_GETSELECTIONEND, 0, 0); + if (end < start) + { + const size_t tmp = start; + start = end; + end = tmp; + } + const size_t selectedLength = end - start; + if (selectedLength == 0) return; + + char* selectedText = new char[selectedLength + 1]; + ::SendMessage(hCurrScintilla, SCI_SETTARGETSTART, start, 0); + ::SendMessage(hCurrScintilla, SCI_SETTARGETEND, end, 0); + ::SendMessage(hCurrScintilla, SCI_GETTARGETTEXT, 0, reinterpret_cast(selectedText)); + + std::string decodedText; + const unsigned int documentCodePage = static_cast( + ::SendMessage(hCurrScintilla, SCI_GETCODEPAGE, 0, 0)); + const Rfc2047DecodeResult result = decodeRfc2047Header( + selectedText, selectedLength, documentCodePage, decodedText); + + if (result.decodedWords == 0) + { + ::MessageBox(nppData._nppHandle, + TEXT("No valid RFC 2047 encoded-words were found in the selection."), + TEXT("MIME Header Decode"), MB_OK | MB_ICONINFORMATION); + } + else + { + ::SendMessage(hCurrScintilla, SCI_SETTARGETSTART, start, 0); + ::SendMessage(hCurrScintilla, SCI_SETTARGETEND, end, 0); + ::SendMessage(hCurrScintilla, SCI_REPLACETARGET, decodedText.size(), + reinterpret_cast(decodedText.data())); + ::SendMessage(hCurrScintilla, SCI_SETSEL, start, start + decodedText.size()); + + if (result.skippedWords != 0) + { + ::MessageBox(nppData._nppHandle, + TEXT("Some malformed, unsupported, or unrepresentable encoded-words were left unchanged."), + TEXT("MIME Header Decode"), MB_OK | MB_ICONWARNING); + } + } + + delete[] selectedText; +} + void convertURLMinEncode() { convertURLEncode (UrlEncodeMethod::RFC1738); @@ -512,6 +1098,65 @@ void about() ::SetWindowPos(g_hAboutDlg, HWND_TOP, x, y, (dlgRect.right - dlgRect.left), (dlgRect.bottom - dlgRect.top), SWP_SHOWWINDOW); } +void convertSamlEncode() +{ + HWND hCurrScintilla = getCurrentScintillaHandle(); + size_t nbSelections = ::SendMessage(hCurrScintilla, SCI_GETSELECTIONS, 0, 0); + if (nbSelections > 1) return; + + size_t bufLength = ::SendMessage(hCurrScintilla, SCI_GETSELTEXT, 0, 0); + if (bufLength == 0) return; + + char *selectedText = new char[bufLength + 1]; + ::SendMessage(hCurrScintilla, SCI_GETSELTEXT, 0, (LPARAM)selectedText); + + // Scintilla 5.1.5+ reports selection length without the terminating NUL. + // strlen also keeps compatibility with older Scintilla length semantics. + const size_t selectedLength = strlen(selectedText); + if (selectedLength == 0) + { + delete [] selectedText; + return; + } + + std::string samlEncodedText; + const int len = samlEncode(samlEncodedText, selectedText, selectedLength); + + switch (len) + { + case 0: + ::MessageBox(nppData._nppHandle, TEXT("SAML Encode returned zero size."), TEXT("SAML Encode"), MB_OK); + break; + case SAML_ENCODE_ERROR_DEFLATE: + ::MessageBox(nppData._nppHandle, TEXT("Could not DEFLATE the selected SAML text."), TEXT("SAML Encode"), MB_OK); + break; + case SAML_ENCODE_ERROR_BASE64: + ::MessageBox(nppData._nppHandle, TEXT("Could not BASE64 Encode the compressed SAML text."), TEXT("SAML Encode"), MB_OK); + break; + case SAML_ENCODE_ERROR_URLENCODE: + ::MessageBox(nppData._nppHandle, TEXT("Could not URL Encode the BASE64 SAML text."), TEXT("SAML Encode"), MB_OK); + break; + default: + { + size_t start = ::SendMessage(hCurrScintilla, SCI_GETSELECTIONSTART, 0, 0); + size_t end = ::SendMessage(hCurrScintilla, SCI_GETSELECTIONEND, 0, 0); + if (end < start) + { + size_t tmp = start; + start = end; + end = tmp; + } + ::SendMessage(hCurrScintilla, SCI_SETTARGETSTART, start, 0); + ::SendMessage(hCurrScintilla, SCI_SETTARGETEND, end, 0); + ::SendMessage(hCurrScintilla, SCI_REPLACETARGET, len, (LPARAM)samlEncodedText.c_str()); + ::SendMessage(hCurrScintilla, SCI_SETSEL, start, start + static_cast(len)); + break; + } + } + + delete [] selectedText; +} + void convertSamlDecode() { HWND hCurrScintilla = getCurrentScintillaHandle(); diff --git a/src/mimeTools.h b/src/mimeTools.h index a98d386..b458f15 100644 --- a/src/mimeTools.h +++ b/src/mimeTools.h @@ -20,8 +20,8 @@ #pragma once -#define VERSION_VALUE "3.1\0" -#define VERSION_DIGITALVALUE 3, 1, 0, 0 +#define VERSION_VALUE "3.5\0" +#define VERSION_DIGITALVALUE 3, 5, 0, 0 #define IDD_ABOUTBOX 250 @@ -32,14 +32,20 @@ #include "url.h" void convertToBase64FromAscii(); +void urlconvertToBase64FromAscii(); void convertToBase64FromAscii_pad(); void convertToBase64FromAscii_B64Format(); +void convertToBase64FromAscii_pad_byline(); void convertToBase64FromAscii_byline(); +void convertToBase64FromUtf16Le(); void convertToAsciiFromBase64(); +void urlconvertToAsciiFromBase64(); void convertToAsciiFromBase64_strict(); void convertToAsciiFromBase64_whitespaceReset(); +void convertToUtf16LeFromBase64(); void convertToQuotedPrintable(); void convertToAsciiFromQuotedPrintable(); +void convertMimeHeaderDecode(); void convertURLMinEncode(); void convertURLEncodeExtended(); void convertURLFullEncode(); @@ -48,6 +54,7 @@ void convertURLEncodeExtendedByLine(); void convertURLFullEncodeByLine(); void convertURLEncode(UrlEncodeMethod method, bool isByLine = false); void convertURLDecode(); +void convertSamlEncode(); void convertSamlDecode(); void convertURLDecode(); void about(); diff --git a/src/qp.cpp b/src/qp.cpp index d71a067..95a792d 100644 --- a/src/qp.cpp +++ b/src/qp.cpp @@ -1,221 +1,153 @@ // This file is part of Notepad++ plugin MIME Tools project // Copyright (C)2023 Don HO -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// at your option any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - - #include "qp.h" -#include -char * QuotedPrintable::encode(const char *str) +#include + +char* QuotedPrintable::encode(const char* str) { - initVar(); - size_t len = strlen(str); - - _bufLen = len * 3; - size_t nbEOL = _bufLen / QP_ENCODED_LINE_LEN_MAX; - _bufLen += nbEOL * 3; - _bufLen += 1; - - _buffer = new char[_bufLen]; - memset(_buffer, 0, _bufLen); - - for (size_t i = 0 ; i < len ; i++) - { - getQPChar(str[i]); - putQPChar(); - } - _buffer[_i] = '\0'; - - return _buffer; + return encode(str, std::strlen(str)); } -void QuotedPrintable::getQPChar(char c) +char* QuotedPrintable::encode(const char* str, std::size_t len) { - bool crlf = false; - if ((c != '=' && c > 32 && c < 127) || c == ' ' || c == ' ' || (UCHAR)c == 0x0D) - { - _chars[0] = c; - _nbChar = 1; - } - else if ((int)c == 0x0A) - { - _chars[0] = c; - _nbChar = 1; - crlf = true; - } - else - { - UCHAR uc = (UCHAR)c; - _chars[0] = '='; - _chars[1] = toChar(uc >> 4); - _chars[2] = toChar(uc & 15); - _chars[3] = '\0'; - _nbChar = 3; - } - - if (crlf) - _nbCharInLine = _nbChar; - else - _nbCharInLine += _nbChar; - - // Lines of Quoted-Printable encoded data must not be longer than 76 characters. - // To satisfy this requirement without altering the encoded text, soft line breaks may be added as desired. - // A soft line break consists of an = at the end of an encoded line, and does not appear as a line break in the decoded text. - // These soft line breaks also allow encoding text without line breaks (or containing very long lines) for an environment where line size is limited, - // such as the 1000 characters per line limit of some SMTP software, as allowed by RFC 2821. - // ref: https://en.wikipedia.org/wiki/Quoted-printable - if (_nbCharInLine >= QP_ENCODED_LINE_LEN_MAX) - { - _buffer[_i++] = '='; - _buffer[_i++] = 0x0D; - _buffer[_i++] = 0x0A; - _nbCharInLine = _nbChar; - } + initVar(); + _bufLen = len * 3 + 1; + const std::size_t nbEOL = (len * 3) / QP_ENCODED_LINE_LEN_MAX; + _bufLen += nbEOL * 3; + + _buffer = new char[_bufLen]; + + for (std::size_t i = 0; i < len; ++i) + { + getQPChar(str[i]); + putQPChar(); + } + _buffer[_i] = '\0'; + return _buffer; } - -void QuotedPrintable::putQPChar() + +void QuotedPrintable::getQPChar(char c) { - // it happens rarely, but it happens - if (_i >= _bufLen) - { - size_t oldLen = _bufLen; - _bufLen *= 2; - char *newBuf = new char[_bufLen]; - - for (size_t i = 0 ; i < oldLen ; i++) - newBuf[i] = _buffer[i]; - - char *tmp = _buffer; - _buffer = newBuf; - delete [] tmp; - } - - for (int i = 0 ; i < _nbChar ; i++) - _buffer[_i++] = _chars[i]; + bool crlf = false; + const auto uc = static_cast(c); + + if ((c != '=' && c > 32 && c < 127) || c == ' ' || c == '\t' || uc == 0x0D) + { + _chars[0] = c; + _nbChar = 1; + } + else if (uc == 0x0A) + { + _chars[0] = c; + _nbChar = 1; + crlf = true; + } + else + { + _chars[0] = '='; + _chars[1] = toChar(uc >> 4); + _chars[2] = toChar(uc & 15); + _nbChar = 3; + } + + if (crlf) + _nbCharInLine = _nbChar; + else + _nbCharInLine += _nbChar; + + if (_nbCharInLine >= QP_ENCODED_LINE_LEN_MAX) + { + if (_i + 3 >= _bufLen) + { + const std::size_t oldLen = _bufLen; + _bufLen = _bufLen * 2 + 4; + char* newBuf = new char[_bufLen]; + std::memcpy(newBuf, _buffer, oldLen); + delete [] _buffer; + _buffer = newBuf; + } + _buffer[_i++] = '='; + _buffer[_i++] = '\r'; + _buffer[_i++] = '\n'; + _nbCharInLine = _nbChar; + } } -char * QuotedPrintable::decode(const char *str) +void QuotedPrintable::putQPChar() { - initVar(); - - char *p = (char *)str; - size_t len = strlen(str); - - _bufLen = len + 1; - _buffer = new char[_bufLen]; - char* line = new char[_bufLen]; - - while (*p) - { - if (readQPLine(&p, line) == -1) - { - delete [] line; - return NULL; - } - - if (!translate(line)) - { - delete[] line; - return NULL; - } - } - _buffer[_i] = '\0'; - delete[] line; - return _buffer; + if (_i + static_cast(_nbChar) >= _bufLen) + { + const std::size_t oldLen = _bufLen; + _bufLen = _bufLen * 2 + static_cast(_nbChar) + 1; + char* newBuf = new char[_bufLen]; + std::memcpy(newBuf, _buffer, oldLen); + delete [] _buffer; + _buffer = newBuf; + } + + for (int i = 0; i < _nbChar; ++i) + _buffer[_i++] = _chars[i]; } -int QuotedPrintable::readQPLine(char **pStr, char *lineBuf) +char* QuotedPrintable::decode(const char* str) { - size_t len = strlen(*pStr); - size_t i = 0; - for (; i < len ; i++) - { - // Make decoding more flexible and less strict (76 characters length of encoded text restriction for decoding is removed). - // - // Both following encoded format - // - // =D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BB=D0=B8=D1=81=D1=8C - // - // and - // - // =D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0= - // =BB=D0=B8=D1=81=D1=8C - // - // are allowed and the result of both are the same. - - /* - if (i >= (QP_ENCODED_LINE_LEN_MAX + 2 + 1)) return -1; - */ - - char c = (*pStr)[i]; - if (c == 0x0D) - { - lineBuf[i] = c; - i++; - if ((i >= len) || (i >= (QP_ENCODED_LINE_LEN_MAX + 2 + 1))) return -1; - if ((*pStr)[i] != (char)0x0A) return -1; - lineBuf[i] = (*pStr)[i]; - i++; - if (i >= (QP_ENCODED_LINE_LEN_MAX + 2 + 1)) return -1; - lineBuf[i] = '\0'; - *pStr += i; - - // Make sure there's no soft line break. - if (i >= 3 && lineBuf[i-3] == '=') - { - lineBuf[i-3] = '\0'; - return int(i - 3); - } - return int(i); - } - else if (c == 0x0A) - { - return -1; - } - else - lineBuf[i] = c; - } - *pStr += i; - lineBuf[i] = '\0'; - return int(i); + return decode(str, std::strlen(str)); } -bool QuotedPrintable::translate(char *line2Trans) +char* QuotedPrintable::decode(const char* str, std::size_t len) { - size_t len = strlen(line2Trans); - for (size_t i = 0 ; i < len ; i++) - { - if (line2Trans[i] == '=') - { - if (i == len || (i + 1) == len|| (i + 2) == len) - return false; - UCHAR restoredChar; - // - - restoredChar = makeChar(line2Trans[i+1], line2Trans[i+2]); - i += 2; - - if (!restoredChar) - return false; - _buffer[_i++] = restoredChar; - } - else - { - _buffer[_i++] = line2Trans[i]; - } - } - return true; + initVar(); + _bufLen = len + 1; + _buffer = new char[_bufLen]; + + // Single linear pass. The previous implementation repeatedly called strlen() + // on the remaining suffix and copied each line into a full-size temporary buffer. + for (std::size_t i = 0; i < len; ) + { + const char c = str[i]; + + if (c == '=') + { + // Soft line break: remove =CRLF entirely. + if (i + 2 < len && str[i + 1] == '\r' && str[i + 2] == '\n') + { + i += 3; + continue; + } + + if (i + 2 >= len) + return nullptr; + + unsigned char restored = 0; + if (!makeChar(str[i + 1], str[i + 2], restored) || restored == 0) + return nullptr; // Preserve the legacy decoder's rejection of =00. + + _buffer[_i++] = static_cast(restored); + i += 3; + continue; + } + + if (c == '\r') + { + if (i + 1 >= len || str[i + 1] != '\n') + return nullptr; + _buffer[_i++] = '\r'; + _buffer[_i++] = '\n'; + i += 2; + continue; + } + + // Preserve the existing decoder's requirement that physical newlines are CRLF. + if (c == '\n') + return nullptr; + + _buffer[_i++] = c; + ++i; + } + + _buffer[_i] = '\0'; + return _buffer; } diff --git a/src/qp.h b/src/qp.h index f415897..1d53998 100644 --- a/src/qp.h +++ b/src/qp.h @@ -1,92 +1,59 @@ // This file is part of Notepad++ plugin MIME Tools project // Copyright (C)2023 Don HO -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// at your option any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - - #pragma once -#include -#include -#include +#include +#include -// "QP works by using the equals sign = as an escape character.It also limits line length to 76, as some software has limits on line length." -// ref: https://en.wikipedia.org/wiki/Quoted-printable constexpr auto QP_ENCODED_LINE_LEN_MAX = 76; class QuotedPrintable { +public: + QuotedPrintable() : _buffer(nullptr) {} + ~QuotedPrintable() { delete [] _buffer; } -public: - QuotedPrintable() : _buffer(NULL) {}; - ~QuotedPrintable() { - if (_buffer) - delete [] _buffer; - }; - char * encode(const char *str); - char * decode(const char *str); + char* encode(const char* str); + char* encode(const char* str, std::size_t len); + char* decode(const char* str); + char* decode(const char* str, std::size_t len); + std::size_t length() const { return _i; } private: - char *_buffer = nullptr; - size_t _bufLen = 0; - size_t _i = 0; - int _nbCharInLine = 0; - - int _nbChar = 0; - char _chars[4] = {}; - - int readQPLine(char **pStr, char *lineBuf); - bool translate(char *line2Trans); - - void putQPChar(); - void getQPChar(char c); - - int32_t charToDigit(char c) const { - if (c >= '0' && c <= '9') - return (c - '0'); - if (c >= 'A' && c <= 'F') - return (10 + c - 'A'); - return -1; - }; - - unsigned char makeChar(char hiChar, char loChar) const { - auto hi = charToDigit(hiChar); - if (hi == -1) - return 0; - auto lo = charToDigit(loChar); - if (lo == -1) - return 0; - return static_cast(hi << 4 | lo); - }; - - - void initVar() { - if (_buffer) - { - delete [] _buffer; - _buffer = NULL; - } - _bufLen = 0; - _i = 0; - _nbChar = 0; - _nbCharInLine = 0; - }; - - char toChar(int i) { - if (i < 10) - return (char)((int)'0'+ i); - else - return (char)((int)'A'+ i-10); - }; - + char* _buffer = nullptr; + std::size_t _bufLen = 0; + std::size_t _i = 0; + int _nbCharInLine = 0; + int _nbChar = 0; + char _chars[4] = {}; + + void putQPChar(); + void getQPChar(char c); + + int32_t charToDigit(char c) const { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return 10 + c - 'A'; + return -1; + } + + bool makeChar(char hiChar, char loChar, unsigned char& value) const { + const auto hi = charToDigit(hiChar); + const auto lo = charToDigit(loChar); + if (hi < 0 || lo < 0) return false; + value = static_cast((hi << 4) | lo); + return true; + } + + void initVar() { + delete [] _buffer; + _buffer = nullptr; + _bufLen = 0; + _i = 0; + _nbChar = 0; + _nbCharInLine = 0; + } + + static char toChar(int i) { + return i < 10 ? static_cast('0' + i) : static_cast('A' + i - 10); + } }; diff --git a/src/rfc2047.cpp b/src/rfc2047.cpp new file mode 100644 index 0000000..b2b3165 --- /dev/null +++ b/src/rfc2047.cpp @@ -0,0 +1,371 @@ +// RFC 2047 encoded-word decoder for MIME Tools. +// Designed for a single linear pass over the selected text and reuses the +// plugin's optimized Base64 implementation for "B" encoded-words. + +#include "rfc2047.h" +#include "b64.h" + +#include +#include +#include + +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "oleaut32.lib") +#pragma comment(lib, "uuid.lib") + +#include +#include +#include +#include + +namespace +{ + +inline char asciiLower(char c) +{ + return (c >= 'A' && c <= 'Z') ? static_cast(c + ('a' - 'A')) : c; +} + +bool asciiEquals(const char* text, std::size_t length, const char* literal) +{ + std::size_t i = 0; + for (; i < length && literal[i] != '\0'; ++i) + { + if (asciiLower(text[i]) != asciiLower(literal[i])) + return false; + } + return i == length && literal[i] == '\0'; +} + +bool isLinearWhitespace(const char* text, std::size_t length) +{ + for (std::size_t i = 0; i < length; ++i) + { + const char c = text[i]; + if (c != ' ' && c != '\t' && c != '\r' && c != '\n') + return false; + } + return true; +} + +int hexValue(unsigned char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; +} + +bool decodeQWord(const char* encoded, std::size_t length, std::string& decoded) +{ + decoded.clear(); + decoded.reserve(length); + + for (std::size_t i = 0; i < length; ++i) + { + const unsigned char c = static_cast(encoded[i]); + if (c == '_') + { + decoded.push_back(' '); + } + else if (c == '=') + { + if (i + 2 >= length) + return false; + const int hi = hexValue(static_cast(encoded[i + 1])); + const int lo = hexValue(static_cast(encoded[i + 2])); + if (hi < 0 || lo < 0) + return false; + decoded.push_back(static_cast((hi << 4) | lo)); + i += 2; + } + else + { + // RFC 2047 encoded-text cannot contain SPACE, TAB, CR, LF or '?'. + // Rejecting these here keeps malformed words from being rewritten. + if (c <= 0x20 || c == '?' || c == 0x7f) + return false; + decoded.push_back(static_cast(c)); + } + } + + return true; +} + +bool decodeBWord(const char* encoded, std::size_t length, std::string& decoded) +{ + if (length == 0 || length > static_cast(INT_MAX)) + return false; + + // RFC 2047 B encoded-text cannot contain whitespace or arbitrary bytes. + // Validate before entering the permissive legacy Base64 lookup table. + for (std::size_t i = 0; i < length; ++i) + { + const unsigned char c = static_cast(encoded[i]); + const bool valid = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '+' || c == '/' || c == '='; + if (!valid) + return false; + } + + decoded.assign(length, '\0'); + const int decodedLength = base64Decode(&decoded[0], encoded, length, true, false); + if (decodedLength < 0) + { + decoded.clear(); + return false; + } + + decoded.resize(static_cast(decodedLength)); + return true; +} + +class CharsetResolver +{ +public: + CharsetResolver() : _multiLanguage(nullptr), _comInitialized(false), _triedCom(false) {} + + ~CharsetResolver() + { + if (_multiLanguage != nullptr) + _multiLanguage->Release(); + if (_comInitialized) + ::CoUninitialize(); + } + + UINT resolve(const char* charset, std::size_t length) + { + // Fast paths for the overwhelmingly common MIME charsets. These avoid + // COM/MIME-database setup for the normal UTF-8 case. + if (asciiEquals(charset, length, "utf-8") || asciiEquals(charset, length, "utf8")) + return CP_UTF8; + if (asciiEquals(charset, length, "us-ascii") || asciiEquals(charset, length, "ascii")) + return 20127; + if (asciiEquals(charset, length, "iso-8859-1") || + asciiEquals(charset, length, "latin1") || asciiEquals(charset, length, "latin-1")) + return 28591; + if (asciiEquals(charset, length, "windows-1252") || asciiEquals(charset, length, "cp1252")) + return 1252; + + // Fall back to Windows' MIME charset database for less common aliases + // and legacy encodings rather than carrying a large hand-maintained map. + if (!ensureMultiLanguage() || length > static_cast(UINT_MAX)) + return 0; + + BSTR charsetName = ::SysAllocStringLen(nullptr, static_cast(length)); + if (charsetName == nullptr) + return 0; + + for (std::size_t i = 0; i < length; ++i) + charsetName[i] = static_cast(charset[i]); + + MIMECSETINFO info = {}; + const HRESULT hr = _multiLanguage->GetCharsetInfo(charsetName, &info); + ::SysFreeString(charsetName); + return SUCCEEDED(hr) ? (info.uiInternetEncoding != 0 ? info.uiInternetEncoding : info.uiCodePage) : 0; + } + +private: + bool ensureMultiLanguage() + { + if (_triedCom) + return _multiLanguage != nullptr; + + _triedCom = true; + const HRESULT init = ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + if (init == S_OK || init == S_FALSE) + _comInitialized = true; + else if (init != RPC_E_CHANGED_MODE) + return false; + + const HRESULT hr = ::CoCreateInstance( + CLSID_CMultiLanguage, nullptr, CLSCTX_INPROC_SERVER, + IID_IMultiLanguage2, reinterpret_cast(&_multiLanguage)); + return SUCCEEDED(hr) && _multiLanguage != nullptr; + } + + IMultiLanguage2* _multiLanguage; + bool _comInitialized; + bool _triedCom; +}; + +bool appendConverted(std::string& output, const std::string& decodedBytes, + UINT sourceCodePage, UINT targetCodePage) +{ + if (decodedBytes.empty()) + return true; + + if (targetCodePage == 0) + targetCodePage = ::GetACP(); + + // No transcode needed. This is the hot path for UTF-8 mail in a UTF-8 tab. + if (sourceCodePage == targetCodePage) + { + output.append(decodedBytes); + return true; + } + + if (decodedBytes.size() > static_cast(INT_MAX)) + return false; + + const DWORD inFlags = sourceCodePage == CP_UTF8 ? MB_ERR_INVALID_CHARS : 0; + const int wideLength = ::MultiByteToWideChar( + sourceCodePage, inFlags, decodedBytes.data(), + static_cast(decodedBytes.size()), nullptr, 0); + if (wideLength <= 0) + return false; + + std::vector wide(static_cast(wideLength)); + if (::MultiByteToWideChar(sourceCodePage, inFlags, decodedBytes.data(), + static_cast(decodedBytes.size()), &wide[0], wideLength) != wideLength) + return false; + + if (targetCodePage == CP_UTF8 || targetCodePage == 54936) + { + const int outLength = ::WideCharToMultiByte( + targetCodePage, 0, &wide[0], wideLength, nullptr, 0, nullptr, nullptr); + if (outLength <= 0) + return false; + + const std::size_t oldSize = output.size(); + output.resize(oldSize + static_cast(outLength)); + if (::WideCharToMultiByte(targetCodePage, 0, &wide[0], wideLength, + &output[oldSize], outLength, nullptr, nullptr) != outLength) + { + output.resize(oldSize); + return false; + } + return true; + } + + BOOL usedDefault = FALSE; + const int outLength = ::WideCharToMultiByte( + targetCodePage, WC_NO_BEST_FIT_CHARS, &wide[0], wideLength, + nullptr, 0, nullptr, &usedDefault); + if (outLength <= 0 || usedDefault) + return false; + + const std::size_t oldSize = output.size(); + output.resize(oldSize + static_cast(outLength)); + usedDefault = FALSE; + if (::WideCharToMultiByte(targetCodePage, WC_NO_BEST_FIT_CHARS, &wide[0], wideLength, + &output[oldSize], outLength, nullptr, &usedDefault) != outLength || usedDefault) + { + output.resize(oldSize); + return false; + } + return true; +} + +std::size_t findEncodedWordStart(const char* input, std::size_t from, std::size_t length) +{ + while (from + 1 < length) + { + const void* found = std::memchr(input + from, '=', length - from - 1); + if (found == nullptr) + return length; + + const std::size_t pos = static_cast(found) - input; + if (input[pos + 1] == '?') + return pos; + from = pos + 1; + } + return length; +} + +std::size_t findQuestion(const char* input, std::size_t from, std::size_t length) +{ + const void* found = std::memchr(input + from, '?', length - from); + return found == nullptr ? length : static_cast(found) - input; +} + +bool tryDecodeWord(const char* input, std::size_t length, std::size_t start, + UINT targetCodePage, CharsetResolver& resolver, + std::string& converted, std::size_t& wordEnd) +{ + const std::size_t charsetEnd = findQuestion(input, start + 2, length); + if (charsetEnd == length || charsetEnd == start + 2) + return false; + + const std::size_t encodingEnd = findQuestion(input, charsetEnd + 1, length); + if (encodingEnd == length || encodingEnd != charsetEnd + 2) + return false; + + std::size_t textEnd = encodingEnd + 1; + while (textEnd + 1 < length && !(input[textEnd] == '?' && input[textEnd + 1] == '=')) + ++textEnd; + if (textEnd + 1 >= length || textEnd == encodingEnd + 1) + return false; + + const char encoding = asciiLower(input[charsetEnd + 1]); + if (encoding != 'b' && encoding != 'q') + return false; + + const UINT sourceCodePage = resolver.resolve(input + start + 2, charsetEnd - (start + 2)); + if (sourceCodePage == 0) + return false; + + const char* encodedText = input + encodingEnd + 1; + const std::size_t encodedLength = textEnd - (encodingEnd + 1); + + std::string decodedBytes; + const bool decoded = encoding == 'b' + ? decodeBWord(encodedText, encodedLength, decodedBytes) + : decodeQWord(encodedText, encodedLength, decodedBytes); + if (!decoded) + return false; + + converted.clear(); + converted.reserve(decodedBytes.size()); + if (!appendConverted(converted, decodedBytes, sourceCodePage, targetCodePage)) + return false; + + wordEnd = textEnd + 2; + return true; +} + +} // namespace + +Rfc2047DecodeResult decodeRfc2047Header(const char* input, std::size_t inputLength, + unsigned int targetCodePage, std::string& output) +{ + Rfc2047DecodeResult result = { 0, 0 }; + output.clear(); + output.reserve(inputLength); + + CharsetResolver resolver; + std::size_t cursor = 0; + bool previousWasEncodedWord = false; + + while (cursor < inputLength) + { + const std::size_t wordStart = findEncodedWordStart(input, cursor, inputLength); + if (wordStart == inputLength) + break; + + std::string converted; + std::size_t wordEnd = wordStart; + if (tryDecodeWord(input, inputLength, wordStart, targetCodePage, resolver, converted, wordEnd)) + { + const std::size_t betweenLength = wordStart - cursor; + if (!(previousWasEncodedWord && isLinearWhitespace(input + cursor, betweenLength))) + output.append(input + cursor, betweenLength); + + output.append(converted); + cursor = wordEnd; + previousWasEncodedWord = true; + ++result.decodedWords; + continue; + } + + // Preserve a malformed/unsupported candidate exactly and advance past + // the marker so the scan remains linear even on hostile input. + output.append(input + cursor, wordStart + 2 - cursor); + cursor = wordStart + 2; + previousWasEncodedWord = false; + ++result.skippedWords; + } + + output.append(input + cursor, inputLength - cursor); + return result; +} diff --git a/src/rfc2047.h b/src/rfc2047.h new file mode 100644 index 0000000..d21a5ac --- /dev/null +++ b/src/rfc2047.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +struct Rfc2047DecodeResult +{ + std::size_t decodedWords; + std::size_t skippedWords; +}; + +// Decode RFC 2047 encoded-words in a selected header fragment. +// targetCodePage is Scintilla's current document code page (0 means system ANSI). +Rfc2047DecodeResult decodeRfc2047Header(const char* input, std::size_t inputLength, + unsigned int targetCodePage, std::string& output); diff --git a/src/saml.cpp b/src/saml.cpp index c1e68af..6fe7607 100644 --- a/src/saml.cpp +++ b/src/saml.cpp @@ -18,8 +18,78 @@ #include "saml.h" #include "b64.h" #include "url.h" +#include "tdeflate.h" #include "tinf.h" +#include +#include +#include +#include +#include + + +int samlEncode(std::string& dest, const char* samlStr, std::size_t samlLength) +{ + dest.clear(); + + if (samlStr == nullptr || samlLength == 0) + return 0; + + if (samlLength > static_cast(SAML_MESSAGE_MAX_SIZE)) + return SAML_ENCODE_ERROR_DEFLATE; + + std::vector compressed; + if (!tdefl_compress_raw(compressed, + reinterpret_cast(samlStr), + samlLength)) + { + return SAML_ENCODE_ERROR_DEFLATE; + } + + if (compressed.size() > std::numeric_limits::max() - 2) + return SAML_ENCODE_ERROR_BASE64; + + const std::size_t base64Groups = (compressed.size() + 2) / 3; + if (base64Groups > std::numeric_limits::max() / 4) + return SAML_ENCODE_ERROR_BASE64; + + const std::size_t base64Capacity = base64Groups * 4; + if (base64Capacity > static_cast(INT_MAX)) + return SAML_ENCODE_ERROR_BASE64; + + std::string base64Text(base64Capacity, '\0'); + const int base64Length = base64Encode( + &base64Text[0], + reinterpret_cast(compressed.data()), + compressed.size(), + 0, + true, + false); + + if (base64Length < 0) + return SAML_ENCODE_ERROR_BASE64; + + base64Text.resize(static_cast(base64Length)); + + // SAML HTTP-Redirect requires the Base64 value to be URL encoded. Use the + // extended encoder so '+' is escaped as %2B instead of being vulnerable to + // form/query parsers that interpret '+' as a space. + const std::size_t base64Size = base64Text.size(); + if (base64Size > (static_cast(INT_MAX) - 1) / 3) + return SAML_ENCODE_ERROR_URLENCODE; + + const int urlCapacity = static_cast(base64Size * 3 + 1); + std::string urlText(static_cast(urlCapacity), '\0'); + const int urlLength = AsciiToUrl(&urlText[0], base64Text.c_str(), + urlCapacity, UrlEncodeMethod::extended); + if (urlLength < 0 || urlLength >= urlCapacity) + return SAML_ENCODE_ERROR_URLENCODE; + + urlText.resize(static_cast(urlLength)); + dest = std::move(urlText); + return urlLength; +} + int samlDecode(char *dest, const char *encodedSamlStr, int bufLength) { @@ -30,7 +100,7 @@ int samlDecode(char *dest, const char *encodedSamlStr, int bufLength) // URL Decode - size_t urlDecodedLen = UrlToAscii(pUrlDecodedText, encodedSamlStr, bufLength); + int urlDecodedLen = UrlToAscii(pUrlDecodedText, encodedSamlStr, bufLength); if (urlDecodedLen < 0) { @@ -45,7 +115,10 @@ int samlDecode(char *dest, const char *encodedSamlStr, int bufLength) delete[] pUrlDecodedText; if (base64DecodedLen < 0) - return SAML_DECODE_ERROR_BASE64DECODE; + { + delete [] base64DecodedText; + return SAML_DECODE_ERROR_BASE64DECODE; + } base64DecodedText[base64DecodedLen] = '\0'; diff --git a/src/saml.h b/src/saml.h index 2b76ec7..5aa4668 100644 --- a/src/saml.h +++ b/src/saml.h @@ -16,15 +16,17 @@ #pragma once -#include "b64.h" -#include "url.h" +#include +#include constexpr int SAML_DECODE_ERROR_URLDECODE = -1; constexpr int SAML_DECODE_ERROR_BASE64DECODE = -2; constexpr int SAML_DECODE_ERROR_INFLATE = -3; - +constexpr int SAML_ENCODE_ERROR_DEFLATE = -4; +constexpr int SAML_ENCODE_ERROR_BASE64 = -5; +constexpr int SAML_ENCODE_ERROR_URLENCODE = -6; constexpr int SAML_MESSAGE_MAX_SIZE = 200000; +int samlEncode(std::string& dest, const char* samlStr, std::size_t samlLength); int samlDecode(char *dest, const char *samlStr, int bufLength); - diff --git a/src/tdeflate.cpp b/src/tdeflate.cpp new file mode 100644 index 0000000..880ae27 --- /dev/null +++ b/src/tdeflate.cpp @@ -0,0 +1,358 @@ +// This file is part of Notepad++ plugin MIME Tools project +// Copyright (C)2026 ExSlam contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// at your option any later version. + +#include "tdeflate.h" + +#include +#include +#include +#include + +namespace { + +constexpr std::size_t kWindowSize = 32768; +constexpr std::size_t kMaxMatch = 258; +constexpr std::size_t kMinMatch = 3; +constexpr std::size_t kHashBits = 15; +constexpr std::size_t kHashSize = static_cast(1) << kHashBits; +constexpr int kMaxChain = 64; + +constexpr unsigned short kLengthBase[] = { + 3, 4, 5, 6, 7, 8, 9, 10, + 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, + 131, 163, 195, 227, 258 +}; + +constexpr unsigned char kLengthExtra[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, + 5, 5, 5, 5, 0 +}; + +constexpr unsigned short kDistanceBase[] = { + 1, 2, 3, 4, 5, 7, 9, 13, + 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, + 4097, 6145, 8193, 12289, 16385, 24577 +}; + +constexpr unsigned char kDistanceExtra[] = { + 0, 0, 0, 0, 1, 1, 2, 2, + 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, + 11, 11, 12, 12, 13, 13 +}; + +unsigned reverseBits(unsigned value, unsigned bitCount) +{ + unsigned reversed = 0; + for (unsigned i = 0; i < bitCount; ++i) + { + reversed = (reversed << 1) | (value & 1U); + value >>= 1; + } + return reversed; +} + +class BitWriter +{ +public: + void reserve(std::size_t bytes) + { + _bytes.reserve(bytes); + } + + void writeBits(unsigned value, unsigned bitCount) + { + _bitBuffer |= static_cast(value) << _bitCount; + _bitCount += bitCount; + + while (_bitCount >= 8) + { + _bytes.push_back(static_cast(_bitBuffer & 0xffU)); + _bitBuffer >>= 8; + _bitCount -= 8; + } + } + + void finish() + { + if (_bitCount != 0) + { + _bytes.push_back(static_cast(_bitBuffer & 0xffU)); + _bitBuffer = 0; + _bitCount = 0; + } + } + + std::vector& bytes() + { + return _bytes; + } + +private: + std::vector _bytes; + std::uint64_t _bitBuffer = 0; + unsigned _bitCount = 0; +}; + +void writeFixedSymbol(BitWriter& writer, unsigned symbol) +{ + unsigned code = 0; + unsigned bitCount = 0; + + if (symbol <= 143) + { + code = 0x30U + symbol; + bitCount = 8; + } + else if (symbol <= 255) + { + code = 0x190U + (symbol - 144U); + bitCount = 9; + } + else if (symbol <= 279) + { + code = symbol - 256U; + bitCount = 7; + } + else + { + code = 0xC0U + (symbol - 280U); + bitCount = 8; + } + + writer.writeBits(reverseBits(code, bitCount), bitCount); +} + +void writeLength(BitWriter& writer, std::size_t length) +{ + for (unsigned i = 0; i < 29; ++i) + { + const unsigned extraBits = kLengthExtra[i]; + const std::size_t maxLength = static_cast(kLengthBase[i]) + + (extraBits == 0 ? 0 : ((static_cast(1) << extraBits) - 1)); + + if (length <= maxLength) + { + writeFixedSymbol(writer, 257U + i); + if (extraBits != 0) + { + const unsigned extraValue = static_cast( + length - static_cast(kLengthBase[i])); + writer.writeBits(extraValue, extraBits); + } + return; + } + } +} + +void writeDistance(BitWriter& writer, std::size_t distance) +{ + for (unsigned i = 0; i < 30; ++i) + { + const unsigned extraBits = kDistanceExtra[i]; + const std::size_t maxDistance = static_cast(kDistanceBase[i]) + + (extraBits == 0 ? 0 : ((static_cast(1) << extraBits) - 1)); + + if (distance <= maxDistance) + { + // Fixed distance codes are 5-bit canonical codes. Huffman codes are + // transmitted MSB-first, so reverse before feeding our LSB bit writer. + writer.writeBits(reverseBits(i, 5), 5); + if (extraBits != 0) + { + const unsigned extraValue = static_cast( + distance - static_cast(kDistanceBase[i])); + writer.writeBits(extraValue, extraBits); + } + return; + } + } +} + +std::size_t hash3(const unsigned char* input, std::size_t position) +{ + const std::uint32_t value = + (static_cast(input[position]) << 16) | + (static_cast(input[position + 1]) << 8) | + static_cast(input[position + 2]); + return static_cast((value * 2654435761U) >> (32U - kHashBits)); +} + +void insertPosition(const unsigned char* input, + std::size_t inputLength, + std::size_t position, + std::vector& head, + std::vector& previous) +{ + if (position + kMinMatch > inputLength) + return; + + const std::size_t hash = hash3(input, position); + previous[position] = head[hash]; + head[hash] = static_cast(position); +} + +void findMatch(const unsigned char* input, + std::size_t inputLength, + std::size_t position, + const std::vector& head, + const std::vector& previous, + std::size_t& bestLength, + std::size_t& bestDistance) +{ + bestLength = 0; + bestDistance = 0; + + if (position + kMinMatch > inputLength) + return; + + const std::size_t maxLength = std::min(kMaxMatch, inputLength - position); + int candidate = head[hash3(input, position)]; + int chain = 0; + + while (candidate >= 0 && chain < kMaxChain) + { + const std::size_t candidatePosition = static_cast(candidate); + const std::size_t distance = position - candidatePosition; + if (distance == 0 || distance > kWindowSize) + break; + + if (bestLength == 0 || + (bestLength < maxLength && + input[candidatePosition + bestLength] == input[position + bestLength])) + { + std::size_t length = 0; + while (length < maxLength && + input[candidatePosition + length] == input[position + length]) + { + ++length; + } + + if (length >= kMinMatch && length > bestLength) + { + bestLength = length; + bestDistance = distance; + if (length == maxLength) + break; + } + } + + candidate = previous[candidatePosition]; + ++chain; + } +} + +std::vector compressFixed(const unsigned char* input, std::size_t inputLength) +{ + BitWriter writer; + writer.reserve(inputLength + inputLength / 16 + 32); + + // BFINAL=1, BTYPE=01 (fixed Huffman codes). + writer.writeBits(1, 1); + writer.writeBits(1, 2); + + std::vector head(kHashSize, -1); + std::vector previous(inputLength, -1); + + std::size_t position = 0; + while (position < inputLength) + { + std::size_t matchLength = 0; + std::size_t matchDistance = 0; + findMatch(input, inputLength, position, head, previous, matchLength, matchDistance); + + // Three-byte matches with very large distances can cost more than literals. + // Requiring four bytes in that case gives better worst-case output while + // preserving useful short nearby matches. + const bool useMatch = matchLength >= kMinMatch && + !(matchLength == 3 && matchDistance > 1024); + + if (useMatch) + { + writeLength(writer, matchLength); + writeDistance(writer, matchDistance); + + for (std::size_t i = 0; i < matchLength; ++i) + insertPosition(input, inputLength, position + i, head, previous); + + position += matchLength; + } + else + { + writeFixedSymbol(writer, input[position]); + insertPosition(input, inputLength, position, head, previous); + ++position; + } + } + + writeFixedSymbol(writer, 256); // End-of-block. + writer.finish(); + return std::move(writer.bytes()); +} + +std::vector compressStored(const unsigned char* input, std::size_t inputLength) +{ + std::vector output; + const std::size_t blockCount = inputLength == 0 ? 1 : (inputLength + 65534) / 65535; + output.reserve(inputLength + blockCount * 5); + + std::size_t position = 0; + do + { + const std::size_t remaining = inputLength - position; + const unsigned short length = static_cast( + std::min(remaining, 65535)); + const bool finalBlock = position + length == inputLength; + + // Stored blocks start on a byte boundary. The low three bits are + // BFINAL followed by BTYPE=00; the remaining five padding bits are zero. + output.push_back(static_cast(finalBlock ? 0x01 : 0x00)); + output.push_back(static_cast(length & 0xffU)); + output.push_back(static_cast((length >> 8) & 0xffU)); + + const unsigned short inverseLength = static_cast(~length); + output.push_back(static_cast(inverseLength & 0xffU)); + output.push_back(static_cast((inverseLength >> 8) & 0xffU)); + + output.insert(output.end(), input + position, input + position + length); + position += length; + } + while (position < inputLength); + + return output; +} + +} // namespace + +bool tdefl_compress_raw(std::vector& output, + const unsigned char* input, + std::size_t inputLength) +{ + if (input == nullptr && inputLength != 0) + return false; + + try + { + const unsigned char emptyInput = 0; + const unsigned char* source = inputLength == 0 ? &emptyInput : input; + std::vector fixed = compressFixed(source, inputLength); + std::vector stored = compressStored(source, inputLength); + + output = fixed.size() <= stored.size() ? std::move(fixed) : std::move(stored); + return true; + } + catch (const std::bad_alloc&) + { + output.clear(); + return false; + } +} diff --git a/src/tdeflate.h b/src/tdeflate.h new file mode 100644 index 0000000..76e6cee --- /dev/null +++ b/src/tdeflate.h @@ -0,0 +1,19 @@ +// This file is part of Notepad++ plugin MIME Tools project +// Copyright (C)2026 ExSlam contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// at your option any later version. + +#pragma once + +#include +#include + +// Compresses input as an RFC 1951 raw DEFLATE stream (no zlib/gzip wrapper). +// The encoder uses fixed Huffman blocks with LZ77 matching and falls back to +// stored blocks if that would be smaller. Returns false on allocation failure. +bool tdefl_compress_raw(std::vector& output, + const unsigned char* input, + std::size_t inputLength); diff --git a/src/tinf.h b/src/tinf.h index 97bb952..dabe404 100644 --- a/src/tinf.h +++ b/src/tinf.h @@ -28,21 +28,15 @@ extern "C" { #define TINF_OK 0 #define TINF_DATA_ERROR (-3) -/* function prototypes */ + /* function prototypes used by MIME Tools */ -void TINFCC tinf_init(); + void TINFCC tinf_init(); -int TINFCC tinf_uncompress(void *dest, unsigned int *destLen, const void *source); - -int TINFCC tinf_gzip_uncompress(void *dest, unsigned int *destLen, - const void *source, unsigned int sourceLen); - -int TINFCC tinf_zlib_uncompress(void *dest, unsigned int *destLen, - const void *source, unsigned int sourceLen); - -unsigned int TINFCC tinf_adler32(const void *data, unsigned int length); - -unsigned int TINFCC tinf_crc32(const void *data, unsigned int length); + int TINFCC tinf_uncompress( + void* dest, + unsigned int* destLen, + const void* source + ); #ifdef __cplusplus } /* extern "C" */ diff --git a/src/tinflate.c b/src/tinflate.c index 3eaaec4..a367326 100644 --- a/src/tinflate.c +++ b/src/tinflate.c @@ -89,12 +89,12 @@ static void tinf_build_bits_base(unsigned char *bits, unsigned short *base, int for (i = 0; i < delta; ++i) bits[i] = 0; for (i = 0; i < 30 - delta; ++i) - bits[i + delta] = (char)(i / delta); + bits[i + delta] = (unsigned char)(i / delta); /* build base table */ for (sum = first, i = 0; i < 30; ++i) { - base[i] = (char)sum; + base[i] = (unsigned short)sum; sum += 1 << bits[i]; } } diff --git a/vs.proj/mimeTools.vcxproj b/vs.proj/mimeTools.vcxproj index aeb0817..a746876 100644 --- a/vs.proj/mimeTools.vcxproj +++ b/vs.proj/mimeTools.vcxproj @@ -1,4 +1,4 @@ - + @@ -29,8 +29,10 @@ + + @@ -38,11 +40,13 @@ + + @@ -151,11 +155,13 @@ MultiThreadedDebug true false + true true Windows true + UseLinkTimeCodeGeneration shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) @@ -172,11 +178,13 @@ MultiThreadedDebug true false + true true Windows true + UseLinkTimeCodeGeneration shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) @@ -193,11 +201,13 @@ MultiThreadedDebug true false + true true Windows true + UseLinkTimeCodeGeneration shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) @@ -205,22 +215,24 @@ Level4 NotUsing - Full + MaxSpeed true - false + true WIN32;NDEBUG;_WINDOWS;_USRDLL;MIMETOOLS_EXPORTS;%(PreprocessorDefinitions) true Speed - false + true MultiThreaded true + true true Windows false + UseLinkTimeCodeGeneration true true shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) @@ -236,21 +248,24 @@ copy ..\readme.txt ..\bin\readme.txt Level4 - Full + MaxSpeed true - false + true WIN32;NDEBUG;_WINDOWS;_USRDLL;MIMETOOLS_EXPORTS;%(PreprocessorDefinitions) true Speed false - false + true MultiThreaded true + true + true true Windows false + UseLinkTimeCodeGeneration true true shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) @@ -266,21 +281,23 @@ copy ..\readme.txt ..\bin64\readme.txt Level4 - Full + MaxSpeed true - false + true WIN32;NDEBUG;_WINDOWS;_USRDLL;MIMETOOLS_EXPORTS;%(PreprocessorDefinitions) true Speed false - false + true MultiThreaded true + true true Windows false + UseLinkTimeCodeGeneration true true shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)