diff --git a/firmware/application/apps/ui_flash_utility.cpp b/firmware/application/apps/ui_flash_utility.cpp index 0ea1f2b6..930837cb 100644 --- a/firmware/application/apps/ui_flash_utility.cpp +++ b/firmware/application/apps/ui_flash_utility.cpp @@ -50,6 +50,17 @@ FlashUtilityView::FlashUtilityView(NavigationView& nav) this->firmware_selected(path); }}); } + for (const auto& entry : std::filesystem::directory_iterator(firmware_folder, u"*.ppfw.tar")) { + auto filename = entry.path().filename(); + auto path = entry.path().native(); + + menu_view.add_item({filename.string().substr(0, max_filename_length), + ui::Color::purple(), + &bitmap_icon_temperature, + [this, path](KeyEvent) { + this->firmware_selected(path); + }}); + } } void FlashUtilityView::firmware_selected(std::filesystem::path::string_type path) { @@ -65,7 +76,43 @@ void FlashUtilityView::firmware_selected(std::filesystem::path::string_type path }); } +bool FlashUtilityView::endsWith(const std::u16string& str, const std::u16string& suffix) { + if (str.length() >= suffix.length()) { + std::u16string endOfString = str.substr(str.length() - suffix.length()); + return endOfString == suffix; + } else { + return false; + } +} + +std::filesystem::path FlashUtilityView::extract_tar(std::filesystem::path::string_type path) { + // + ui::Painter painter; + painter.fill_rectangle( + {0, 0, portapack::display.width(), portapack::display.height()}, + ui::Color::black()); + painter.draw_string({12, 24}, this->nav_.style(), "Unpacking TAR file..."); + + auto res = UnTar::untar(path, [this](const std::string fileName) { + ui::Painter painter; + painter.fill_rectangle({0, 50, portapack::display.width(), 90}, ui::Color::black()); + painter.draw_string({0, 60}, this->nav_.style(), fileName); + }); + if (res.string().empty()) { + ui::Painter painter; + painter.fill_rectangle({0, 50, portapack::display.width(), 90}, ui::Color::black()); + painter.draw_string({0, 60}, this->nav_.style(), "BAD TAR FILE"); + chThdSleepMilliseconds(5000); + } + return res; +} + void FlashUtilityView::flash_firmware(std::filesystem::path::string_type path) { + if (endsWith(path, u".ppfw.tar")) { + // extract, then update + path = extract_tar(u'/' + path).native(); + if (path.empty()) return; + } ui::Painter painter; painter.fill_rectangle( {0, 0, portapack::display.width(), portapack::display.height()}, diff --git a/firmware/application/apps/ui_flash_utility.hpp b/firmware/application/apps/ui_flash_utility.hpp index 9ea96c0b..01ea2875 100644 --- a/firmware/application/apps/ui_flash_utility.hpp +++ b/firmware/application/apps/ui_flash_utility.hpp @@ -29,7 +29,7 @@ #include "ff.h" #include "baseband_api.hpp" #include "core_control.hpp" - +#include "untar.hpp" #include namespace ui { @@ -55,8 +55,10 @@ class FlashUtilityView : public View { {0, 2 * 8, 240, 26 * 8}, true}; + std::filesystem::path extract_tar(std::filesystem::path::string_type path); // extracts the tar file, and returns the firmware.bin path from it. empty string if no fw void firmware_selected(std::filesystem::path::string_type path); void flash_firmware(std::filesystem::path::string_type path); + bool endsWith(const std::u16string& str, const std::u16string& suffix); }; } /* namespace ui */ diff --git a/firmware/application/usb_serial_shell.cpp b/firmware/application/usb_serial_shell.cpp index 37ff2f85..72f9ed2e 100644 --- a/firmware/application/usb_serial_shell.cpp +++ b/firmware/application/usb_serial_shell.cpp @@ -40,6 +40,7 @@ #include "ff.h" #include "chprintf.h" #include "chqueues.h" +#include "untar.hpp" #include #include @@ -156,6 +157,15 @@ std::filesystem::path path_from_string8(char* path) { return conv.from_bytes(path); } +bool strEndsWith(const std::u16string& str, const std::u16string& suffix) { + if (str.length() >= suffix.length()) { + std::u16string endOfString = str.substr(str.length() - suffix.length()); + return endOfString == suffix; + } else { + return false; + } +} + static void cmd_flash(BaseSequentialStream* chp, int argc, char* argv[]) { if (argc != 1) { chprintf(chp, "Usage: flash /FIRMWARE/portapack-h1_h2-mayhem.bin\r\n"); @@ -163,15 +173,37 @@ static void cmd_flash(BaseSequentialStream* chp, int argc, char* argv[]) { } auto path = path_from_string8(argv[0]); - size_t filename_length = strlen(argv[0]); if (!std::filesystem::file_exists(path)) { chprintf(chp, "file not found.\r\n"); return; } - std::memcpy(&shared_memory.bb_data.data[0], path.c_str(), (filename_length + 1) * 2); - + // check file extensions + if (strEndsWith(path.native(), u".ppfw.tar")) { + // extract tar + chprintf(chp, "Extracting TAR file.\r\n"); + auto res = UnTar::untar( + path.native(), [chp](const std::string fileName) { + chprintf(chp, fileName.c_str()); + chprintf(chp, "\r\n"); + }); + if (res.empty()) { + chprintf(chp, "error bad TAR file.\r\n"); + return; + } + path = res; // it will contain the last bin file in tar + } else if (strEndsWith(path.native(), u".bin")) { + // nothing to do for this case yet. + } else { + chprintf(chp, "error only .bin or .ppfw.tar files canbe flashed.\r\n"); + return; + } + chprintf(chp, "Flashing: "); + chprintf(chp, path.string().c_str()); + chprintf(chp, "\r\n"); + chThdSleepMilliseconds(50); + std::memcpy(&shared_memory.bb_data.data[0], path.native().c_str(), (path.native().length() + 1) * 2); m4_request_shutdown(); chThdSleepMilliseconds(50); m4_init(portapack::spi_flash::image_tag_flash_utility, portapack::memory::map::m4_code, false); diff --git a/firmware/common/untar.hpp b/firmware/common/untar.hpp new file mode 100644 index 00000000..4f6a762d --- /dev/null +++ b/firmware/common/untar.hpp @@ -0,0 +1,166 @@ +#ifndef __UNTAR +#define __UNTAR + +#include +#include +#include +#include "string_format.hpp" +#include "file.hpp" + +class UnTar { + public: + static std::filesystem::path untar(std::u16string tar, std::function cb = NULL) { + File tf; + auto result = tf.open(tar, true, false); + if (!result.value().ok()) return ""; + return untar_int(&tf, cb); + } + + private: + static int parseoct(const char* p, size_t n) { + int i = 0; + while (*p < '0' || *p > '7') { + ++p; + --n; + } + while (*p >= '0' && *p <= '7' && n > 0) { + i *= 8; + i += *p - '0'; + ++p; + --n; + } + return i; + } + + static bool is_end_of_archive(const char* p) { + for (int n = 511; n >= 0; --n) + if (p[n] != '\0') return false; + return true; + } + + static size_t strnlen(const char* s, size_t maxlen) { + for (size_t i = 0; i < maxlen; i++) { + if (s[i] == '\0') + return i; + } + return maxlen; + } + + static bool isValidName(char* name) { + size_t pathStrLen = strnlen(name, 100); + if (pathStrLen == 0 || pathStrLen >= 100) return false; + return true; + } + + static bool create_dir(char* pathname) { + char* p; + std::filesystem::filesystem_error r; + + if (!isValidName(pathname)) return false; + /* Strip trailing '/' */ + if (pathname[strlen(pathname) - 1] == '/') + pathname[strlen(pathname) - 1] = '\0'; + + /* Try creating the directory. */ + std::string dirnameStr = u'/' + pathname; + std::filesystem::path dirname = dirnameStr; + + r = make_new_directory(dirname); + + if (!r.ok()) { + /* On failure, try creating parent directory. */ + p = strrchr(pathname, '/'); + if (p != NULL) { + *p = '\0'; + create_dir(pathname); + *p = '/'; + r = make_new_directory(dirname); + } + } + return (r.ok()); + } + + static bool verify_checksum(const char* p) { + int n, u = 0; + for (n = 0; n < 512; ++n) { + if (n < 148 || n > 155) + /* Standard tar checksum adds unsigned bytes. */ + u += ((unsigned char*)p)[n]; + else + u += 0x20; + } + return (u == parseoct(p + 148, 8)); + } + + static std::filesystem::path untar_int(File* a, std::function cb = NULL) { + char buff[512]; + UINT bytes_read; + std::string binfile = ""; + std::string fn = ""; + int filesize; + for (;;) { + auto readres = a->read(buff, 512); + if (!readres.is_ok()) return ""; + bytes_read = readres.value(); + if (bytes_read < 512) { + return ""; + } + if (is_end_of_archive(buff)) { + return binfile; + } + if (!verify_checksum(buff)) { + return ""; + } + filesize = parseoct(buff + 124, 12); + switch (buff[156]) { + case '1': + break; + case '2': + break; + case '3': + break; + case '4': + break; + case '5': + create_dir(buff); + filesize = 0; + break; + case '6': + break; + default: + if (isValidName(buff)) { + fn = buff; + if (fn.length() > 5 && fn.substr(fn.length() - 4) == ".bin") { + binfile = fn; + } + if (cb != NULL) cb(fn); + } else { + return ""; // bad tar + } + break; + } + File f; + if (filesize > 0) { + delete_file(fn); + auto fres = f.open(fn, false, true); + if (!fres.value().ok()) return ""; + } + while (filesize > 0) { + readres = a->read(buff, 512); + if (!readres.is_ok()) return ""; + bytes_read = readres.value(); + if (bytes_read < 512) { + return ""; + } + if (filesize < 512) + bytes_read = filesize; + auto fwres = f.write(buff, bytes_read); + if (!fwres.is_ok()) return ""; + filesize -= bytes_read; + f.sync(); + } + } + return binfile; + } +}; +#endif \ No newline at end of file