Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import leaves ScriptName/File/Dir[Utf8] in an inconsistent state if an error occurs during the import #349

Open
osir3z opened this issue Apr 10, 2023 · 0 comments

Comments

@osir3z
Copy link

osir3z commented Apr 10, 2023

Issue

Consider the following example:

# import-bug\test2.avs
SetLogParams("stderr", LOG_DEBUG)

LogMsg(Format(e"ScriptName()=\"{0}\"", ScriptName()), LOG_DEBUG)
LogMsg(Format(e"ScriptFile()=\"{0}\"", ScriptFile()), LOG_DEBUG)
LogMsg(Format(e"ScriptDir()=\"{0}\"", ScriptDir()), LOG_DEBUG)
try {
  Import(ScriptDir() + "\lib\bom.avsi")
}
catch (error) {
  LogMsg(error, LOG_WARNING)
}
LogMsg(Format(e"ScriptName()=\"{0}\"", ScriptName()), LOG_DEBUG)
LogMsg(Format(e"ScriptFile()=\"{0}\"", ScriptFile()), LOG_DEBUG)
LogMsg(Format(e"ScriptDir()=\"{0}\"", ScriptDir()), LOG_DEBUG)
Import(ScriptDir() + "\lib\valid.avsi")

BlankClip()
return Last

which gives the following output:

---------------------------------------------------------------------
DEBUG: ScriptName()="C:\import-bug\test2.avs"
---------------------------------------------------------------------
DEBUG: ScriptFile()="test2.avs"
---------------------------------------------------------------------
DEBUG: ScriptDir()="C:\import-bug\"
---------------------------------------------------------------------
ERROR: Import: UTF-8 source files with BOM are not supported, re-save script with ANSI or UTF8 w/o BOM encoding! : "C:\import-bug\\lib\bom.avsi"
---------------------------------------------------------------------
ERROR: Import: UTF-8 source files with BOM are not supported, re-save script with ANSI or UTF8 w/o BOM encoding! : "C:\import-bug\\lib\bom.avsi"
(import-bug\test2.avs, line 8)
---------------------------------------------------------------------
WARNING: Import: UTF-8 source files with BOM are not supported, re-save script with ANSI or UTF8 w/o BOM encoding! : "C:\import-bug\\lib\bom.avsi"
(import-bug\test2.avs, line 8)
---------------------------------------------------------------------
DEBUG: ScriptName()="C:\import-bug\lib\bom.avsi"
---------------------------------------------------------------------
DEBUG: ScriptFile()="bom.avsi"
---------------------------------------------------------------------
DEBUG: ScriptDir()="C:\import-bug\lib\"
---------------------------------------------------------------------
ERROR: Import: couldn't open "C:\import-bug\lib\lib\valid.avsi"
---------------------------------------------------------------------
ERROR: Import: couldn't open "C:\import-bug\lib\lib\valid.avsi"
(import-bug\test2.avs, line 16)
avs2pipemod[error]: Import: couldn't open "C:\import-bug\lib\lib\valid.avsi"
(import-bug\test2.avs, line 16)

In the example, lib\valid.avsi exists and contains valid code, whilst lib\bom.avsi exists but contains a UTF-8 BOM which causes an error. As you can see, after the error importing lib\bom.avsi, all the ScriptName/File/Dir[Utf8] functions return values referring to lib\bom.avsi when they should refer to test2.avs.

Something similar occurs if you replace the Import line in the try/catch block above with:

Import(ScriptDir() + "\lib\valid.avsi", ScriptDir() + "noexist.avsi")

