Эта история началась когда удобный для меня инструмент VSCode из за одного плагина на борту стал не совсем удобен. А именно, «natizyskunk.sftp» плагин, который позволяет подключится по протоколу (S)FTP к серверу, и вносить изменения на сервер, просто сохранив файл. Всё нечего, если бы настройки подключения к серверам не хранились в корне отдельно взятого проекта. Когда у тебя с десяток проектов, ещё можно потерпеть, но более 70 проектов и они все на разных серверах, то перенос и добавление в каждый из проектов актуальной настройки, например пароль, который периодически меняется, заставляет как минимум раздражённо искать подходящую настройку и с комбинацией клавиш Ctrl+C, Ctrl+V, менять устаревшие настройки или добавлять новые. В дальнейшем оказалось, что коллеги пользуются другими идентичными плагинами с названиями «liximomo.sftp», «doujinya.sftp-revived». Идентичность их на столько существенная, что разница заключается в паре символов из интересующего кода. Прочитав лицензионное соглашение на всех трёх плагинах и выяснив что лицензия позволяет вносить изменения в код без спроса. Пробравшись в файл плагина и так как они в VSCode написаны на JS, увидел обфусцированный JS код, состоящий из нескольких строк. Комментарии и большой объём кода без перехода на следующую строку.

Зная, что плагин создаёт файл с настройками в корне проекта в директории с названием .vscode, я решил начать с поиска этого слова в коде.

Видно, что происходит конкатенация значений переменной «r» названия этого каталога и значением (которое можно увидеть перед «t.CONFIG_PATH») константы «t.CONGIF_FILENAME» . Странно что при этом вызывается функция «join()» которая по стандарту js принимает только один аргумент. Если конечно переменная «r» не является массивом. Скорей всего в данном контексте реализация функции «join()» находится в задействованном объекте, не исключаю что цель этой реализации заключается в том чтобы сбить с толку читателя кода. Потому что стандартное объединение строк выглядит не страшно и очень похоже «string.concat(string1, string2, …, stringX)».

Теперь я уверен, что путь к файлу находится в константе объекта «CONFIG_PATH». В поиске найдено два объекта, первый уже мы видели, а во втором случае в объявленной функции, в качестве возвращаемого значения, происходит такая-же конкатенация как в прошлый раз.

Где потом эта функция вызывается искать не очень хочется, да и не думаю что понадобится. Ради эксперимента заменил значение переменной на полный путь до директории где будут храниться настройки подключения к серверам. После перезагрузки VSCode с помощью комбинации клавиш «Ctrl+Shift+p» откроется командная строка и запустив команду «>SFTP: Config» плагин создаст необходимый файл с настройками по заданному пути. Файл стал общим для всех проектов и работает как задумывалось! Это победа! Теперь надо поделится этим открытием с коллегами и избавить их от мучений.

Но я не я если не избавлю их от столь слегка не удобного способа корректировать путь к файлу. Подумав над выбором языка, для того чтобы не терять накопленный опыт, поскольку практики с ним уже нет, остановился на C++. Не простой но интересный путь, при условии что, на уже родных для меня PHP или Python задачу можно выполнить быстрее, проще и понятней, если C# то и графический интерфейс запилить без проблем (при условии если ОП будет Windows). Так как в организации мы работаем на операционной системе Windows, а дома я работаю под Linux, задача становится ещё интересней. Тем более опыта с крос-компиляцией у меня немного. Разведку на неизвестных мне языках, таких как Rust, Go, Ruby я оставлю на потом.

С этого момента хочу сразу предупредить: - я не профессиональный разработчик на C++, и изложенный ниже материал не стоит воспринимать как учебное пособие. В процессе разработки учишься чему-то новому, а документирование пройденного материала в статье, помогает закрепить знания.

Для компиляции программы под систему Windows потребуются установить компилятор GNU C++ для MinGW-w64. Установка в Ubuntu / Linux Mint / Debian выглядит так:

sudo apt update
sudo apt install g++-mingw-w64

Makefile выглядит так:

#Для Windowd 32bit
# CXX := i686-w64-mingw32-g++
#Для Windowd 64bit
# CXX := x86_64-w64-mingw32-g++
# Для Linux
CXX := g++

CXX_FLAGS := -Wall -Wextra -std=c++17 -ggdb

BIN := bin
SRC := src
INCLUDE := include
LIB := lib 

ifeq ($(CXX),x86_64-w64-mingw32-g++)
	DEFINE := WIN32=WIN32
	EXECUTABLE := sftp_pat.exe
else ifeq ($(CXX),i686-w64-mingw32-g++)
	DEFINE := WIN32=WIN32
	EXECUTABLE := sftp_pat.exe
else ifeq ($(CXX),g++)
	DEFINE := LINUX=LINUX
	EXECUTABLE := sftp_patch
endif

LIBRARIES := -static-libstdc++ -static-libgcc -lstdc++fs 

all: $(BIN)/$(EXECUTABLE)

