diff --git a/src/common.cpp b/src/common.cpp index 10e2343f8..fac1556ce 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -1744,8 +1744,7 @@ int common_get_width() { return get_current_winsize().ws_col; } int common_get_height() { return get_current_winsize().ws_row; } bool string_prefixes_string(const wchar_t *proposed_prefix, const wcstring &value) { - size_t prefix_size = wcslen(proposed_prefix); - return prefix_size <= value.size() && value.compare(0, prefix_size, proposed_prefix) == 0; + return string_prefixes_string(proposed_prefix, value.c_str()); } bool string_prefixes_string(const wcstring &proposed_prefix, const wcstring &value) { @@ -1763,6 +1762,17 @@ bool string_prefixes_string(const wchar_t *proposed_prefix, const wchar_t *value return true; } +bool string_prefixes_string(const char *proposed_prefix, const std::string &value) { + return string_prefixes_string(proposed_prefix, value.c_str()); +} + +bool string_prefixes_string(const char *proposed_prefix, const char *value) { + for (size_t idx = 0; proposed_prefix[idx] != L'\0'; idx++) { + if (proposed_prefix[idx] != value[idx]) return false; + } + return true; +} + bool string_prefixes_string_case_insensitive(const wcstring &proposed_prefix, const wcstring &value) { size_t prefix_size = proposed_prefix.size(); diff --git a/src/common.h b/src/common.h index d540d0996..349974f02 100644 --- a/src/common.h +++ b/src/common.h @@ -342,6 +342,8 @@ std::string wcs2string(const wcstring &input); bool string_prefixes_string(const wcstring &proposed_prefix, const wcstring &value); bool string_prefixes_string(const wchar_t *proposed_prefix, const wcstring &value); bool string_prefixes_string(const wchar_t *proposed_prefix, const wchar_t *value); +bool string_prefixes_string(const char *proposed_prefix, const std::string &value); +bool string_prefixes_string(const char *proposed_prefix, const char *value); /// Test if a string is a suffix of another. bool string_suffixes_string(const wcstring &proposed_suffix, const wcstring &value); diff --git a/src/env_universal_common.cpp b/src/env_universal_common.cpp index bfc5afcf9..999b084cf 100644 --- a/src/env_universal_common.cpp +++ b/src/env_universal_common.cpp @@ -74,6 +74,9 @@ /// Small note about not editing ~/.fishd manually. Inserted at the top of all .fishd files. #define SAVE_MSG "# This file contains fish universal variable definitions.\n" +/// Version for fish 3.0 +#define UVARS_VERSION_3_0 "3.0" + /// The different types of messages found in the fishd file. enum class uvar_message_type_t { set, set_export }; @@ -764,7 +767,7 @@ var_table_t env_universal_t::read_message_internal(int fd) { // Process it if it's a newline (which is true if we are before the end of the buffer). if (cursor < bufflen && !line.empty()) { if (utf8_to_wchar(line.data(), line.size(), &wide_line, 0)) { - env_universal_t::parse_message_internal(wide_line, &result, &storage); + env_universal_t::parse_message_2x_internal(wide_line, &result, &storage); } line.clear(); } @@ -778,8 +781,70 @@ var_table_t env_universal_t::read_message_internal(int fd) { return result; } -/// Parse message msg/ -void env_universal_t::parse_message_internal(const wcstring &msgstr, var_table_t *vars, +/// \return the format corresponding to file contents \p s. +uvar_format_t env_universal_t::format_for_contents(const std::string &s) { + // Walk over leading comments, looking for one like '# version' + line_iterator_t iter{s}; + while (iter.next()) { + const std::string &line = iter.line(); + if (line.empty()) continue; + if (line.front() != L'#') { + // Exhausted leading comments. + break; + } + // Note scanf %s is max characters to write; add 1 for null terminator. + char versionbuf[64 + 1]; + if (sscanf(line.c_str(), "# VERSION: %64s", versionbuf) != 1) continue; + + // Try reading the version. + if (!strcmp(versionbuf, UVARS_VERSION_3_0)) { + return uvar_format_t::fish_3_0; + } else { + // Unknown future version. + return uvar_format_t::future; + } + } + // No version found, assume 2.x + return uvar_format_t::fish_2_x; +} + +void env_universal_t::populate_variables(const std::string &s, var_table_t *out_vars) { + // Decide on the format. + const uvar_format_t format = format_for_contents(s); + + line_iterator_t iter{s}; + wcstring wide_line; + wcstring storage; + while (iter.next()) { + const std::string &line = iter.line(); + // Skip empties and constants. + if (line.empty() || line.front() == L'#') continue; + + // Convert to UTF8. + wide_line.clear(); + if (!utf8_to_wchar(line.data(), line.size(), &wide_line, 0)) continue; + + switch (format) { + case uvar_format_t::fish_2_x: + env_universal_t::parse_message_2x_internal(wide_line, out_vars, &storage); + break; + case uvar_format_t::fish_3_0: + // For future formats, just try with the most recent one. + case uvar_format_t::future: + env_universal_t::parse_message_30_internal(wide_line, out_vars, &storage); + break; + } + } +} + +/// Parse message msg per fish 3.0 format. +void env_universal_t::parse_message_30_internal(const wcstring &msgstr, var_table_t *vars, + wcstring *storage) { + // TODO. +} + +/// Parse message msg per fish 2.x format. +void env_universal_t::parse_message_2x_internal(const wcstring &msgstr, var_table_t *vars, wcstring *storage) { const wchar_t *msg = msgstr.c_str(); diff --git a/src/env_universal_common.h b/src/env_universal_common.h index c597a031a..daebe7a2c 100644 --- a/src/env_universal_common.h +++ b/src/env_universal_common.h @@ -30,6 +30,10 @@ struct callback_data_t { typedef std::vector callback_data_list_t; +// List of fish universal variable formats. +// This is exposed for testing. +enum class uvar_format_t { fish_2_x, fish_3_0, future }; + bool get_hostname_identifier(wcstring &result); /// Class representing universal variables. class env_universal_t { @@ -68,7 +72,10 @@ class env_universal_t { // vars_to_acquire. void acquire_variables(var_table_t &vars_to_acquire); - static void parse_message_internal(const wcstring &msg, var_table_t *vars, wcstring *storage); + static void parse_message_2x_internal(const wcstring &msg, var_table_t *vars, + wcstring *storage); + static void parse_message_30_internal(const wcstring &msg, var_table_t *vars, + wcstring *storage); static var_table_t read_message_internal(int fd); public: @@ -95,6 +102,13 @@ class env_universal_t { /// Reads and writes variables at the correct path. Returns true if modified variables were /// written. bool sync(callback_data_list_t &callbacks); + + /// Populate a variable table \p out_vars from a \p s string. + /// This is exposed for testing only. + static void populate_variables(const std::string &s, var_table_t *out_vars); + + /// Guess a file format. Exposed for testing only. + static uvar_format_t format_for_contents(const std::string &s); }; /// The "universal notifier" is an object responsible for broadcasting and receiving universal diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index cc2dbeef7..ce1282a9a 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -2940,6 +2940,27 @@ static void test_universal_callbacks() { (void)system("rm -Rf test/fish_uvars_test/"); } +static void test_universal_formats() { + say(L"Testing universal format detection"); + const struct { + const char *str; + uvar_format_t format; + } tests[] = { + {"# VERSION: 3.0", uvar_format_t::fish_3_0}, + {"# version: 3.0", uvar_format_t::fish_2_x}, + {"# blah blahVERSION: 3.0", uvar_format_t::fish_2_x}, + {"stuff\n# blah blahVERSION: 3.0", uvar_format_t::fish_2_x}, + {"# blah\n# VERSION: 3.0", uvar_format_t::fish_3_0}, + {"# blah\n#VERSION: 3.0", uvar_format_t::fish_3_0}, + {"# blah\n#VERSION:3.0", uvar_format_t::fish_3_0}, + {"# blah\n#VERSION:3.1", uvar_format_t::future}, + }; + for (const auto &test : tests) { + uvar_format_t format = env_universal_t::format_for_contents(test.str); + do_test(format == test.format); + } +} + bool poll_notifier(const std::unique_ptr ¬e) { bool result = false; if (note->usec_delay_between_polls() > 0) { @@ -4824,6 +4845,7 @@ int main(int argc, char **argv) { if (should_test_function("line_iterator")) test_line_iterator(); if (should_test_function("universal")) test_universal(); if (should_test_function("universal")) test_universal_callbacks(); + if (should_test_function("universal")) test_universal_formats(); if (should_test_function("notifiers")) test_universal_notifiers(); if (should_test_function("completion_insertions")) test_completion_insertions(); if (should_test_function("autosuggestion_ignores")) test_autosuggestion_ignores();