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;
43 std::vector<String> args;
46 const TCHAR* p = pluginPipeline.c_str();
47 while (_istspace(*p)) p++;
55 if (*p == '"' || *p == '\'')
60 else if (_istspace(*p))
77 if (*(p + 1) == quoteChar)
100 args.push_back(token);
102 while (_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* 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 += String{ variables[(c - '1')].data(), variables[(c - '1')].length() };
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 bReverse,
215 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>>& plugins,
216 String *pPluginPipelineResolved, String& errorMessage) const
218 auto result = ParsePluginPipeline(errorMessage);
219 if (!errorMessage.empty())
221 std::vector<PluginForFile::PipelineItem> pipelineResolved;
222 for (auto& [pluginName, args, quoteChar] : result)
224 PluginInfo* plugin = nullptr;
225 bool bWithFile = true;
226 if (pluginName == _T("<None>") || pluginName == _("<None>"))
228 else if (pluginName == _T("<Automatic>") || pluginName == _("<Automatic>"))
230 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"FILE_PACK_UNPACK", filteredFilenames);
231 if (plugin == nullptr)
232 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"FILE_FOLDER_PACK_UNPACK", filteredFilenames);
233 if (plugin == nullptr)
235 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"BUFFER_PACK_UNPACK", filteredFilenames);
241 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"FILE_PACK_UNPACK", pluginName);
242 if (plugin == nullptr)
244 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"FILE_FOLDER_PACK_UNPACK", pluginName);
245 if (plugin == nullptr)
247 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"BUFFER_PACK_UNPACK", pluginName);
248 if (plugin == nullptr)
250 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(nullptr, pluginName);
251 if (plugin == nullptr)
253 errorMessage = strutils::format_string1(_("Plugin not found or invalid: %1"), pluginName);
258 errorMessage = strutils::format(_T("'%s' is not PACK_UNPACK plugin"), pluginName);
268 pipelineResolved.push_back({plugin->m_name, args, quoteChar });
270 plugins.insert(plugins.begin(), { plugin, args, bWithFile });
272 plugins.push_back({ plugin, args, bWithFile });
275 if (pPluginPipelineResolved)
276 *pPluginPipelineResolved = MakePluginPipeline(pipelineResolved);
281 bool PackingInfo::Packing(String & filepath, const std::vector<int>& handlerSubcodes, const std::vector<StringView>& variables) const
283 // no handler : return true
284 if (m_PluginPipeline.empty())
289 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>> plugins;
290 if (!GetPackUnpackPlugin(_T(""), true, plugins, nullptr, errorMessage))
292 AppErrorMessageBox(errorMessage);
296 auto itSubcode = handlerSubcodes.rbegin();
297 for (auto& [plugin, args, bWithFile] : plugins)
299 bool bHandled = false;
300 storageForPlugins bufferData;
301 bufferData.SetDataFileAnsi(filepath);
303 LPDISPATCH piScript = plugin->m_lpDispatch;
304 Poco::FastMutex::ScopedLock lock(g_mutex);
306 if (plugin->m_hasVariablesProperty)
308 if (!plugin::InvokePutPluginVariables(String(variables[0].data(), variables[0].length()), piScript))
311 if (plugin->m_hasArgumentsProperty)
313 if (!plugin::InvokePutPluginArguments(args.empty() ? plugin->m_arguments : MakeArguments(args, variables), piScript))
319 // use a temporary dest name
320 String srcFileName = bufferData.GetDataFileAnsi(); // <-Call order is important
321 String dstFileName = bufferData.GetDestFileName(); // <-Call order is important
322 bHandled = plugin::InvokePackFile(srcFileName,
324 bufferData.GetNChanged(),
325 piScript, *itSubcode);
327 bufferData.ValidateNewFile();
331 bHandled = plugin::InvokePackBuffer(*bufferData.GetDataBufferAnsi(),
332 bufferData.GetNChanged(),
333 piScript, *itSubcode);
335 bufferData.ValidateNewBuffer();
338 // if this packer does not work, that is an error
342 // if the buffer changed, write it before leaving
343 if (bufferData.GetNChangedValid() > 0)
345 bool bSuccess = bufferData.SaveAsFile(filepath);
354 bool PackingInfo::Packing(const String& srcFilepath, const String& dstFilepath, const std::vector<int>& handlerSubcodes, const std::vector<StringView>& variables) const
356 String csTempFileName = srcFilepath;
357 if (!Packing(csTempFileName, handlerSubcodes, variables))
361 TFile file1(csTempFileName);
362 file1.copyTo(dstFilepath);
363 if (srcFilepath!= csTempFileName)
367 catch (Poco::Exception& e)
369 LogErrorStringUTF8(e.displayText());
374 bool PackingInfo::Unpacking(std::vector<int> * handlerSubcodes, String & filepath, const String& filteredText, const std::vector<StringView>& variables)
376 // no handler : return true
377 if (m_PluginPipeline.empty())
382 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>> plugins;
383 if (!GetPackUnpackPlugin(filteredText, false, plugins, &m_PluginPipeline, errorMessage))
385 AppErrorMessageBox(errorMessage);
389 for (auto& [plugin, args, bWithFile] : plugins)
391 if (plugin->m_argumentsRequired && args.empty())
398 handlerSubcodes->clear();
400 for (auto& [plugin, args, bWithFile] : plugins)
402 bool bHandled = false;
403 storageForPlugins bufferData;
404 bufferData.SetDataFileAnsi(filepath);
409 LPDISPATCH piScript = plugin->m_lpDispatch;
410 Poco::FastMutex::ScopedLock lock(g_mutex);
412 if (plugin->m_hasVariablesProperty)
414 if (!plugin::InvokePutPluginVariables(String(variables[0].data(), variables[0].length()), piScript))
417 if (plugin->m_hasArgumentsProperty)
419 if (!plugin::InvokePutPluginArguments(args.empty() ? plugin->m_arguments : MakeArguments(args, variables), piScript))
425 // use a temporary dest name
426 bufferData.SetDestFileExtension(!plugin->m_ext.empty() ? plugin->m_ext : paths::FindExtension(filepath));
427 String srcFileName = bufferData.GetDataFileAnsi(); // <-Call order is important
428 String dstFileName = bufferData.GetDestFileName(); // <-Call order is important
429 bHandled = plugin::InvokeUnpackFile(srcFileName,
431 bufferData.GetNChanged(),
434 bufferData.ValidateNewFile();
438 bHandled = plugin::InvokeUnpackBuffer(*bufferData.GetDataBufferAnsi(),
439 bufferData.GetNChanged(),
442 bufferData.ValidateNewBuffer();
445 // if this unpacker does not work, that is an error
451 handlerSubcodes->push_back(subcode);
453 // if the buffer changed, write it before leaving
454 if (bufferData.GetNChangedValid() > 0)
456 bool bSuccess = bufferData.SaveAsFile(filepath);
464 String PackingInfo::GetUnpackedFileExtension(const String& filteredFilenames) const
468 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>> plugins;
469 if (GetPackUnpackPlugin(filteredFilenames, false, plugins, nullptr, errorMessage))
471 for (auto& [plugin, args, bWithFile] : plugins)
472 ext += plugin->m_ext;
477 ////////////////////////////////////////////////////////////////////////////////
478 // transformation prediffing
480 bool PrediffingInfo::GetPrediffPlugin(const String& filteredFilenames, bool bReverse,
481 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>>& plugins,
482 String *pPluginPipelineResolved, String& errorMessage) const
484 auto result = ParsePluginPipeline(errorMessage);
485 if (!errorMessage.empty())
487 std::vector<PluginForFile::PipelineItem> pipelineResolved;
488 for (auto& [pluginName, args, quoteChar] : result)
490 PluginInfo* plugin = nullptr;
491 bool bWithFile = true;
492 if (pluginName == _T("<None>") || pluginName == _("<None>"))
494 else if (pluginName == _T("<Automatic>") || pluginName == _("<Automatic>"))
496 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"FILE_PREDIFF", filteredFilenames);
497 if (plugin == nullptr)
499 plugin = CAllThreadsScripts::GetActiveSet()->GetAutomaticPluginByFilter(L"BUFFER_PREDIFF", filteredFilenames);
506 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"FILE_PREDIFF", pluginName);
507 if (plugin == nullptr)
509 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(L"BUFFER_PREDIFF", pluginName);
510 if (plugin == nullptr)
512 plugin = CAllThreadsScripts::GetActiveSet()->GetPluginByName(nullptr, pluginName);
513 if (plugin == nullptr)
515 errorMessage = strutils::format_string1(_("Plugin not found or invalid: %1"), pluginName);
519 errorMessage = strutils::format(_T("'%s' is not PREDIFF plugin"), pluginName);
528 pipelineResolved.push_back({ plugin->m_name, args, quoteChar });
530 plugins.insert(plugins.begin(), { plugin, args, bWithFile });
532 plugins.push_back({ plugin, args, bWithFile });
535 if (pPluginPipelineResolved)
536 *pPluginPipelineResolved = MakePluginPipeline(pipelineResolved);
540 bool PrediffingInfo::Prediffing(String & filepath, const String& filteredText, bool bMayOverwrite, const std::vector<StringView>& variables)
542 // no handler : return true
543 if (m_PluginPipeline.empty())
547 bool bHandled = false;
549 std::vector<std::tuple<PluginInfo*, std::vector<String>, bool>> plugins;
550 if (!GetPrediffPlugin(filteredText, false, plugins, &m_PluginPipeline, errorMessage))
552 AppErrorMessageBox(errorMessage);
556 for (const auto& [plugin, args, bWithFile] : plugins)
558 storageForPlugins bufferData;
559 // detect Ansi or Unicode file
560 bufferData.SetDataFileUnknown(filepath, bMayOverwrite);
561 // TODO : set the codepage
562 // bufferData.SetCodepage();
564 LPDISPATCH piScript = plugin->m_lpDispatch;
565 Poco::FastMutex::ScopedLock lock(g_mutex);
567 if (plugin->m_hasVariablesProperty)
569 if (!plugin::InvokePutPluginVariables(String(variables[0].data(), variables[0].length()), piScript))
572 if (plugin->m_hasArgumentsProperty)
574 if (!plugin::InvokePutPluginArguments(args.empty() ? plugin->m_arguments : MakeArguments(args, variables), piScript))
580 // use a temporary dest name
581 String srcFileName = bufferData.GetDataFileAnsi(); // <-Call order is important
582 String dstFileName = bufferData.GetDestFileName(); // <-Call order is important
583 bHandled = plugin::InvokePrediffFile(srcFileName,
585 bufferData.GetNChanged(),
588 bufferData.ValidateNewFile();
592 // probably it is for VB/VBscript so use a BSTR as argument
593 bHandled = plugin::InvokePrediffBuffer(*bufferData.GetDataBufferUnicode(),
594 bufferData.GetNChanged(),
597 bufferData.ValidateNewBuffer();
600 // if this unpacker does not work, that is an error
604 // if the buffer changed, write it before leaving
605 if (bufferData.GetNChangedValid() > 0)
607 // bufferData changes filepath here to temp filepath
608 bool bSuccess = bufferData.SaveAsFile(filepath);
616 namespace FileTransform
619 bool AutoUnpacking = false;
620 bool AutoPrediffing = false;
622 ////////////////////////////////////////////////////////////////////////////////
624 bool AnyCodepageToUTF8(int codepage, String & filepath, bool bMayOverwrite)
626 String tempDir = env::GetTemporaryPath();
629 String tempFilepath = env::GetTemporaryFileName(tempDir, _T("_W3"));
630 if (tempFilepath.empty())
632 // TODO : is it better with the BOM or without (just change the last argument)
633 int nFileChanged = 0;
634 bool bSuccess = ::AnyCodepageToUTF8(codepage, filepath, tempFilepath, nFileChanged, false);
635 if (bSuccess && nFileChanged!=0)
637 // we do not overwrite so we delete the old file
642 TFile(filepath).remove();
646 LogErrorStringUTF8(e.displayText());
649 // and change the filepath if everything works
650 filepath = tempFilepath;
656 TFile(tempFilepath).remove();
660 LogErrorStringUTF8(e.displayText());
668 ////////////////////////////////////////////////////////////////////////////////
669 // transformation : TextTransform_Interactive (editor scripts)
671 std::vector<String> GetFreeFunctionsInScripts(const wchar_t *TransformationEvent)
673 std::vector<String> sNamesArray;
675 // get an array with the available scripts
676 PluginArray * piScriptArray =
677 CAllThreadsScripts::GetActiveSet()->GetAvailableScripts(TransformationEvent);
679 // fill in these structures
682 for (iScript = 0 ; iScript < piScriptArray->size() ; iScript++)
684 const PluginInfoPtr & plugin = piScriptArray->at(iScript);
685 if (plugin->m_disabled)
687 LPDISPATCH piScript = plugin->m_lpDispatch;
688 std::vector<String> scriptNamesArray;
689 std::vector<int> scriptIdsArray;
690 int nScriptFnc = plugin::GetMethodsFromScript(piScript, scriptNamesArray, scriptIdsArray);
691 sNamesArray.resize(nFnc+nScriptFnc);
694 for (iFnc = 0 ; iFnc < nScriptFnc ; iFnc++)
695 sNamesArray[nFnc+iFnc] = scriptNamesArray[iFnc];
702 bool Interactive(String & text, const std::vector<String>& args, const wchar_t *TransformationEvent, int iFncChosen, const std::vector<StringView>& variables)
707 // get an array with the available scripts
708 PluginArray * piScriptArray =
709 CAllThreadsScripts::GetActiveSet()->GetAvailableScripts(TransformationEvent);
712 for (iScript = 0 ; iScript < piScriptArray->size() ; iScript++)
714 if (iFncChosen < piScriptArray->at(iScript)->m_nFreeFunctions)
715 // we have found the script file
717 iFncChosen -= piScriptArray->at(iScript)->m_nFreeFunctions;
720 if (iScript >= piScriptArray->size())
723 PluginInfo* plugin = piScriptArray->at(iScript).get();
725 // iFncChosen is the index of the function in the script file
726 // we must convert it to the function ID
727 int fncID = plugin::GetMethodIDInScript(plugin->m_lpDispatch, iFncChosen);
729 if (plugin->m_hasVariablesProperty)
731 if (!plugin::InvokePutPluginVariables(String(variables[0].data(), variables[0].length()), plugin->m_lpDispatch))
734 if (plugin->m_hasArgumentsProperty)
736 if (!plugin::InvokePutPluginArguments(args.empty() ? plugin->m_arguments : PluginForFile::MakeArguments(args, variables), plugin->m_lpDispatch))
740 // execute the transform operation
742 plugin::InvokeTransformText(text, nChanged, plugin->m_lpDispatch, fncID);
744 return (nChanged != 0);
748 std::vector<std::tuple<String, String, unsigned, PluginInfo *>>,
749 std::map<String, std::vector<std::tuple<String, String, unsigned, PluginInfo *>>>
751 CreatePluginMenuInfos(const String& filteredFilenames, const std::vector<std::wstring>& events, unsigned baseId)
753 std::vector<std::tuple<String, String, unsigned, PluginInfo *>> suggestedPlugins;
754 std::map<String, std::vector<std::tuple<String, String, unsigned, PluginInfo *>>> allPlugins;
755 std::map<String, int> captions;
756 unsigned id = baseId;
757 bool addedNoneAutomatic = false;
758 for (const auto& event: events)
761 CAllThreadsScripts::GetActiveSet()->GetAvailableScripts(event.c_str());
762 for (auto& plugin : *pScriptArray)
764 if (!plugin->m_disabled)
766 if (event != L"EDITOR_SCRIPT")
768 if (!addedNoneAutomatic)
770 String process = _T("");
771 allPlugins.insert_or_assign(process, std::vector<std::tuple<String, String, unsigned, PluginInfo *>>());
772 allPlugins[process].emplace_back(_("<None>"), _T(""), id++, plugin.get());
773 allPlugins[process].emplace_back(_("<Automatic>"), _T("<Automatic>"), id++, plugin.get());
774 addedNoneAutomatic = true;
776 const auto menuCaption = plugin->GetExtendedPropertyValue(_T("MenuCaption"));
777 const auto processType = plugin->GetExtendedPropertyValue(_T("ProcessType"));
778 const String caption = tr(ucr::toUTF8(menuCaption.has_value() ?
779 String{ menuCaption->data(), menuCaption->size() } : plugin->m_name));
780 const String process = tr(ucr::toUTF8(processType.has_value() ?
781 String{ processType->data(), processType->size() } : _T("&Others")));
783 if (plugin->TestAgainstRegList(filteredFilenames))
784 suggestedPlugins.emplace_back(caption, plugin->m_name, id, plugin.get());
786 if (allPlugins.find(process) == allPlugins.end())
787 allPlugins.insert_or_assign(process, std::vector<std::tuple<String, String, unsigned, PluginInfo *>>());
788 allPlugins[process].emplace_back(caption, plugin->m_name, id, plugin.get());
794 LPDISPATCH piScript = plugin->m_lpDispatch;
795 std::vector<String> scriptNamesArray;
796 std::vector<int> scriptIdsArray;
797 int nScriptFnc = plugin::GetMethodsFromScript(piScript, scriptNamesArray, scriptIdsArray);
798 bool matched = plugin->TestAgainstRegList(filteredFilenames);
799 for (int i = 0; i < nScriptFnc; ++i, ++id)
801 const auto menuCaption = plugin->GetExtendedPropertyValue(scriptNamesArray[i] + _T(".MenuCaption"));
802 auto processType = plugin->GetExtendedPropertyValue(scriptNamesArray[i] + _T(".ProcessType"));
803 if (!processType.has_value())
804 processType = plugin->GetExtendedPropertyValue(_T("ProcessType"));
805 const String caption = tr(ucr::toUTF8(menuCaption.has_value() ?
806 String{ menuCaption->data(), menuCaption->size() } : scriptNamesArray[i]));
807 const String process = tr(ucr::toUTF8(processType.has_value() ?
808 String{ processType->data(), processType->size() } : _T("&Others")));
810 suggestedPlugins.emplace_back(caption, plugin->m_name, id, plugin.get());
811 if (allPlugins.find(process) == allPlugins.end())
812 allPlugins.insert_or_assign(process, std::vector<std::tuple<String, String, unsigned, PluginInfo *>>());
813 allPlugins[process].emplace_back(caption, plugin->m_name, id, plugin.get());
819 auto ResolveConflictMenuCaptions = [&captions](auto& plugins)
821 for (auto& plugin : plugins)
823 const String& caption = std::get<0>(plugin);
824 if (captions[caption] > 1)
825 std::get<0>(plugin) = caption + _T("(") + std::get<1>(plugin) + _T(")");
828 ResolveConflictMenuCaptions(suggestedPlugins);
829 for (auto& [processType, plugins] : allPlugins)
830 ResolveConflictMenuCaptions(plugins);
831 return { suggestedPlugins, allPlugins };
836 ////////////////////////////////////////////////////////////////////////////////