run: clean all
	clear
	./$(BIN)/$(EXECUTABLE)

$(BIN)/$(EXECUTABLE): $(SRC)/*.cpp
$(CXX) $(CXX_FLAGS) -I $(INCLUDE) -L $(LIB) $^ -o $@ $(LIBRARIES) -D $(DEFINE)

clean:
	-rm $(BIN)/*

Для выбора компилятора нужно раскомментировать нужный CXX и закомментировать остальные.


Теперь можно переходить к точке входа под названием main.cpp. Для начала стоит разбить задачу на ключевые моменты.

  1. Определить пользователя и перейти к нему в home директорию

  2. Найти нужный файл для правки и найти место правки

  3. Получить от пользователя путь к новому месту хранения настроек и узнать какой плагин интересует пользователя

  4. Внести изменения в файле

Всё просто. Я разделил задачи поровну на два объекта GetFileTarget и FileRewrite. Финальная версия main.cpp выглядит так:

/**
 * @file main.cpp
 * @author Tolsedum (tolsedum@gmail.com)
 * @brief Entry point
 * @version 1.0
 * @date 2022-06-12
 * 
 * @copyright Copyright (c) 2022
 * 
 */ 
#include "GetFileTarget.hpp"
#include "FileRewrite.hpp"

int main() {
    try{
        GetFileTarget getFileTarget;
        // Определить пользователя и перейти к нему в home дирикторию
        getFileTarget.jumpToDirectory();
        // Для Windows задает режим преобразования файлов и выводит логотип. Для Linux выводит логотип.
        getFileTarget.setMode();
        
        // Найти нужный файл для правки и найти место правки
        GenerelInformation fileDate = getFileTarget.getFilePosition();
        FileRewrite fileRewrite(fileDate);

        // Получить от пользователя путь к новому месту хранения настроек и узнать какой плагин интересует пользователя
        fileRewrite.getSettingsPatch();

        // Внести изменения в файле
        fileRewrite.rewrite();
      
    }catch(const my_char* error){
        fn::printString(error);
    }catch (my_stryng error) {
        fn::printString(error);
    }catch(const fs::filesystem_error& e){
        fn::printString(e.what());
    }catch(std::exception & e) {
        fn::printString(e.what());
    }
    fn::pause();
	return 0;
}

Для того чтобы подключать заголовочные файлы, определять функции, константы которые используются во всех объектах мне нравится держать один общий для всех объект. Его я назвал «GlobalIncluds».

