1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
5 // SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
8 * @file FileTransform.cpp
10 * @brief Implementation of file transformations
14 #include "FileTransform.h"
16 #include <Poco/Exception.h>
17 #include <Poco/Mutex.h>
19 #include "multiformatText.h"
20 #include "Environment.h"
25 using Poco::Exception;
27 static Poco::FastMutex g_mutex;
29 ////////////////////////////////////////////////////////////////////////////////
30 // transformations : packing unpacking
33 std::vector<PluginForFile::PipelineItem> PluginForFile::ParsePluginPipeline(String& errorMessage) const
35 return ParsePluginPipeline(m_PluginPipeline, errorMessage);
38 std::vector<PluginForFile::PipelineItem> PluginForFile::ParsePluginPipeline(const String& pluginPipeline, String& errorMessage)
40 std::vector<PluginForFile::PipelineItem> result;
41 bool inQuotes = false;
42 tchar_t quoteChar = 0;
43 std::vector<String> args;
46 const tchar_t* p = pluginPipeline.c_str();
47 while (tc::istspace(*p)) p++;
55 if (*p == '"' || *p == '\'')
60 else if (tc::istspace(*p))
77 if (*(p + 1) == quoteChar)
100 args.push_back(token);
102 while (tc::istspace(*p)) p++;
108 if (sep == '|' || !*p)
110 if (name.empty() || (sep == '|' && !*p))
112 errorMessage = strutils::format_string1(_("Missing plugin name in plugin pipeline: %1"), pluginPipeline);
115 result.push_back({ name, args, quoteChar });
122 errorMessage = strutils::format_string1(_("Missing quotation mark in plugin pipeline: %1"), pluginPipeline);
126 String PluginForFile::MakePluginPipeline(const std::vector<PluginForFile::PipelineItem>& list)
130 for (const auto& [name, args, quoteChar] : list)
132 if (quoteChar && name.find_first_of(_T(" '\"")) != String::npos)
134 String nameQuoted = name;
135 strutils::replace(nameQuoted, String(1, quoteChar), String(2, quoteChar));
136 pipeline += strutils::format(_T("%c%s%c"), quoteChar, nameQuoted, quoteChar);
144 for (const auto& arg : args)
148 String argQuoted = arg;
149 strutils::replace(argQuoted, String(1, quoteChar), String(2, quoteChar));
150 pipeline += strutils::format(_T(" %c%s%c"), quoteChar, argQuoted, quoteChar);
154 pipeline += _T(" ") + arg;
158 if (i < list.size() - 1)
165 String PluginForFile::MakeArguments(const std::vector<String>& args, const std::vector<StringView>& variables)
169 for (const auto& arg : args)
172 for (const tchar_t* p = arg.c_str(); *p; ++p)
174 if (*p == '%' && *(p + 1) != 0)
182 else if (c >= '1' && c <= '9')
184 if ((c - '1') < variables.size())
185 newarg += strutils::to_str(variables[(c - '1')]);
198 if (newarg.find_first_of(_T(" \"")) != String::npos)
200 strutils::replace(newarg, _T("\""), _T("\"\""));
201 newstr += _T("\"") + newarg + _T("\"");
207 if (i < args.size() - 1)
214 bool PackingInfo::GetPackUnpackPlugin(const String& filteredFilenames, bool bUrl, bool bReverse,
215 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>>& plugins,
216 String *pPluginPipelineResolved, String *pURLHandlerResolved, String& errorMessage) const
218 auto result = ParsePluginPipeline(errorMessage);
219 if (!errorMessage.empty())
223 std::vector<String> args;
224 bool bWithFile = true;
225 PluginInfo* plugin = nullptr;
226 if (m_URLHandler.empty())
227 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"URL_PACK_UNPACK", filteredFilenames);
229 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"URL_PACK_UNPACK", m_URLHandler);
231 plugins.push_back({ plugin, args, bWithFile });
232 if (pURLHandlerResolved)
233 *pURLHandlerResolved = plugin ? plugin->m_name : _T("");
235 std::vector<PluginForFile::PipelineItem> pipelineResolved;
236 for (auto& [pluginName, args, quoteChar] : result)
238 PluginInfo* plugin = nullptr;
239 bool bWithFile = true;
240 if (pluginName == _T("<None>") || pluginName == _("<None>"))
242 else if (pluginName == _T("<Automatic>") || pluginName == _("<Automatic>"))
244 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"FILE_PACK_UNPACK", filteredFilenames);
245 if (plugin == nullptr)
246 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"FILE_FOLDER_PACK_UNPACK", filteredFilenames);
247 if (plugin == nullptr)
249 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"BUFFER_PACK_UNPACK", filteredFilenames);
255 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"FILE_PACK_UNPACK", pluginName);
256 if (plugin == nullptr)
258 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"FILE_FOLDER_PACK_UNPACK", pluginName);
259 if (plugin == nullptr)
261 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"BUFFER_PACK_UNPACK", pluginName);
262 if (plugin == nullptr)
264 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(nullptr, pluginName);
265 if (plugin == nullptr)
267 errorMessage = strutils::format_string1(_("Plugin not found or invalid: %1"), pluginName);
272 errorMessage = strutils::format_string1(_("'%1' is not unpacker plugin"), pluginName);
282 pipelineResolved.push_back({plugin->m_name, args, quoteChar });
284 plugins.insert(plugins.begin(), { plugin, args, bWithFile });
286 plugins.push_back({ plugin, args, bWithFile });
289 if (pPluginPipelineResolved)
290 *pPluginPipelineResolved = MakePluginPipeline(pipelineResolved);
295 bool PackingInfo::pack(String & filepath, const String& dstFilepath, const std::vector<int>& handlerSubcodes, const std::vector<StringView>& variables) const
297 // no handler : return true
298 bool bUrl = paths::IsURL(dstFilepath);
299 if (m_PluginPipeline.empty() && !bUrl)
304 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>> plugins;
305 if (!GetPackUnpackPlugin(_T(""), bUrl, true, plugins, nullptr, nullptr, errorMessage))
307 AppErrorMessageBox(errorMessage);
311 if (m_bWebBrowser && m_PluginPipeline.empty())
314 auto itSubcode = handlerSubcodes.rbegin();
315 for (auto& [plugin, args, bWithFile] : plugins)
317 bool bHandled = false;
318 storageForPlugins bufferData;
319 bufferData.SetDataFileAnsi(filepath);
321 LPDISPATCH piScript = plugin->m_lpDispatch;
322 Poco::FastMutex::ScopedLock lock(g_mutex);
324 if (plugin->m_hasVariablesProperty)
326 if (!plugin::InvokePutPluginVariables(strutils::to_str(variables[0]), piScript))
329 if (plugin->m_hasArgumentsProperty)
331 if (!plugin::InvokePutPluginArguments(args.empty() ? plugin->m_arguments : MakeArguments(args, variables), piScript))
337 // use a temporary dest name
338 String srcFileName = bufferData.GetDataFileAnsi(); // <-Call order is important
339 String dstFileName = plugin->m_event == L"URL_PACK_UNPACK" ?
340 dstFilepath : bufferData.GetDestFileName(); // <-Call order is important
341 bHandled = plugin::InvokePackFile(srcFileName,
343 bufferData.GetNChanged(),
344 piScript, *itSubcode);
346 bufferData.ValidateNewFile();
350 bHandled = plugin::InvokePackBuffer(*bufferData.GetDataBufferAnsi(),
351 bufferData.GetNChanged(),
352 piScript, *itSubcode);
354 bufferData.ValidateNewBuffer();
357 // if this packer does not work, that is an error
361 // if the buffer changed, write it before leaving
362 if (bufferData.GetNChangedValid() > 0)
364 bool bSuccess = bufferData.SaveAsFile(filepath);
373 bool PackingInfo::Packing(const String& srcFilepath, const String& dstFilepath, const std::vector<int>& handlerSubcodes, const std::vector<StringView>& variables) const
375 String csTempFileName = srcFilepath;
376 if (!pack(csTempFileName, dstFilepath, handlerSubcodes, variables))
380 if (!paths::IsURL(dstFilepath))
382 TFile file1(csTempFileName);
383 file1.copyTo(dstFilepath);
384 if (srcFilepath != csTempFileName)
389 catch (Poco::Exception& e)
391 DWORD dwErrCode = GetLastError();
392 LogErrorStringUTF8(e.displayText());
393 SetLastError(dwErrCode);
398 bool PackingInfo::Unpacking(std::vector<int> * handlerSubcodes, String & filepath, const String& filteredText, const std::vector<StringView>& variables)
401 handlerSubcodes->clear();
403 // no handler : return true
404 bool bUrl = paths::IsURL(filepath);
405 if (m_PluginPipeline.empty() && !bUrl)
410 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>> plugins;
411 if (!GetPackUnpackPlugin(filteredText, bUrl, false, plugins, &m_PluginPipeline, &m_URLHandler, errorMessage))
413 AppErrorMessageBox(errorMessage);
417 if (m_bWebBrowser && m_PluginPipeline.empty())
420 for (auto& [plugin, args, bWithFile] : plugins)
422 bool bHandled = false;
423 storageForPlugins bufferData;
424 bufferData.SetDataFileAnsi(filepath);
429 LPDISPATCH piScript = plugin->m_lpDispatch;
430 Poco::FastMutex::ScopedLock lock(g_mutex);
432 if (plugin->m_hasVariablesProperty)
434 if (!plugin::InvokePutPluginVariables(strutils::to_str(variables[0]), piScript))
437 if (plugin->m_hasArgumentsProperty)
439 if (!plugin::InvokePutPluginArguments(args.empty() ? plugin->m_arguments : MakeArguments(args, variables), piScript))
445 // use a temporary dest name
446 bufferData.SetDestFileExtension(!plugin->m_ext.empty() ? plugin->m_ext : paths::FindExtension(filepath));
447 String srcFileName = bufferData.GetDataFileAnsi(); // <-Call order is important
448 String dstFileName = bufferData.GetDestFileName(); // <-Call order is important
449 bHandled = plugin::InvokeUnpackFile(srcFileName,
451 bufferData.GetNChanged(),
454 bufferData.ValidateNewFile();
458 bHandled = plugin::InvokeUnpackBuffer(*bufferData.GetDataBufferAnsi(),
459 bufferData.GetNChanged(),
462 bufferData.ValidateNewBuffer();
465 // if this unpacker does not work, that is an error
471 handlerSubcodes->push_back(subcode);
473 // if the buffer changed, write it before leaving
474 if (bufferData.GetNChangedValid() > 0)
476 bool bSuccess = bufferData.SaveAsFile(filepath);
484 String PackingInfo::GetUnpackedFileExtension(const String& filteredFilenames, int& preferredWindowType) const
486 preferredWindowType = -1;
489 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>> plugins;
490 if (GetPackUnpackPlugin(filteredFilenames, false, false, plugins, nullptr, nullptr, errorMessage))
492 for (auto& [plugin, args, bWithFile] : plugins)
494 ext += plugin->m_ext;
495 auto preferredWindowTypeStr = plugin->GetExtendedPropertyValue(_T("PreferredWindowType"));
496 if (preferredWindowTypeStr.has_value())
498 if (preferredWindowTypeStr == L"Text")
499 preferredWindowType = 0;
500 else if (preferredWindowTypeStr == L"Table")
501 preferredWindowType = 1;
502 else if (preferredWindowTypeStr == L"Binary")
503 preferredWindowType = 2;
504 else if (preferredWindowTypeStr == L"Image")
505 preferredWindowType = 3;
506 else if (preferredWindowTypeStr == L"Webpage")
507 preferredWindowType = 4;
514 ////////////////////////////////////////////////////////////////////////////////
515 // transformation prediffing
517 bool PrediffingInfo::GetPrediffPlugin(const String& filteredFilenames, bool bReverse,
518 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>>& plugins,
519 String *pPluginPipelineResolved, String& errorMessage) const
521 auto result = ParsePluginPipeline(errorMessage);
522 if (!errorMessage.empty())
524 std::vector<PluginForFile::PipelineItem> pipelineResolved;
525 for (auto& [pluginName, args, quoteChar] : result)
527 PluginInfo* plugin = nullptr;
528 bool bWithFile = true;
529 if (pluginName == _T("<None>") || pluginName == _("<None>"))
531 else if (pluginName == _T("<Automatic>") || pluginName == _("<Automatic>"))
533 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"FILE_PREDIFF", filteredFilenames);
534 if (plugin == nullptr)
536 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"BUFFER_PREDIFF", filteredFilenames);
543 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"FILE_PREDIFF", pluginName);
544 if (plugin == nullptr)
546 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"BUFFER_PREDIFF", pluginName);
547 if (plugin == nullptr)
549 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(nullptr, pluginName);
550 if (plugin == nullptr)
552 errorMessage = strutils::format_string1(_("Plugin not found or invalid: %1"), pluginName);
556 errorMessage = strutils::format_string1(_("'%1' is not prediffer plugin"), pluginName);
565 pipelineResolved.push_back({ plugin->m_name, args, quoteChar });
567 plugins.insert(plugins.begin(), { plugin, args, bWithFile });
569 plugins.push_back({ plugin, args, bWithFile });
572 if (pPluginPipelineResolved)
573 *pPluginPipelineResolved = MakePluginPipeline(pipelineResolved);
577 bool PrediffingInfo::Prediffing(String & filepath, const String& filteredText, bool bMayOverwrite, const std::vector<StringView>& variables)
579 // no handler : return true
580 if (m_PluginPipeline.empty())
584 bool bHandled = false;
586 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>> plugins;
587 if (!GetPrediffPlugin(filteredText, false, plugins, &m_PluginPipeline, errorMessage))
589 AppErrorMessageBox(errorMessage);
593 for (const auto& [plugin, args, bWithFile] : plugins)
595 storageForPlugins bufferData;
596 // detect Ansi or Unicode file
597 bufferData.SetDataFileUnknown(filepath, bMayOverwrite);
598 // TODO : set the codepage
599 // bufferData.SetCodepage();
601 LPDISPATCH piScript = plugin->m_lpDispatch;
602 Poco::FastMutex::ScopedLock lock(g_mutex);
604 if (plugin->m_hasVariablesProperty)
606 if (!plugin::InvokePutPluginVariables(strutils::to_str(variables[0]), piScript))
609 if (plugin->m_hasArgumentsProperty)
611 if (!plugin::InvokePutPluginArguments(args.empty() ? plugin->m_arguments : MakeArguments(args, variables), piScript))
617 // use a temporary dest name
618 String srcFileName = bufferData.GetDataFileAnsi(); // <-Call order is important
619 String dstFileName = bufferData.GetDestFileName(); // <-Call order is important
620 bHandled = plugin::InvokePrediffFile(srcFileName,
622 bufferData.GetNChanged(),
625 bufferData.ValidateNewFile();
629 // probably it is for VB/VBscript so use a BSTR as argument
630 bHandled = plugin::InvokePrediffBuffer(*bufferData.GetDataBufferUnicode(),
631 bufferData.GetNChanged(),
634 bufferData.ValidateNewBuffer();
637 // if this unpacker does not work, that is an error
641 // if the buffer changed, write it before leaving
642 if (bufferData.GetNChangedValid() > 0)
644 // bufferData changes filepath here to temp filepath
645 bool bSuccess = bufferData.SaveAsFile(filepath);
653 ////////////////////////////////////////////////////////////////////////////////
654 // transformation text
656 bool EditorScriptInfo::GetEditorScriptPlugin(std::vector<std::tuple<PluginInfo*, std::vector<String>, int>>& plugins,
657 String& errorMessage) const
659 auto result = ParsePluginPipeline(errorMessage);
660 if (!errorMessage.empty())
662 for (auto& [pluginName, args, quoteChar] : result)
665 PluginArray *pluginInfoArray = CAllThreadsScripts::GetActiveSet()->GetAvailableScripts(L"EDITOR_SCRIPT");
666 for (const auto& plugin : *pluginInfoArray)
668 std::vector<String> namesArray;
669 std::vector<int> idArray;
670 int nFunc = plugin::GetMethodsFromScript(plugin->m_lpDispatch, namesArray, idArray);
671 for (int i = 0; i < nFunc; ++i)
673 if (namesArray[i] == pluginName)
675 plugins.push_back({ plugin.get(), args, idArray[i] });
685 errorMessage = strutils::format_string1(_("Plugin not found or invalid: %1"), pluginName);
692 bool EditorScriptInfo::TransformText(String & text, const std::vector<StringView>& variables, bool& changed)
695 // no handler : return true
696 if (m_PluginPipeline.empty())
701 std::vector<std::tuple<PluginInfo*, std::vector<String>, int>> plugins;
702 if (!GetEditorScriptPlugin(plugins, errorMessage))
704 AppErrorMessageBox(errorMessage);
708 for (const auto& [plugin, args, fncID] : plugins)
710 LPDISPATCH piScript = plugin->m_lpDispatch;
711 Poco::FastMutex::ScopedLock lock(g_mutex);
713 if (plugin->m_hasVariablesProperty)
715 if (!plugin::InvokePutPluginVariables(strutils::to_str(variables[0]), piScript))
718 if (plugin->m_hasArgumentsProperty)
720 if (!plugin::InvokePutPluginArguments(args.empty() ? plugin->m_arguments : MakeArguments(args, variables), piScript))
724 // execute the transform operation
726 if (!plugin::InvokeTransformText(text, nChanged, plugin->m_lpDispatch, fncID))
729 changed = (nChanged != 0);
734 namespace FileTransform
737 bool AutoUnpacking = false;
738 bool AutoPrediffing = false;
740 ////////////////////////////////////////////////////////////////////////////////
742 bool AnyCodepageToUTF8(int codepage, String & filepath, bool bMayOverwrite)
744 String tempDir = env::GetTemporaryPath();
747 String tempFilepath = env::GetTemporaryFileName(tempDir, _T("_W3"));
748 if (tempFilepath.empty())
750 // TODO : is it better with the BOM or without (just change the last argument)
751 int nFileChanged = 0;
752 bool bSuccess = ::AnyCodepageToUTF8(codepage, filepath, tempFilepath, nFileChanged, false);
753 if (bSuccess && nFileChanged!=0)
755 // we do not overwrite so we delete the old file
760 TFile(filepath).remove();
764 LogErrorStringUTF8(e.displayText());
767 // and change the filepath if everything works
768 filepath = tempFilepath;
774 TFile(tempFilepath).remove();
778 LogErrorStringUTF8(e.displayText());
786 std::vector<std::tuple<String, String, unsigned, PluginInfo *>>,
787 std::map<String, std::vector<std::tuple<String, String, unsigned, PluginInfo *>>>
789 CreatePluginMenuInfos(const String& filteredFilenames, const std::vector<std::wstring>& events, unsigned baseId)
791 std::vector<std::tuple<String, String, unsigned, PluginInfo *>> suggestedPlugins;
792 std::map<String, std::vector<std::tuple<String, String, unsigned, PluginInfo *>>> allPlugins;
793 std::map<String, int> captions;
794 unsigned id = baseId;
795 bool addedNoneAutomatic = false;
796 static PluginInfo noPlugin;
797 static PluginInfo autoPlugin;
798 if (autoPlugin.m_name.empty())
799 autoPlugin.m_name = _T("<Automatic>");
800 for (const auto& event: events)
803 CAllThreadsScripts::GetActiveSet()->GetAvailableScripts(event.c_str());
804 for (auto& plugin : *pScriptArray)
806 if (!plugin->m_disabled)
808 if (event != L"EDITOR_SCRIPT")
810 if (!addedNoneAutomatic)
812 String process = _T("");
813 allPlugins.insert_or_assign(process, std::vector<std::tuple<String, String, unsigned, PluginInfo *>>());
814 allPlugins[process].emplace_back(_("<None>"), _T(""), id++, &noPlugin);
815 allPlugins[process].emplace_back(_("<Automatic>"), _T("<Automatic>"), id++, &autoPlugin);
816 addedNoneAutomatic = true;
818 const auto menuCaption = plugin->GetExtendedPropertyValue(_T("MenuCaption"));
819 const auto processType = plugin->GetExtendedPropertyValue(_T("ProcessType"));
820 const String caption = tr(ucr::toUTF8(menuCaption.has_value() ?
821 strutils::to_str(*menuCaption) : plugin->m_name));
822 const String process = tr(ucr::toUTF8(processType.has_value() ?
823 strutils::to_str(*processType) : _T("&Others")));
825 if (plugin->TestAgainstRegList(filteredFilenames))
826 suggestedPlugins.emplace_back(caption, plugin->m_name, id, plugin.get());
828 if (allPlugins.find(process) == allPlugins.end())
829 allPlugins.insert_or_assign(process, std::vector<std::tuple<String, String, unsigned, PluginInfo *>>());
830 allPlugins[process].emplace_back(caption, plugin->m_name, id, plugin.get());
836 LPDISPATCH piScript = plugin->m_lpDispatch;
837 std::vector<String> scriptNamesArray;
838 std::vector<int> scriptIdsArray;
839 int nScriptFnc = plugin::GetMethodsFromScript(piScript, scriptNamesArray, scriptIdsArray);
840 bool matched = plugin->TestAgainstRegList(filteredFilenames);
841 for (int i = 0; i < nScriptFnc; ++i, ++id)
843 if (scriptNamesArray[i] == L"PluginOnEvent")
845 const auto menuCaption = plugin->GetExtendedPropertyValue(scriptNamesArray[i] + _T(".MenuCaption"));
846 auto processType = plugin->GetExtendedPropertyValue(scriptNamesArray[i] + _T(".ProcessType"));
847 if (!processType.has_value())
848 processType = plugin->GetExtendedPropertyValue(_T("ProcessType"));
849 const String caption = tr(ucr::toUTF8(menuCaption.has_value() ?
850 strutils::to_str(*menuCaption) : scriptNamesArray[i]));
851 const String process = tr(ucr::toUTF8(processType.has_value() ?
852 strutils::to_str(*processType) : _T("&Others")));
854 suggestedPlugins.emplace_back(caption, scriptNamesArray[i], id, plugin.get());
855 if (allPlugins.find(process) == allPlugins.end())
856 allPlugins.insert_or_assign(process, std::vector<std::tuple<String, String, unsigned, PluginInfo *>>());
857 allPlugins[process].emplace_back(caption, scriptNamesArray[i], id, plugin.get());
863 auto ResolveConflictMenuCaptions = [&captions](auto& plugins)
865 for (auto& plugin : plugins)
867 const String& caption = std::get<0>(plugin);
868 if (captions[caption] > 1)
869 std::get<0>(plugin) = caption + _T("(") + std::get<1>(plugin) + _T(")");
872 ResolveConflictMenuCaptions(suggestedPlugins);
873 for (auto& [processType, plugins] : allPlugins)
874 ResolveConflictMenuCaptions(plugins);
875 return { suggestedPlugins, allPlugins };
880 ////////////////////////////////////////////////////////////////////////////////