3 #define POCO_NO_UNWINDOWS 1
4 #include <Poco/FileStream.h>
5 #include <Poco/SAX/SAXParser.h>
6 #include <Poco/SAX/SAXException.h>
7 #include <Poco/SAX/ContentHandler.h>
8 #include <Poco/SAX/Attributes.h>
9 #include <Poco/Exception.h>
12 #include <unordered_set>
20 #include "Environment.h"
21 #include "OptionsMgr.h"
22 #include "OptionsDef.h"
23 #include "codepage_detect.h"
25 #include "WinMergePluginBase.h"
28 using Poco::XML::SAXParser;
29 using Poco::XML::ContentHandler;
30 using Poco::XML::Locator;
31 using Poco::XML::XMLChar;
32 using Poco::XML::XMLString;
33 using Poco::XML::Attributes;
34 using namespace std::literals::string_literals;
38 HRESULT ReadFile(const String& path, String& text)
41 if (!file.OpenReadOnly(path))
42 return HRESULT_FROM_WIN32(GetLastError());
46 int iGuessEncodingType = GetOptionsMgr()->GetInt(OPT_CP_DETECT);
47 int64_t fileSize = file.GetFileSize();
48 FileTextEncoding encoding = codepage_detect::Guess(
49 paths::FindExtension(path), file.GetBase(), static_cast<size_t>(
50 fileSize < static_cast<int64_t>(codepage_detect::BufSize) ?
51 fileSize : static_cast<int64_t>(codepage_detect::BufSize)),
53 file.SetCodepage(encoding.m_codepage);
55 file.ReadStringAll(text);
60 HRESULT WriteFile(const String& path, const String& text, bool bom = true)
63 if (!fileOut.Open(path, _T("wb")))
64 return HRESULT_FROM_WIN32(GetLastError());
65 fileOut.SetUnicoding(ucr::UNICODESET::UTF8);
66 fileOut.WriteString(text);
72 namespace internal_plugin
78 String m_fileExtension;
84 std::unique_ptr<Script> m_script;
89 Info(const String& name) : m_name(name) {}
90 Info(Info&& info) = default;
95 bool m_isAutomatic = false;
96 String m_unpackedFileExtension;
97 String m_extendedProperties;
99 std::unique_ptr<Method> m_prediffFile;
100 std::unique_ptr<Method> m_unpackFile;
101 std::unique_ptr<Method> m_packFile;
102 std::unique_ptr<Method> m_unpackFolder;
103 std::unique_ptr<Method> m_packFolder;
104 std::map<String, Method> m_editorScripts;
107 class XMLHandler : public Poco::XML::ContentHandler
110 inline static const std::string Empty = "";
111 inline static const std::string PluginsElement = "plugins";
112 inline static const std::string PluginElement = "plugin";
113 inline static const std::string EventElement = "event";
114 inline static const std::string DescriptionElement = "description";
115 inline static const std::string FileFiltersElement = "file-filters";
116 inline static const std::string IsAutomaticElement = "is-automatic";
117 inline static const std::string UnpackedFileExtensionElement = "unpacked-file-extension";
118 inline static const std::string ExtendedPropertiesElement = "extended-properties";
119 inline static const std::string ArgumentsElement = "arguments";
120 inline static const std::string PrediffFileElement = "prediff-file";
121 inline static const std::string UnpackFileElement = "unpack-file";
122 inline static const std::string PackFileElement = "pack-file";
123 inline static const std::string UnpackFolderElement = "unpack-folder";
124 inline static const std::string PackFolderElement = "pack-folder";
125 inline static const std::string CommandElement = "command";
126 inline static const std::string ScriptElement = "script";
127 inline static const std::string NameAttribute = "name";
128 inline static const std::string ValueAttribute = "value";
129 inline static const std::string FileExtensionAttribute = "fileExtension";
131 explicit XMLHandler(std::list<Info>* pPlugins) : m_pPlugins(pPlugins) {}
133 void setDocumentLocator(const Locator* loc) {}
134 void startDocument() {}
135 void endDocument() {}
136 void startElement(const XMLString& uri, const XMLString& localName, const XMLString& qname, const Attributes& attributes)
138 if (!m_stack.empty())
140 if (m_stack.top() == PluginsElement)
142 if (localName == PluginElement)
145 int index = attributes.getIndex(Empty, NameAttribute);
147 name = ucr::toTString(attributes.getValue(index));
148 m_pPlugins->emplace_back(name);
152 else if (m_stack.top() == PluginElement)
154 Info& plugin = m_pPlugins->back();
156 int index = attributes.getIndex(Empty, ValueAttribute);
159 value = ucr::toTString(attributes.getValue(index));
160 if (localName == EventElement)
161 plugin.m_event = value;
162 else if (localName == DescriptionElement)
163 plugin.m_description = value;
164 else if (localName == FileFiltersElement)
165 plugin.m_fileFilters = value;
166 else if (localName == IsAutomaticElement)
168 TCHAR ch = value.c_str()[0];
169 plugin.m_isAutomatic = (ch == 't' || ch == 'T');
171 else if (localName == UnpackedFileExtensionElement)
172 plugin.m_unpackedFileExtension = value;
173 else if (localName == ExtendedPropertiesElement)
174 plugin.m_extendedProperties = value;
175 else if (localName == ArgumentsElement)
176 plugin.m_arguments = value;
178 else if (localName == PrediffFileElement)
180 plugin.m_prediffFile.reset(new Method());
181 m_pMethod = plugin.m_prediffFile.get();
183 else if (localName == UnpackFileElement)
185 plugin.m_unpackFile.reset(new Method());
186 m_pMethod = plugin.m_unpackFile.get();
188 else if (localName == PackFileElement)
190 plugin.m_packFile.reset(new Method());
191 m_pMethod = plugin.m_packFile.get();
193 else if (localName == UnpackFolderElement)
195 plugin.m_unpackFolder.reset(new Method());
196 m_pMethod = plugin.m_unpackFolder.get();
198 else if (localName == PackFolderElement)
200 plugin.m_packFolder.reset(new Method());
201 m_pMethod = plugin.m_packFolder.get();
206 if (localName == ScriptElement)
208 m_pMethod->m_script.reset(new Script);
209 int index = attributes.getIndex(Empty, FileExtensionAttribute);
211 m_pMethod->m_script->m_fileExtension = ucr::toTString(attributes.getValue(index));
215 m_stack.push(localName);
217 void endElement(const XMLString& uri, const XMLString& localName, const XMLString& qname)
221 void characters(const XMLChar ch[], int start, int length)
225 if (m_stack.top() == CommandElement && m_pMethod)
227 m_pMethod->m_command += xmlch2tstr(ch, length);
229 else if (m_stack.top() == ScriptElement && m_pMethod && m_pMethod->m_script)
231 m_pMethod->m_script->m_body += xmlch2tstr(ch, length);
234 void ignorableWhitespace(const XMLChar ch[], int start, int length) {}
235 void processingInstruction(const XMLString& target, const XMLString& data) {}
236 void startPrefixMapping(const XMLString& prefix, const XMLString& uri) {}
237 void endPrefixMapping(const XMLString& prefix) {}
238 void skippedEntity(const XMLString& name) {}
241 static String xmlch2tstr(const XMLChar* ch, int length)
243 return ucr::toTString(std::string(ch, length));
246 std::list<Info>* m_pPlugins = nullptr;
247 std::stack<std::string> m_stack;
248 Method* m_pMethod = nullptr;
251 class UnpackerGeneratedFromEditorScript : public WinMergePluginBase
254 UnpackerGeneratedFromEditorScript(const PluginInfo& plugin, const std::wstring funcname, int id)
255 : WinMergePluginBase(
257 strutils::format_string1(_T("Unpacker to execute %1 script (automatically generated)"), funcname),
259 , m_pDispatch(plugin.m_lpDispatch)
261 , m_hasArgumentsProperty(plugin.m_hasArgumentsProperty)
262 , m_hasVariablesProperty(plugin.m_hasVariablesProperty)
264 auto desc = plugin.GetExtendedPropertyValue(funcname + _T(".Description"));
265 if (desc.has_value())
266 m_sDescription = *desc;
267 m_pDispatch->AddRef();
268 auto menuCaption = plugin.GetExtendedPropertyValue(funcname + _T(".MenuCaption"));
269 String caption = menuCaption.has_value() ? String{ menuCaption->data(), menuCaption->length() } : funcname;
270 m_sExtendedProperties = ucr::toUTF16(strutils::format(_T("ProcessType=Editor script;MenuCaption=%s"), caption))
271 + (plugin.GetExtendedPropertyValue(funcname + _T(".ArgumentsRequired")).has_value() ? L";ArgumentsRequired" : L"");
272 StringView args = plugin.GetExtendedPropertyValue(funcname + _T(".Arguments")).value_or(_T(""));
273 m_sArguments = { args.data(), args.length() };
276 virtual ~UnpackerGeneratedFromEditorScript()
278 m_pDispatch->Release();
281 HRESULT STDMETHODCALLTYPE UnpackFile(BSTR fileSrc, BSTR fileDst, VARIANT_BOOL* pbChanged, INT* pSubcode, VARIANT_BOOL* pbSuccess) override
284 HRESULT hr = ReadFile(fileSrc, text);
287 if (m_hasVariablesProperty && !plugin::InvokePutPluginVariables(ucr::toTString(fileSrc), m_pDispatch))
289 if (m_hasArgumentsProperty && !plugin::InvokePutPluginArguments(m_sArguments, m_pDispatch))
292 if (!plugin::InvokeTransformText(text, changed, m_pDispatch, m_funcid))
294 hr = WriteFile(fileDst, text);
298 *pbChanged = VARIANT_TRUE;
299 *pbSuccess = VARIANT_TRUE;
303 HRESULT STDMETHODCALLTYPE PackFile(BSTR fileSrc, BSTR fileDst, VARIANT_BOOL* pbChanged, INT subcode, VARIANT_BOOL* pbSuccess) override
305 *pbChanged = VARIANT_FALSE;
306 *pbSuccess = VARIANT_FALSE;
311 IDispatch* m_pDispatch;
313 bool m_hasArgumentsProperty;
314 bool m_hasVariablesProperty;
317 class InternalPlugin : public WinMergePluginBase
320 InternalPlugin(Info&& info)
321 : WinMergePluginBase(info.m_event, info.m_description, info.m_fileFilters, info.m_unpackedFileExtension, info.m_extendedProperties, info.m_arguments, info.m_isAutomatic)
322 , m_info(std::move(info))
326 virtual ~InternalPlugin()
330 HRESULT STDMETHODCALLTYPE PrediffFile(BSTR fileSrc, BSTR fileDst, VARIANT_BOOL* pbChanged, VARIANT_BOOL* pbSuccess) override
332 if (!m_info.m_prediffFile)
334 *pbChanged = VARIANT_FALSE;
335 *pbSuccess = VARIANT_FALSE;
339 String command = replaceMacros(m_info.m_prediffFile->m_command, fileSrc, fileDst);
340 if (m_info.m_prediffFile->m_script)
342 createScript(*m_info.m_prediffFile->m_script, scriptFile);
343 strutils::replace(command, _T("${SCRIPT_FILE}"), scriptFile.GetPath());
346 HRESULT hr = launchProgram(command, SW_HIDE, dwExitCode);
348 *pbChanged = SUCCEEDED(hr);
349 *pbSuccess = SUCCEEDED(hr);
353 HRESULT STDMETHODCALLTYPE UnpackFile(BSTR fileSrc, BSTR fileDst, VARIANT_BOOL* pbChanged, INT* pSubcode, VARIANT_BOOL* pbSuccess) override
355 if (!m_info.m_unpackFile)
358 *pbChanged = VARIANT_FALSE;
359 *pbSuccess = VARIANT_FALSE;
363 String command = replaceMacros(m_info.m_unpackFile->m_command, fileSrc, fileDst);
364 if (m_info.m_unpackFile->m_script)
366 createScript(*m_info.m_unpackFile->m_script, scriptFile);
367 strutils::replace(command, _T("${SCRIPT_FILE}"), scriptFile.GetPath());
370 HRESULT hr = launchProgram(command, SW_HIDE, dwExitCode);
373 *pbChanged = SUCCEEDED(hr);
374 *pbSuccess = SUCCEEDED(hr);
378 HRESULT STDMETHODCALLTYPE PackFile(BSTR fileSrc, BSTR fileDst, VARIANT_BOOL* pbChanged, INT subcode, VARIANT_BOOL* pbSuccess) override
380 if (!m_info.m_packFile)
382 *pbChanged = VARIANT_FALSE;
383 *pbSuccess = VARIANT_FALSE;
387 String command = replaceMacros(m_info.m_packFile->m_command, fileSrc, fileDst);
388 if (m_info.m_packFile->m_script)
390 createScript(*m_info.m_packFile->m_script, scriptFile);
391 strutils::replace(command, _T("${SCRIPT_FILE}"), scriptFile.GetPath());
394 HRESULT hr = launchProgram(command, SW_HIDE, dwExitCode);
396 *pbChanged = SUCCEEDED(hr);
397 *pbSuccess = SUCCEEDED(hr);
403 String replaceMacros(const String& cmd, const String & fileSrc, const String& fileDst)
405 String command = cmd;
406 strutils::replace(command, _T("${SRC_FILE}"), fileSrc);
407 strutils::replace(command, _T("${DST_FILE}"), fileDst);
408 std::vector<StringView> vars = strutils::split(m_sVariables, '\0');
409 for (size_t i = 0; i < vars.size(); ++i)
410 strutils::replace(command, strutils::format(_T("${%d}"), i), { vars[i].data(), vars[i].length() });
411 strutils::replace(command, _T("${*}"), m_sArguments);
415 static HRESULT createScript(const Script& script, TempFile& tempFile)
417 String path = tempFile.Create(_T(""), script.m_fileExtension);
418 return WriteFile(path, script.m_body, false);
421 static HRESULT launchProgram(const String& sCmd, WORD wShowWindow, DWORD &dwExitCode)
424 String sOutputFile = stderrFile.Create();
425 if (_wgetenv(L"WINMERGE_HOME") == nullptr)
426 _wputenv_s(L"WINMERGE_HOME", env::GetProgPath().c_str());
427 String command = sCmd;
428 strutils::replace(command, _T("${WINMERGE_HOME}"), env::GetProgPath());
429 STARTUPINFO stInfo = { sizeof(STARTUPINFO) };
430 stInfo.dwFlags = STARTF_USESHOWWINDOW;
431 stInfo.wShowWindow = wShowWindow;
432 SECURITY_ATTRIBUTES sa{ sizeof(sa) };
433 sa.bInheritHandle = true;
434 stInfo.hStdError = CreateFile(sOutputFile.c_str(), GENERIC_READ | GENERIC_WRITE,
435 FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
436 stInfo.hStdOutput = stInfo.hStdError;
437 stInfo.dwFlags |= STARTF_USESTDHANDLES;
438 PROCESS_INFORMATION processInfo;
439 bool retVal = !!CreateProcess(nullptr, (LPTSTR)command.c_str(),
440 nullptr, nullptr, TRUE, CREATE_DEFAULT_ERROR_MODE, nullptr, nullptr,
441 &stInfo, &processInfo);
443 return HRESULT_FROM_WIN32(GetLastError());
444 WaitForSingleObject(processInfo.hProcess, INFINITE);
445 GetExitCodeProcess(processInfo.hProcess, &dwExitCode);
446 CloseHandle(processInfo.hThread);
447 CloseHandle(processInfo.hProcess);
448 DWORD dwStdErrorSize = 0;
449 if (stInfo.hStdError != nullptr && stInfo.hStdError != INVALID_HANDLE_VALUE)
451 DWORD dwStdErrorSizeHigh = 0;
452 dwStdErrorSize = GetFileSize(stInfo.hStdError, &dwStdErrorSizeHigh);
453 CloseHandle(stInfo.hStdError);
455 if (dwExitCode != 0 && dwStdErrorSize > 0)
458 ReadFile(sOutputFile, error);
459 ICreateErrorInfo* pCreateErrorInfo = nullptr;
460 if (SUCCEEDED(CreateErrorInfo(&pCreateErrorInfo)))
462 pCreateErrorInfo->SetSource(const_cast<OLECHAR*>(command.c_str()));
463 pCreateErrorInfo->SetDescription(const_cast<OLECHAR*>(ucr::toUTF16(error).c_str()));
464 IErrorInfo* pErrorInfo = nullptr;
465 pCreateErrorInfo->QueryInterface(&pErrorInfo);
466 SetErrorInfo(0, pErrorInfo);
467 pErrorInfo->Release();
468 pCreateErrorInfo->Release();
469 return DISP_E_EXCEPTION;
479 class EditorScriptGeneratedFromUnpacker: public WinMergePluginBase
482 EditorScriptGeneratedFromUnpacker(const PluginInfo& plugin, const String& funcname)
483 : WinMergePluginBase(
485 plugin.m_description,
486 plugin.m_filtersTextDefault, L"", plugin.m_extendedProperties, plugin.m_arguments)
487 , m_pDispatch(plugin.m_lpDispatch)
489 m_pDispatch->AddRef();
490 AddFunction(ucr::toUTF16(funcname), CallUnpackFile);
493 virtual ~EditorScriptGeneratedFromUnpacker()
495 m_pDispatch->Release();
498 static HRESULT STDMETHODCALLTYPE CallUnpackFile(IDispatch *pDispatch, BSTR text, BSTR* pbstrResult)
501 String fileSrc = src.Create();
502 String fileDst = dst.Create();
503 HRESULT hr = WriteFile(fileSrc, text);
508 auto* thisObj = static_cast<EditorScriptGeneratedFromUnpacker*>(pDispatch);
509 auto* pInternalPlugin = dynamic_cast<InternalPlugin*>(thisObj->m_pDispatch);
512 BSTR bstrFileSrc = SysAllocString(ucr::toUTF16(fileSrc).c_str());
513 BSTR bstrFileDst= SysAllocString(ucr::toUTF16(fileDst).c_str());
514 VARIANT_BOOL bChanged;
515 VARIANT_BOOL bSuccess;
516 hr = pInternalPlugin->UnpackFile(bstrFileSrc, bstrFileDst, &bChanged, &subcode, &bSuccess);
517 SysFreeString(bstrFileSrc);
518 SysFreeString(bstrFileDst);
524 if (!plugin::InvokeUnpackFile(fileSrc, fileDst, changed, thisObj->m_pDispatch, subcode))
528 hr = ReadFile(fileDst, unpackedText);
531 *pbstrResult = SysAllocStringLen(ucr::toUTF16(unpackedText).c_str(),
532 static_cast<unsigned>(unpackedText.length()));
537 IDispatch* m_pDispatch;
544 CAllThreadsScripts::RegisterInternalPluginsLoader(&Load);
547 static bool Load(std::map<String, PluginArrayPtr>& plugins, String& errmsg)
549 if (plugins.find(L"EDITOR_SCRIPT") != plugins.end())
551 for (auto plugin : *plugins[L"EDITOR_SCRIPT"])
553 if (!plugin->m_disabled && plugin->GetExtendedPropertyValue(_T("GenerateUnpacker")).has_value())
555 std::vector<String> namesArray;
556 std::vector<int> idArray;
557 int validFuncs = plugin::GetMethodsFromScript(plugin->m_lpDispatch, namesArray, idArray);
558 for (int i = 0; i < validFuncs; ++i)
560 if (plugins.find(L"FILE_PACK_UNPACK") == plugins.end())
561 plugins[L"FILE_PACK_UNPACK"].reset(new PluginArray);
562 PluginInfoPtr pluginNew(new PluginInfo());
563 IDispatch* pDispatch = new UnpackerGeneratedFromEditorScript(*plugin, namesArray[i], idArray[i]);
565 pluginNew->MakeInfo(namesArray[i], pDispatch);
566 plugins[L"FILE_PACK_UNPACK"]->push_back(pluginNew);
572 std::list<Info> internalPlugins;
573 XMLHandler handler(&internalPlugins);
575 parser.setContentHandler(&handler);
578 for (const auto& path : {
579 paths::ConcatPath(env::GetProgPath(), _T("MergePlugins\\Plugins.xml")),
580 env::ExpandEnvironmentVariables(_T("%APPDATA%\\WinMerge\\MergePlugins\\Plugins.xml"))
585 parser.parse(ucr::toUTF8(path));
587 catch (Poco::FileNotFoundException&)
592 catch (Poco::XML::SAXParseException& e)
594 errmsg = ucr::toTString(e.message());
598 for (auto& info : internalPlugins)
600 String event = info.m_event;
601 String name = info.m_name;
602 if (plugins.find(event) == plugins.end())
603 plugins[event].reset(new PluginArray);
604 PluginInfoPtr pluginNew(new PluginInfo());
605 IDispatch* pDispatch = new InternalPlugin(std::move(info));
607 pluginNew->MakeInfo(name, pDispatch);
608 plugins[event]->push_back(pluginNew);
611 if (plugins.find(L"FILE_PACK_UNPACK") != plugins.end())
613 for (auto plugin : *plugins[L"FILE_PACK_UNPACK"])
615 if (!plugin->m_disabled && plugin->GetExtendedPropertyValue(_T("GenerateEditorScript")).has_value())
617 if (plugins.find(L"EDITOR_SCRIPT") == plugins.end())
618 plugins[L"EDITOR_SCRIPT"].reset(new PluginArray);
619 PluginInfoPtr pluginNew(new PluginInfo());
620 auto menuCaption = plugin->GetExtendedPropertyValue(_T("MenuCaption"));
621 String funcname = menuCaption.has_value() ? String{menuCaption->data(), menuCaption->length() } : plugin->m_name;
622 IDispatch* pDispatch = new EditorScriptGeneratedFromUnpacker(*plugin, funcname);
624 pluginNew->MakeInfo(plugin->m_name, pDispatch);
625 plugins[L"EDITOR_SCRIPT"]->push_back(pluginNew);