/**
 * @file GlobalIncluds.hpp
 * @author Tolsedum (tolsedum@gmail.com)
 * @brief Global include headers, var, structs and functins
 * @version 0.1
 * @date 2022-06-12
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#pragma once
#ifndef GLOBAL_INCLUDE
#define GLOBAL_INCLUDE "GLOBAL_INCLUDE"


#include <regex>
#include <iostream>
#include <string>
#include <string.h>
#include <fstream>
#include <experimental/filesystem>
#include <vector>

#ifdef WIN32
    #include <direct.h>
    #include <fcntl.h>
    typedef wchar_t my_char;
    typedef std::wstring my_stryng;
#else
    #include "unistd.h"
    typedef std::string my_stryng;
    typedef char my_char;
#endif


namespace fs = std::experimental::filesystem;

// Структура для передачи данных от одного объекта другому
struct GenerelInformation{
    public:
        std::string tmp_file_name;
        std::string file_path;
        std::size_t pos_begin = 0;
        std::size_t pos_end = 0;
        short selected_number;
        GenerelInformation(){};
        ~GenerelInformation(){};
};

namespace fn{
    #ifdef WIN32
        inline std::string wstringToString(std::wstring w_str_to_convert){
            std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> w_conv;
            std::string ret_value = w_conv.to_bytes(w_str_to_convert);
            return ret_value;
        }
        inline std::wstring stringToWstring(std::string str_to_convert){
            std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> w_conv;
            std::wstring ret_value = w_conv.from_bytes(str_to_convert);
            return ret_value;
        }
    #endif // WIN32

    // ----------------------- getLineCin -----------------------
    // Конвертация строк. Требуются только в Windows
    inline void getLineCin(std::wstring &in_str){
        std::wcin >> in_str;
    }
    inline void getLineCin(std::string &in_str){
        std::getline(std::cin, in_str);
    }
    // ----------------------- getLineCin end -------------------

    // ----------------------- printString -----------------------
    // Универсальный метод для вывода одного сообщения
    inline void printString(wchar_t str_to_print){
        std::wcout << str_to_print <<std::endl;
    }
    inline void printString(char str_to_print){
        std::cout << str_to_print <<std::endl;
    }
    inline void printString(wchar_t* str_to_print){
        std::wcout << str_to_print <<std::endl;
    }
    inline void printString(char* str_to_print){
        std::cout << str_to_print <<std::endl;
    }
    inline void printString(const wchar_t* str_to_print){
        std::wcout << str_to_print <<std::endl;
    }
    inline void printString(const char* str_to_print){
        std::cout << str_to_print <<std::endl;
    }
    inline void printString(std::wstring str_to_print){
        std::wcout << str_to_print <<std::endl;
    }
    inline void printString(std::string str_to_print){
        std::cout << str_to_print <<std::endl;
    }
    inline void printString(std::exception except){
        #ifdef WIN32
            std::wcout << except.what() <<std::endl;
        #else
            std::cout << except.what() <<std::endl;
        #endif // WIN32
    }
    // ----------------------- printString end---------------------
    // Пауза в конце программы
    inline void pause(){
        #ifdef WIN32
            system("PAUSE");
        #else
            std::cout<< "Введите символ и нажмите enter" <<std::endl;
            char d;
            std::cin >> d;
        #endif // WIN32
    }
}
#endif // !GLOBAL_INCLUDE

Введу того что путь к файлу и сам файл пациент я обрабатываю в std::string, а пути к файлам в Windows содержат кириллицу и её в адекватном виде получить из cmd Windows можно только через std::wstring, пришлось делать функции конвертации.

Для вывода в консоль информации для пользователя, пришлось сделать перегрузку функции printString, так как после изменения режима вывода информации в UTF-16 работает только std::wcout. Про изменение режима чуть позже. За первую и вторую задачи, ответственность несёт объект GetFileTarget.

Заголовочный файл GetFileTarget:

/**
 * @file GetFileTarget.hpp
 * @author Tolsedum (tolsedum@gmail.com)
 * @brief To go to the worcing dirictory
 * @version 0.1
 * @date 2022-06-12
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#pragma once
#ifndef GET_FILE_TARGET
#define GET_FILE_TARGET "GET_FILE_TARGET"

#include "GlobalIncluds.hpp"

class GetFileTarget{
private:
    /** @brief Имя файла который надо менять*/
    std::string name_file_target;
    /** @brief Имя временного файла для сохранения в него информации*/
    std::string tmp_file_name;
    /** @brief Список плагинов в которых изменить путь*/
    std::vector<std::string> use_plagin;
    /** @brief Список строк для определения позиции начала правки*/
    std::vector<std::string> search_begin;
    /** @brief Номер плагина из списка use_plagin. Соответствует итератору vector*/
    short selected_number;
    /** @brief Предложение пользователю о выборе плагина*/
    const my_char *ask_user_a_plagin;
    /** @brief Сообщение о том что надо ввести число*/
    const my_char *invalid_argument;
    /** @brief Сообщение о ограничении вводимых чисел*/
    const my_char *out_of_range;
    /** @brief Сообщение о том что файл не может быть открыт*/
    const my_char *file_is_not_open;
    /** @brief Сообщение о том что файл не найден*/
    const my_char *file_is_not_found;
    /** @brief Путь к каталогу с файлом*/
    const char *profile_patch;
    void getIntCin();
public:
    GetFileTarget();
    /** Определить пользователя и перейти к нему в home директорию*/
    void jumpToDirectory();
    /** Найти нужный файл для правки и найти место правки*/
    GenerelInformation getFilePosition();
    // Для Windows задает режим преобразования файлов и выводит логотип. Для Linux выводит логотип.
    void setMode();
    
};
#endif // !GET_FILE_TARGET

В конструкторе класса GetFileTarget инициализация переменных организована для каждой ОС в соответствии с типом.

GetFileTarget::GetFileTarget(){
    this->tmp_file_name = "tmp_file.tmp";
    
    this->use_plagin = {
        "natizyskunk.sftp",
        "liximomo.sftp",
        "doujinya.sftp-revived"
    };
    this->search_begin = {
        "function d(e){return Object.assign({},h,e)}", // for natizyskunk.sftp
        "function p(e){return Object.assign({},h,e)}", // for liximomo.sftp
        "function d(e){return Object.assign({},h,e)}"  // for doujinya.sftp-revived
    };
    
    #ifdef WIN32
        this->file_is_not_found = L"Файл не найден";
        this->file_is_not_open = L"Файл не может быть открыт";
        this->out_of_range = L"Число не должно быть больше 3 и меньше 1";
        this->invalid_argument = L"Введите число: ";
        this->ask_user_a_plagin = L"\
Каким плагином вы пользуетесь: \n \
1) natizyskunk.sftp \n \
2) liximomo.sftp \n \
3) doujinya.sftp-revived \n\
Введите номер: ";
        this->name_file_target = "\\\\dist\\\\extension.js";
        this->profile_patch = ".vscode\\\\extensions";
    #else
        this->file_is_not_found = "Файл не найден";
        this->file_is_not_open = "Файл не может быть открыт";
        this->out_of_range = "Число не должно быть больше 3 и меньше 1";
        this->invalid_argument = "Введите число: ";
        this->ask_user_a_plagin = "\
Каким плагином вы пользуетесь: \n \
1) natizyskunk.sftp \n \
2) liximomo.sftp \n \
3) doujinya.sftp-revived \n \
Введите номер: ";
        this->name_file_target = "/dist/extension.js";
        this->profile_patch = ".vscode/extensions";
    #endif // WIN32
}

За переход в директорию пользователя отвечает функция jumpToDirectory.

void GetFileTarget::jumpToDirectory(){
    #ifdef WIN32
        // Изменение кодировки в cmd на UTF-8
        system("chcp 65001");
        std::string current_patch;
        // Команда на сохранение списка файла и каталогов во временный файл tmp_file_name
        std::string cmd = "dir %UserProfile% >> "+this->tmp_file_name;
        // Выполнение заданной команды в cmd Windous
        system(cmd.c_str());
        // Получить из временного файла путь к пользовательскому каталогу
        std::ifstream tmp_file_point(this->tmp_file_name);
        if(tmp_file_point.is_open()){
            std::string search_str = " Directory of ";
            for(std::string line; getline(tmp_file_point, line);){
                std::size_t pos = line.find(search_str);
                if(pos != std::string::npos){
                    // Убрать то что находится перед нужной информацией
                    current_patch = line.replace(0, search_str.size(), "");
                    // Приводим в соответствие сам путь к директории. Должен выглядеть например вот так (C:\\Users\\user_himself)
                    current_patch = std::regex_replace(current_patch, std::regex("\\\\"), "\\\\");
                    break;
                }
            }
            tmp_file_point.close();
            if(remove(this->tmp_file_name.c_str()) != 0){
                throw L"Удалить временный файл не удалось, удалите его сами.";
            }
        }else{
            throw L"Временный файл открыть не удалось";
        }
        // Переход в домашнюю директорию пользователя
        _wchdir(fn::stringToWstring(current_patch).c_str());
    #else
        // Переход в домашнюю директорию пользователя
        chdir(getenv("HOME"));
    #endif // WIN32
}

Перейти в домашнюю директорию под Linux как видно очень просто. Что касается Windows, я не знаю как красиво можно это сделать, не используя при этом Visual C++, примеров в интернете так и не нашёл. Абстрагировавшись от задачи и попив чаю «молочный улун» вприкуску с бубликом я вспомнил, что в cmd Windows есть возможность использовать распознаваемые переменные среды. При помощи переменной %UserProfile% и команды DIR получится вывести список содержимого домашней директории пользователя и выдернуть от туда путь к нему.

Изменение кодировки в cmd на UTF-8 (system("chcp 65001");) приводит к тому, что во временный файл будет сохранены данные не на кириллице, а на латинице. Если этого не делать и сохранить на кириллице, то выйдет то, что изображено на скриншоте. Остаётся дело за малым, читаем файл, находим нужную строку (по наличию в ней содержимого переменной search_str), убираем из неё всё лишнее и приводим в соответствие, чтобы было два слэша (\\).

Всё что делает функция «setMode» это вывод моего логотипа и для Windows меняет режим преобразования файлов.

void GetFileTarget::setMode(){
    #ifdef WIN32
        //  Задает режим преобразования файлов
        if(_setmode(_fileno(stdout), _O_U16TEXT) < 0)throw L"Не удалось установить режим преобразования файла!";
        auto const & sz_message
        {
            L" __________________________________________ \n"
            L"|                                          |\n"
            L"|   ╭━━━━┳━━━┳╮╱╱╭━━━┳━━━┳━━━┳╮╱╭┳━╮╭━╮    |\n"
            L"|   ┃╭╮╭╮┃╭━╮┃┃╱╱┃╭━╮┃╭━━┻╮╭╮┃┃╱┃┃┃╰╯┃┃    |\n"
            L"|   ╰╯┃┃╰┫┃╱┃┃┃╱╱┃╰━━┫╰━━╮┃┃┃┃┃╱┃┃╭╮╭╮┃    |\n"
            L"|   ╱╱┃┃╱┃┃╱┃┃┃╱╭╋━━╮┃╭━━╯┃┃┃┃┃╱┃┃┃┃┃┃┃    |\n"
            L"|   ╱╱┃┃╱┃╰━╯┃╰━╯┃╰━╯┃╰━━┳╯╰╯┃╰━╯┃┃┃┃┃┃    |\n"
            L"|   ╱╱╰╯╱╰━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻╯╰╯╰╯    |\n"
            L"|__________________________________________|\n"
            L"|                                          |\n"
            L"| Permission is hereby granted, free of    |\n"
            L"| charge, to any person obtaining a copy of|\n"
            L"| of this software and accompanying files, |\n"
            L"| to use them without restriction,         |\n"
            L"| including, without limitation, the       |\n"
            L"| rights to use, copy, modify, merge,      |\n"
            L"| publish, distribute, sublicense and/or   |\n"
            L"| sell copies of the software. The authors |\n"
            L"| or copyright holders shall not be liable |\n"
            L"| for any claims, damages or other         |\n"
            L"| liability, whether in contract, tort or  |\n"
            L"| otherwise, arising out of or in          |\n"
            L"| connection with the software or your use |\n"
            L"| or other dealings with the software.     |\n"
            L"|__________________________________________|\n"
            L"|   website: ***************               |\n"
            L"|   email: tolsedum@gmail.com              |\n"
            L"|__________________________________________|\n"
        };
        std::wcout << sz_message << std::endl;
    #else
        auto const &sz_message{
            " __________________________________________ \n"
            "|                                          |\n"
            "|   ╭━━━━┳━━━┳╮╱╱╭━━━┳━━━┳━━━┳╮╱╭┳━╮╭━╮    |\n"
            "|   ┃╭╮╭╮┃╭━╮┃┃╱╱┃╭━╮┃╭━━┻╮╭╮┃┃╱┃┃┃╰╯┃┃    |\n"
            "|   ╰╯┃┃╰┫┃╱┃┃┃╱╱┃╰━━┫╰━━╮┃┃┃┃┃╱┃┃╭╮╭╮┃    |\n"
            "|   ╱╱┃┃╱┃┃╱┃┃┃╱╭╋━━╮┃╭━━╯┃┃┃┃┃╱┃┃┃┃┃┃┃    |\n"
            "|   ╱╱┃┃╱┃╰━╯┃╰━╯┃╰━╯┃╰━━┳╯╰╯┃╰━╯┃┃┃┃┃┃    |\n"
            "|   ╱╱╰╯╱╰━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻╯╰╯╰╯    |\n"
            "|__________________________________________|\n"
            "|                                          |\n"
            "| Permission is hereby granted, free of    |\n"
            "| charge, to any person obtaining a copy of|\n"
            "| of this software and accompanying files, |\n"
            "| to use them without restriction,         |\n"
            "| including, without limitation, the       |\n"
            "| rights to use, copy, modify, merge,      |\n"
            "| publish, distribute, sublicense and/or   |\n"
            "| sell copies of the software. The authors |\n"
            "| or copyright holders shall not be liable |\n"
            "| for any claims, damages or other         |\n"
            "| liability, whether in contract, tort or  |\n"
            "| otherwise, arising out of or in          |\n"
            "| connection with the software or your use |\n"
            "| or other dealings with the software.     |\n"
            "|__________________________________________|\n"
            "|   website: ***************               |\n"
            "|   email: tolsedum@gmail.com              |\n"
            "|__________________________________________|\n"
        };
        std::cout << sz_message << std::endl;
    #endif // WIN32
}

С задачей найти нужный файл для правки и найти в нём место правки, справляется функция «getFilePosition» которая возвращает структуру с собранными данными.

GenerelInformation GetFileTarget::getFilePosition(){
    // Получить номер который соответствует выбранному плагину
    this->getIntCin();
    std::ifstream file_point;
    GenerelInformation info;
    info.tmp_file_name = tmp_file_name;

    bool file_finde = false;
    // Поиск подходящей директории плагина
    for (auto const& p : fs::directory_iterator(this->profile_patch)){
        std::size_t pos = p.path().string().find(this->use_plagin[this->selected_number]);
        if(pos != std::string::npos){
            this->name_file_target = p.path().string() + this->name_file_target;
            file_point.open(this->name_file_target);
            info.file_path = this->name_file_target;
            file_finde = true;
            break; 
        }
    }
    if(!file_finde){
        throw this->file_is_not_found;
    }else if (!file_point.is_open()){
        throw this->file_is_not_open;
    }

    int pos_len = 0;
    // Определение позиции в файле для его правки
    for (std::string line; getline(file_point, line);){
        std::size_t pos = line.find(this->search_begin[this->selected_number]);
        if(pos != std::string::npos){
            pos_len += pos + this->search_begin[this->selected_number].size();
            info.pos_begin = pos_len;
            std::string name_searching_str = "function";
            std::size_t pos_end = line.find(name_searching_str, pos + this->search_begin[this->selected_number].size()+name_searching_str.size());
            if(pos_end != std::string::npos){
                info.pos_end = pos_len + (pos_end-(pos + this->search_begin[this->selected_number].size()));
                break;
            }
        }else{
            // Так как в Linux новая строка обозначается \n а в Windows \r\n при работе со строками в системе Win надо увеличить размер строки на 1 символ
            pos_len += line.size();
            #ifdef WIN32
                pos_len +=1;
            #endif // WIN32
        }
    }
    file_point.close();
    info.selected_number = this->selected_number;
    return info;
}

Так как в Linux новая строка обозначается \n а в Windows \r\n при работе со строками в системе Windows приходится после новой строки продвигать каретку ещё на один символ (pos_len += 1).

Ну и получение номера соответствующего выбранному плагину реализовано в функции «getIntCin»

void GetFileTarget::getIntCin(){
    fn::printString(this->ask_user_a_plagin);
    my_stryng tmp_str;
    bool iter = true;
    while (iter){
        try{
            fn::getLineCin(tmp_str);
            this->selected_number = std::stoi(tmp_str);
            if(this->selected_number == 1){
                this->selected_number = 0;
                iter = false;
            }else if(this->selected_number == 2){
                this->selected_number = 1;
                iter = false;
            }else if(this->selected_number == 3){
                this->selected_number = 2;
                iter = false;
            }else{
                fn::printString(this->out_of_range);
            }
        }catch(const std::invalid_argument &e){
            fn::printString(this->invalid_argument);
        }catch(const std::out_of_range &e){ 
            fn::printString(this->out_of_range);
        }
    }
}

Две задачи по изменению файла взял на себя класс «FileRewrite». Ему досталось:

  1. Получить от пользователя путь к новому месту хранения настроек, и узнать какой плагин интересует пользователя;

  2. Заголовочный файл выглядит так:

/**
 * @file FileRewrite.hpp
 * @author Tolsedum (tolsedum@gmail.com)
 * @brief For to change file
 * @version 0.1
 * @date 2022-06-12
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#pragma once
#ifndef FILE_REWRITE
#define FILE_REWRITE "FILE_REWRITE"


#include "GlobalIncluds.hpp"


class FileRewrite{
private:
    /** @brief Информация о файле*/
    GenerelInformation info;
    /** @brief Строка с готовым js кодом*/
    std::string str_to_insert;
    /** @brief Варианты заготовок с js кодом для разных плагинов*/
    std::vector<std::string> function_variant;
    /** @brief Сообщение о неудачном удалении*/
    const my_char *remove_file_failed;
    /** @brief Сообщение о удачном изменении файла*/
    const my_char *file_patched;
    /** @brief Прощание с пользователем*/
    const my_char *say_goodby;
    /** @brief Спросить пользователя путь где хранить файл*/
    const my_char *ask_user_a_path;
public:
    FileRewrite(GenerelInformation &_fileData);
    /** Получить от пользователя путь к новому месту хранения настроек*/
    void getSettingsPatch();
    /** Редактирование файла*/
    void rewrite();
   
    
};

#endif // !FILE_REWRITE

Я решил вынести в переменные класса сообщения пользователю о ошибках, и вопросах для того, чтобы их инициализировать в конструкторе класса в зависимости от операционной системы, также как и в предыдущем объекте.

Конструктор класса FileRewrite выглядит следующем образом:

FileRewrite::FileRewrite(GenerelInformation &_info){
    this->info = _info;
    #ifdef WIN32
        this->remove_file_failed = L"Удаление файла не удалось, удалите его сами.";
        this->file_patched = L"Файл изменён.";
        this->say_goodby = L"Файл изменён. Наслаждайтесь!";
        this->ask_user_a_path = L"Введите путь к месту хранения настроек: ";
        
    #else
        this->remove_file_failed = "Удаление файла не удалось, удалите его сами.";
        this->file_patched = "Файл изменён.";
        this->say_goodby = "Файл изменён. Наслаждайтесь!";
        this->ask_user_a_path = "Введите путь к месту хранения настроек: ";
        
    #endif // WIN32
    this->function_variant = {
        "function p(e){e='%s';return o.join(e,a.CONFIG_PATH)}",// for natizyskunk.sftp
        "function d(e){e='%s';return o.join(e,a.CONFIG_PATH)}", // for liximomo.sftp
        "function p(e){e='%s';return o.join(e,a.CONFIG_PATH)}" // for doujinya.sftp-revived
    };
}

Переменная function_variant хранит варианты шаблонов для замены в файле. Место для вставке введённого пользователем пути обозначено «%s». Первый и второй вариант идентичны, но принадлежат разным плагинам. В дальнейшем, возможно, надо будет добавлять шаблон в зависимости от версии плагина.

Получение от пользователя пути к новому месту хранения настроек реализовано в функции «getSettingsPatch»:

void FileRewrite::getSettingsPatch(){
    my_stryng tmp_path_settings_file;

    fn::printString(this->ask_user_a_path);
    // Получение от пользователя пути хранения настроек
    fn::getLineCin(tmp_path_settings_file);
    fn::printString(' ');
    std::string path_settings;
    #ifdef WIN32
        path_settings = fn::wstringToString(tmp_path_settings_file);
        path_settings = std::regex_replace(path_settings, std::regex("\\\\"), "\\\\");
    #else
        path_settings = tmp_path_settings_file;
    #endif // WIN32
    
    this->str_to_insert = function_variant[this->info.selected_number];
    // Вставка пути в шаблон
    this->str_to_insert = std::regex_replace(this->str_to_insert, std::regex("%s"), path_settings);
}

И последняя функция которая делает то ради чего всё затевалось «rewrite»:

void FileRewrite::rewrite(){
    if(this->info.pos_begin > 0){
        std::ifstream file_target(this->info.file_path, std::ios::in | std::ios::binary);
        std::ofstream tmp_file(this->info.tmp_file_name, std::ios::out | std::ios::binary | std::ios::trunc);
        bool is_opened = tmp_file.is_open() && file_target.is_open();
        
        if(is_opened){
            // Пмшем во временный файл данный из файла с кодом до позиции вставки
            char *big_buffer = new char[this->info.pos_begin];
            file_target.read(big_buffer, this->info.pos_begin);
            tmp_file.write(big_buffer, this->info.pos_begin);
            delete[] big_buffer;
            // Вставляем во временный файл новую запись
            tmp_file.write(this->str_to_insert.c_str(), this->str_to_insert.size());
            
            file_target.seekg(0, file_target.end);
            int size_buffer = file_target.tellg();
            // Определяем сколько надо пропустить
            size_buffer -= this->info.pos_begin+(this->info.pos_end-this->info.pos_begin);
            // Пропускаем старую запись
            file_target.seekg(this->info.pos_end);
            // Записываем всё что осталось
            char *buffer = new char[size_buffer];
            file_target.read(buffer, size_buffer);
            tmp_file.write(buffer, size_buffer);
            delete[] buffer;
        }
        file_target.close();
        tmp_file.close();
        if(is_opened){
            std::ifstream tmp_file_if(this->info.tmp_file_name, std::ios::in | std::ios::binary);
            std::ofstream file_target_in(this->info.file_path.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
            // Переносим содержимое из временного файла в оригинальный
            file_target_in << tmp_file_if.rdbuf();
            tmp_file_if.close();
            file_target_in.close();
            // Удаляем временный файл
            if (remove(this->info.tmp_file_name.c_str()) != 0) {
                throw this->remove_file_failed;
            }else{
                fn::printString(this->file_patched);
            }
        }
        fn::printString(this->say_goodby);
    }

Теперь попробую сравнить две программы, заглянув в их «нутро». Что скажет «Ghidra» о программах. Моё внимание привлекла строчка с размерами файлов. Программа для операционной системой Linux весит намного меньше чем для Windows.

Но это сравнение не совсем точно, если посмотреть на объём файлов, то разница будет ещё более существенная.

Открываем оба файла и сравниваем.

Функция main в варианте Windows мало отличается от оригинала. В процедуре «__main()» находится ещё одна процедура под названием «__do_global_ctors()» она вызывает конструкторы для всех статических объектов. А в варианте с Linux такого нет, но есть кое что другое.

Объявляются две дополнительные переменные «long lVar1, in_FS_OFFSET;». Учитывая то что сначала в «lVarl» ложем определённые данные «lVar1 = *(long *)(in_FS_OFFSET + 0x28);», а перед выходом из функции проверяем на идентичность «if (lVar1 != *(long *)(in_FS_OFFSET + 0x28))» и если не совпало вызываем «__stack_chk_fail()» (сбой проверки стека), можно смело утверждать, что это проверка на переполнение стека перед выходом из функции. Такая конструкция часто встречается в методах, функциях, процедурах, конструкторах приложения для Linux. По какой-то причине «Ghidra» не нравится, что метод «__stack_chk_fail» ничего не возвращает, возможно это связано с тем, что анализ не может пройти дальше содержимого функции. Если проверка на переполнение стека и применение мер по его защите это хорошо, то почему тогда такая проверка отсутствует в приложении для Windows?

Сравнивая функцию «jumpToDirectory» в приложении для Linux, короткая запись, что не удивительно поскольку способ решения задачи перехода в домашнюю директорию там проще.

А в приложении для Windows есть что посмотреть и над чем порассуждать. Например конкатенация строк. Оговорюсь, конкатенация строк не отличается не там не там, просто рассматриваю жучков под ногами.

Исходный код склейки строк выглядит так:

std::string cmd = "dir %UserProfile% >> "+this→tmp_file_name;

В «Ghidra» так:

std::operator+<char,_std::char_traits<char>,_std::allocator<char>_>(&cmd,"dir %UserProfile% >> ",&this→tmp_file_name);

А вызов функции «system(cmd.c_str());» с передачей параметра (const char*) выглядит так:

pcVar4 = std::__cxx11::basic_string<char,
_std::char_traits<char>,
_std::allocator<char>_>::c_str(&cmd);
system(pcVar4);

Это может значить то, что между такой записью:

std::string str_main = "Some text";
const char* c_test_str = str_main.c_str();
system(str);

И такой:

std::string str_main = "Some text";
system(str_main.c_str());

Разница в том, что во втором случае, переменная «c_test_str» есть но нам не доступна, и компилятор её применяет там, где идёт следующий вызов функции «c_str()».

Рассматривая и сравнивая код, я наткнулся на необычные функции, которые находились исключительно в приложении для Windows с названием на подобие таких: «FUN_0046cd10», «FUN_0046cd40». Когда «заглядываешь внутрь» она выглядит так: «void FUN_0046cd10(void){return;}». Ничего не делает и возвращает NULL. Взглянув на те же строки в конце конструктора «FileRewrite», на скриншоте можно увидеть в приложении для Linux деструкторы класса «std::allocator<char>», а в приложении для Windows три одинаковые строки на том-же месте с вызовом функции «FUN_0046cd40()»

Это говорит о том, что «Ghidra» не смогла опознать смысл конструкции написанной ниже:

Linux

Windows

LEA RAX=>local_99,[RBP + -0x91]

LEA RAX=>local_19,[RBP + -0x1]

MOV this,RAX

MOV this,RAX

CALL std::allocator<char>::~allocator

CALL FUN_0046cd40

Я сначала подумал, что если запустить «Ghidra» из Windows, то функции определятся нормально. Ради удовлетворения своего интереса пришлось зайти в давно заброшенную мной, установленную рядом с Linux операционную систему Windows. Тридцать минут ожидания установки обновлений и неизвестно чем обоснованный процесс загрузки жесткого диска, оперативной памяти и процессорного ресурса, заставили меня понервничать. Но поставленная задача была выполнена и я убедился в том что ошибался. Ничего не изменилось. Возможно, причина кроется в самой «Ghidra». После вынесенного вердикта было принято решение, завершить свою разведку кросс-компиляции и подвести итоги.

Во первых, прежде чем браться за подобные задачи и тратить своё драгоценное время, желательно:

  • спросить окружающих решается ли задача в настройках;

  • спросить разработчика, пойдут ли они на добавление необходимого функционала;

  • и вообще оно кому нибудь надо?

Последний пункт не обязателен для тех кто учится и хочет узнать что-то новое.

Во вторых, прежде чем лезть в чужой код, ознакомьтесь с лицензией разработчика.

В третьих, к выбору языка программирования стоит подходить с учётом некоторых факторов:

  • соотношение ресурсов железа против качества написания логики и компиляции или интерпретации кода;

  • сроки выполнения задачи;

К примеру: я наверняка уверен, что с этой задачей можно было справится на Java быстрей, и размер кода был бы на порядок короче. Производительность выполнения бинарника на C++ выше (возможно стоит это проверить) но с учётом мощности железа и не критичности к скорости выполнения программы, Java выиграл бы не по одному пункту.

P.S. Ходит много слухов о том, что Rust лучше по многим пунктам чем C++. Что для написания программы достаточно постучать лицом по клавиатуре для того чтобы что нибудь да получилось. По сему, есть у меня мысли заняться разведкой этого языка, как минимум для расширения своего кругозора, а может, дойдёт до того что и проект какой на нём настучу. Будем посмотреть!

Этот патч находится в свободном доступе на GitHub

Комментарии (7)


  1. qw1
    26.06.2022 16:04
    +3

    Жесть какая. Писать огромную программу на C++ для замены подстроки в js-файле.
    С этим бы справился однострочник на PowerShell, а для linux можно взять bash или awk.
    Разве что время некуда девать…


    1. Tolsedum Автор
      26.06.2022 16:13
      +1

      В случае PowerShell и bash, не удалось бы разведать кросс-компиляцию с её нюансами на С++. Есть языки программирования с помощью которых можно выполнить эту задачу без компиляции, большого объёма кода и с кроссплатформенностью всё в порядке.


  1. Alex-111
    27.06.2022 07:23

    Количество странностей в этой статье просто зашкаливает.

    1. Зачем править минимизированный js, когда доступны исходники. Сделайте форк для себя или пулл-реквест, если считаете, что фитча может быть полезна другим.

    2. Уже сказали выше: километровая программа на с++ для замены подстроки в текстовом файле? Зачем? Это же несколько строчек на bash, питоне или ноде.

    3. Тут вкусовщина, конечно, но посмотрите в сторону Remote Development плагина (который, как следует из названия, помимо копирования файлов предоставляет существенно более расширенные функции для удаленной разработки).


  1. Helltraitor
    27.06.2022 09:15

    Приключение на 20 минут


  1. askhats
    27.06.2022 09:15

    Судя по описанию проекта на github, файлы конфигураций хранятся в папке проекта, потому что у каждого проекта могут быть свои настройки загрузки на (S)FTP сервер. Даже в случае, если все проекты копируются на один сервер, целевые папки все равно различаются по каждому проекту. Поэтому, не понятно, как автор смог объединить настройки вех своих проектов в один конфигурационный файл.


  1. Apoheliy
    27.06.2022 10:22
    +1

    Не соглашусь с комментариями выше. Человек хотел сделать на C++ - он сделал на C++.

    Слова Автора и Комментаторов, что можно было сделать быстрее и короче на ??? желательно дополнить: ТАК И НА C++ можно сделать быстрее и короче.

    Например код:

    if(this->selected_number == 1){ this->selected_number = 0; iter = false; }else if(this->selected_number == 2){ this->selected_number = 1; iter = false; }else if(this->selected_number == 3){ this->selected_number = 2; iter = false; }else{ ...

    Можно написать так:

    if (selected_number > 0 && selected_number < 4) {

    --selected_number;

    iter = false; } else { ...

    В целом: приятно, что люди выбираются из своей зоны комфорта и пробуют что-то новое. Автор - респект.


  1. webhamster
    28.06.2022 09:34
    +1

    По-моему, не хватает тэга "Ненормальное программирование".