(where noexist.avsi doesn't exist), except this time the ScriptName/File/Dir[Utf8] functions end up referring to lib\valid.avsi.

The code and files for these examples can be found in import-bug.zip.

Cause

When run successfully, Import saves the ScriptName/File/Dir[Utf8] environment variables, imports the scripts (changing ScriptName/File/Dir[Utf8] as it goes), and then restores the original environment variables before returning. But the restore code is never executed if an error occurs.

AVSValue Import(AVSValue args, void*, IScriptEnvironment* env)
{
// called as s+ or s+[Utf8]b
const bool bHasUTF8param = args.IsArray() && args.ArraySize() == 2 && args[1].IsBool();
const bool bUtf8 = bHasUTF8param ? args[1].AsBool(false) : false;
args = args[0];
AVSValue result;
InternalEnvironment *envi = static_cast<InternalEnvironment*>(env);
const bool MainScript = (envi->IncrImportDepth() == 1);
AVSValue lastScriptName = env->GetVarDef("$ScriptName$");
AVSValue lastScriptFile = env->GetVarDef("$ScriptFile$");
AVSValue lastScriptDir = env->GetVarDef("$ScriptDir$");
AVSValue lastScriptNameUtf8 = env->GetVarDef("$ScriptNameUtf8$");
AVSValue lastScriptFileUtf8 = env->GetVarDef("$ScriptFileUtf8$");
AVSValue lastScriptDirUtf8 = env->GetVarDef("$ScriptDirUtf8$");
for (int i = 0; i < args.ArraySize(); ++i) {
const char* script_name = args[i].AsString();
#ifdef AVS_WINDOWS
/* Linux, macOS, pretty much every OS aside from Windows uses
UTF-8 pervasively and by default, making all the Ansi<->Unicode
stuff we have to specially handle on Windows (which uses UTF-16
when it does 'Unicode', further complicating things if you don't
force UTF-8) irrelevant. */
// Handling utf8 and ansi, working in wchar_t internally
// filename and path can be full unicode
// unicode input can come from CAVIFileSynth
std::unique_ptr<wchar_t[]> full_path_w;
wchar_t *file_part_w;
// make wchar_t full path strnig from either ansi or utf8
auto script_name_w = !bUtf8 ? AnsiToWideChar(script_name) : Utf8ToWideChar(script_name);
// Long (>MAX_PATH) path support starting in Windows 10, version 1607.
if (wcschr(script_name_w.get(), '\\') || wcschr(script_name_w.get(), '/')) {
DWORD len = GetFullPathNameW(script_name_w.get(), 0, NULL, NULL); // buffer size for path + terminating zero
full_path_w = std::make_unique<wchar_t[]>(len);
len = GetFullPathNameW(script_name_w.get(), len, full_path_w.get(), &file_part_w);
if (len == 0)
env->ThrowError("Import: unable to open \"%s\" (path invalid?), error=0x%x", script_name, GetLastError());
}
else {
DWORD len = SearchPathW(NULL, script_name_w.get(), NULL, 0, NULL, NULL); // buffer size for path + terminating zero
full_path_w = std::make_unique<wchar_t[]>(len);
len = SearchPathW(NULL, script_name_w.get(), NULL, len, full_path_w.get(), &file_part_w);
if (len == 0)
env->ThrowError("Import: unable to locate \"%s\" (try specifying a path), error=0x%x", script_name, GetLastError());
}
// back to 8 bit Ansi and Utf8
auto full_path = WideCharToAnsi(full_path_w.get());
auto full_path_utf8 = WideCharToUtf8(full_path_w.get());
auto file_part = WideCharToAnsi(file_part_w);
auto file_part_utf8 = WideCharToUtf8(file_part_w);
size_t dir_part_len = wcslen(full_path_w.get()) - wcslen(file_part_w);
auto dir_part = WideCharToAnsi_maxn(full_path_w.get(), dir_part_len);
auto dir_part_utf8 = WideCharToUtf8_maxn(full_path_w.get(), dir_part_len);
// supply L"\\\\?\\" if necessary for long file path support
std::wstring full_path_ex = std::wstring(full_path_w.get());
if (full_path_ex.length() > FILENAME_MAX && full_path_ex.substr(0, 4) != L"\\\\?\\")
full_path_ex = L"\\\\?\\" + full_path_ex;
HANDLE h = ::CreateFileW(full_path_ex.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (h == INVALID_HANDLE_VALUE)
env->ThrowError("Import: couldn't open \"%s\"", full_path.get());
env->SetGlobalVar("$ScriptName$", env->SaveString(full_path.get()));
env->SetGlobalVar("$ScriptFile$", env->SaveString(file_part.get()));
env->SetGlobalVar("$ScriptDir$", env->SaveString(dir_part.get()));
env->SetGlobalVar("$ScriptNameUtf8$", env->SaveString(full_path_utf8.get()));
env->SetGlobalVar("$ScriptFileUtf8$", env->SaveString(file_part_utf8.get()));
env->SetGlobalVar("$ScriptDirUtf8$", env->SaveString(dir_part_utf8.get()));
if (MainScript)
{
env->SetGlobalVar("$MainScriptName$", env->SaveString(full_path.get()));
env->SetGlobalVar("$MainScriptFile$", env->SaveString(file_part.get()));
env->SetGlobalVar("$MainScriptDir$", env->SaveString(dir_part.get()));
env->SetGlobalVar("$MainScriptNameUtf8$", env->SaveString(full_path_utf8.get()));
env->SetGlobalVar("$MainScriptFileUtf8$", env->SaveString(file_part_utf8.get()));
env->SetGlobalVar("$MainScriptDirUtf8$", env->SaveString(dir_part_utf8.get()));
}
*file_part_w = 0; // trunc full_path_w to dir-only
CWDChanger change_cwd(full_path_w.get());
// end of filename parsing / file open things
DWORD size = GetFileSize(h, NULL);
std::vector<char> buf(size + 1, 0);
bool status = ReadFile(h, buf.data(), size, &size, NULL);
CloseHandle(h);
if (!status)
env->ThrowError("Import: unable to read \"%s\"", script_name);
// Give poor Unicode users a hint they need to use ANSI encoding import"
if (size >= 2) {
unsigned char* q = reinterpret_cast<unsigned char*>(buf.data());
if ((q[0] == 0xFF && q[1] == 0xFE) || (q[0] == 0xFE && q[1] == 0xFF))
env->ThrowError("Import: Unicode source files are not supported, "
"re-save script with ANSI or UTF8 w/o BOM encoding! : \"%s\"", script_name);
if (q[0] == 0xEF && q[1] == 0xBB && q[2] == 0xBF)
env->ThrowError("Import: UTF-8 source files with BOM are not supported, "
"re-save script with ANSI or UTF8 w/o BOM encoding! : \"%s\"", script_name);
}
#else // adapted from AvxSynth
std::string file_part = fs::path(script_name).filename().string();
std::string full_path = fs::path(script_name).remove_filename();
std::string dir_part = fs::path(script_name).parent_path();
FILE* h = fopen(script_name, "r");
if(NULL == h)
env->ThrowError("Import: couldn't open \"%s\"", script_name );
env->SetGlobalVar("$ScriptName$", env->SaveString(script_name));
env->SetGlobalVar("$ScriptFile$", env->SaveString(file_part.c_str()));
env->SetGlobalVar("$ScriptDir$", env->SaveString(full_path.c_str()));
env->SetGlobalVar("$ScriptNameUtf8$", env->SaveString(script_name));
env->SetGlobalVar("$ScriptFileUtf8$", env->SaveString(file_part.c_str()));
env->SetGlobalVar("$ScriptDirUtf8$", env->SaveString(full_path.c_str()));
if (MainScript)
{
env->SetGlobalVar("$MainScriptName$", env->SaveString(script_name));
env->SetGlobalVar("$MainScriptFile$", env->SaveString(file_part.c_str()));
env->SetGlobalVar("$MainScriptDir$", env->SaveString(full_path.c_str()));
env->SetGlobalVar("$MainScriptNameUtf8$", env->SaveString(script_name));
env->SetGlobalVar("$MainScriptFileUtf8$", env->SaveString(file_part.c_str()));
env->SetGlobalVar("$MainScriptDirUtf8$", env->SaveString(full_path.c_str()));
}
//*file_part = 0; // trunc full_path to dir-only
CWDChanger change_cwd(full_path.c_str());
// end of filename parsing / file open things
fseek(h, 0, SEEK_END);
size_t size = ftell(h);
fseek(h, 0, SEEK_SET);
std::vector<char> buf(size + 1, 0);
if(size != fread(buf.data(), 1, size, h))
env->ThrowError("Import: unable to read \"%s\"", script_name);
fclose(h);
#endif
buf[size] = 0;
AVSValue eval_args[] = { buf.data(), script_name };
result = env->Invoke("Eval", AVSValue(eval_args, 2));
//env->ThrowError("Import: test %s size %d\n", buf.data(), (int)size);
}
env->SetGlobalVar("$ScriptName$", lastScriptName);
env->SetGlobalVar("$ScriptFile$", lastScriptFile);
env->SetGlobalVar("$ScriptDir$", lastScriptDir);
env->SetGlobalVar("$ScriptNameUtf8$", lastScriptNameUtf8);
env->SetGlobalVar("$ScriptFileUtf8$", lastScriptFileUtf8);
env->SetGlobalVar("$ScriptDirUtf8$", lastScriptDirUtf8);
envi->DecrImportDepth();
return result;
}

Possible Solution(s)

It looks as though you could just wrap the whole for-loop in a try/catch and do a cleaup and re-throw on error. Or, if that's too inelegant, you could use something similar to the CWDChanger object (referenced in the Import function). But I'm no expert, so I'll just keenly keep an eye out to see what the final solution is.

Thanks again for all the work you folks do to make AviSynth+ better, and better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant