![](https://habrastorage.org/getpro/habr/upload_files/e35/e9d/3da/e35e9d3da99fc4f3b65928f3ae072409.png)
Эта история началась когда удобный для меня инструмент VSCode из за одного плагина на борту стал не совсем удобен. А именно, «natizyskunk.sftp» плагин, который позволяет подключится по протоколу (S)FTP к серверу, и вносить изменения на сервер, просто сохранив файл. Всё нечего, если бы настройки подключения к серверам не хранились в корне отдельно взятого проекта. Когда у тебя с десяток проектов, ещё можно потерпеть, но более 70 проектов и они все на разных серверах, то перенос и добавление в каждый из проектов актуальной настройки, например пароль, который периодически меняется, заставляет как минимум раздражённо искать подходящую настройку и с комбинацией клавиш Ctrl+C, Ctrl+V, менять устаревшие настройки или добавлять новые. В дальнейшем оказалось, что коллеги пользуются другими идентичными плагинами с названиями «liximomo.sftp», «doujinya.sftp-revived». Идентичность их на столько существенная, что разница заключается в паре символов из интересующего кода. Прочитав лицензионное соглашение на всех трёх плагинах и выяснив что лицензия позволяет вносить изменения в код без спроса. Пробравшись в файл плагина и так как они в VSCode написаны на JS, увидел обфусцированный JS код, состоящий из нескольких строк. Комментарии и большой объём кода без перехода на следующую строку.
![](https://habrastorage.org/getpro/habr/upload_files/9f5/b1c/d25/9f5b1cd25b7fd710ba33a796222496d7.png)
Зная, что плагин создаёт файл с настройками в корне проекта в директории с названием .vscode, я решил начать с поиска этого слова в коде.
![](https://habrastorage.org/getpro/habr/upload_files/ceb/5d2/f8b/ceb5d2f8bd04df3f9d642353f9d58607.png)
Видно, что происходит конкатенация значений переменной «r» названия этого каталога и значением (которое можно увидеть перед «t.CONFIG_PATH») константы «t.CONGIF_FILENAME» . Странно что при этом вызывается функция «join()» которая по стандарту js принимает только один аргумент. Если конечно переменная «r» не является массивом. Скорей всего в данном контексте реализация функции «join()» находится в задействованном объекте, не исключаю что цель этой реализации заключается в том чтобы сбить с толку читателя кода. Потому что стандартное объединение строк выглядит не страшно и очень похоже «string.concat(string1, string2, …, stringX)».
Теперь я уверен, что путь к файлу находится в константе объекта «CONFIG_PATH». В поиске найдено два объекта, первый уже мы видели, а во втором случае в объявленной функции, в качестве возвращаемого значения, происходит такая-же конкатенация как в прошлый раз.
![](https://habrastorage.org/getpro/habr/upload_files/c62/721/72d/c6272172da08f9577c18084e53679abd.png)
Где потом эта функция вызывается искать не очень хочется, да и не думаю что понадобится. Ради эксперимента заменил значение переменной на полный путь до директории где будут храниться настройки подключения к серверам. После перезагрузки 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. Для начала стоит разбить задачу на ключевые моменты.
Определить пользователя и перейти к нему в home директорию
Найти нужный файл для правки и найти место правки
Получить от пользователя путь к новому месту хранения настроек и узнать какой плагин интересует пользователя
Внести изменения в файле
Всё просто. Я разделил задачи поровну на два объекта 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), убираем из неё всё лишнее и приводим в соответствие, чтобы было два слэша (\\).
![](https://habrastorage.org/getpro/habr/upload_files/805/68b/10a/80568b10afa56ed651d30b275e1ac228.png)
Всё что делает функция «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». Ему досталось:
Получить от пользователя путь к новому месту хранения настроек, и узнать какой плагин интересует пользователя;
Заголовочный файл выглядит так:
/**
* @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.
![](https://habrastorage.org/getpro/habr/upload_files/e59/eda/67d/e59eda67d03ab4b3ceb6bb2c2b00cd64.png)
Но это сравнение не совсем точно, если посмотреть на объём файлов, то разница будет ещё более существенная.
![](https://habrastorage.org/getpro/habr/upload_files/5ed/f00/c96/5edf00c96b98a7400d9fc2c80da7646d.png)
![](https://habrastorage.org/getpro/habr/upload_files/760/5ec/32e/7605ec32ed647d9897643cf232ecea08.png)
Открываем оба файла и сравниваем.
![](https://habrastorage.org/getpro/habr/upload_files/261/697/5f2/2616975f2c5fa3089a06a63996120387.png)
Функция main в варианте Windows мало отличается от оригинала. В процедуре «__main()» находится ещё одна процедура под названием «__do_global_ctors()» она вызывает конструкторы для всех статических объектов. А в варианте с Linux такого нет, но есть кое что другое.
![](https://habrastorage.org/getpro/habr/upload_files/8d1/fd7/a7c/8d1fd7a7c229f719430eff411fd8b5bb.png)
Объявляются две дополнительные переменные «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, короткая запись, что не удивительно поскольку способ решения задачи перехода в домашнюю директорию там проще.
![](https://habrastorage.org/getpro/habr/upload_files/db3/cd8/6f4/db3cd86f48bc8e0d5536151e49c22d79.png)
А в приложении для Windows есть что посмотреть и над чем порассуждать. Например конкатенация строк. Оговорюсь, конкатенация строк не отличается не там не там, просто рассматриваю жучков под ногами.
![](https://habrastorage.org/getpro/habr/upload_files/f73/25d/758/f7325d758fa121c69cbfb8ac2da86f88.png)
Исходный код склейки строк выглядит так:
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()».
![](https://habrastorage.org/getpro/habr/upload_files/0e0/2e0/d11/0e02e0d113c92c1ffae990b75a83d063.png)
Рассматривая и сравнивая код, я наткнулся на необычные функции, которые находились исключительно в приложении для Windows с названием на подобие таких: «FUN_0046cd10», «FUN_0046cd40». Когда «заглядываешь внутрь» она выглядит так: «void FUN_0046cd10(void){return;}». Ничего не делает и возвращает NULL. Взглянув на те же строки в конце конструктора «FileRewrite», на скриншоте можно увидеть в приложении для Linux деструкторы класса «std::allocator<char>», а в приложении для Windows три одинаковые строки на том-же месте с вызовом функции «FUN_0046cd40()»
![](https://habrastorage.org/getpro/habr/upload_files/2e2/a12/b37/2e2a12b3784d7c5bacdb4d1b418900d4.png)
![](https://habrastorage.org/getpro/habr/upload_files/613/8c8/ffb/6138c8ffbf4fd04fddc95325363456d4.png)
Это говорит о том, что «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)
Alex-111
27.06.2022 07:23Количество странностей в этой статье просто зашкаливает.
Зачем править минимизированный js, когда доступны исходники. Сделайте форк для себя или пулл-реквест, если считаете, что фитча может быть полезна другим.
Уже сказали выше: километровая программа на с++ для замены подстроки в текстовом файле? Зачем? Это же несколько строчек на bash, питоне или ноде.
Тут вкусовщина, конечно, но посмотрите в сторону Remote Development плагина (который, как следует из названия, помимо копирования файлов предоставляет существенно более расширенные функции для удаленной разработки).
askhats
27.06.2022 09:15Судя по описанию проекта на github, файлы конфигураций хранятся в папке проекта, потому что у каждого проекта могут быть свои настройки загрузки на (S)FTP сервер. Даже в случае, если все проекты копируются на один сервер, целевые папки все равно различаются по каждому проекту. Поэтому, не понятно, как автор смог объединить настройки вех своих проектов в один конфигурационный файл.
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 { ...
В целом: приятно, что люди выбираются из своей зоны комфорта и пробуют что-то новое. Автор - респект.
qw1
Жесть какая. Писать огромную программу на C++ для замены подстроки в js-файле.
С этим бы справился однострочник на PowerShell, а для linux можно взять bash или awk.
Разве что время некуда девать…
Tolsedum Автор
В случае PowerShell и bash, не удалось бы разведать кросс-компиляцию с её нюансами на С++. Есть языки программирования с помощью которых можно выполнить эту задачу без компиляции, большого объёма кода и с кроссплатформенностью всё в порядке.