Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ if(WIN32)
endif(WIN32)
add_executable(send_file
"src/send_file.cpp"
"src/shell.cpp"
"src/snfm.cpp"
"src/config.cpp")
target_link_libraries(send_file PRIVATE snfm_common config)
Expand Down
1 change: 1 addition & 0 deletions include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct RomDestinationRule
struct Config
{
std::string default_rom_directory = ".sni";
std::string post_upload_script;
std::vector<RomDestinationRule> rom_destination_rules;

std::optional<std::reference_wrapper<const RomDestinationRule>> FindRuleForRom(const std::string& rom_name);
Expand Down
20 changes: 20 additions & 0 deletions include/shell.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include <string>
#include <utility>
#include <vector>

// Replaces every "{placeholder}" in template_string with the matching value from
// variables. Placeholders with no matching variable are left untouched.
std::string ExpandTemplate(const std::string& template_string,
const std::vector<std::pair<std::string, std::string>>& variables);

// Escapes value so that the shell hands it to the command as literal text instead of
// acting on anything in it. This only works if the placeholder is left unquoted in
// the config - see snfm_config_example.yaml.
std::string EscapeForShell(const std::string& value);

// Substitutes the (escaped) variables into script_template and runs the result through
// the shell, reporting anything that goes wrong on stdout.
void RunPostUploadScript(const std::string& script_template,
const std::vector<std::pair<std::string, std::string>>& variables);
20 changes: 20 additions & 0 deletions snfm_config_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@
### Default is .sni - directories that start with "." are hidden in the FXPAKPRO UI
# default_rom_directory: ".sni"

### (optional) a command to run once the ROM has been sent to the device and booted.
### Handy for things like writing the MSU pack you picked out to a file for a stream overlay.
### These variables are substituted in before the command is run:
### {rule_name} - the rule that matched, e.g. "alttpr" (empty if nothing matched)
### {destination_name} - the destination you picked, e.g. "WildArms2" (empty if nothing matched)
### {destination_path} - the directory on the device, e.g. "/ROMs/_alttpr/_msu1/wa2"
### {destination_rom_name} - the ROM's name on the device, e.g. "alttp_msu.sfc"
### {destination_file} - the full path on the device, e.g. "/ROMs/_alttpr/_msu1/wa2/alttp_msu.sfc"
### {source_path} - the full path of the ROM on your computer
### {source_name} - the file name of the ROM on your computer
### The command is run by your shell (cmd.exe on Windows). The values are escaped for
### you, so leave the placeholders unquoted - whatever a ROM name or a destination
### happens to contain is then passed along as plain text instead of being run as part
### of the command. Quote the parts of the command you wrote yourself, as usual.
### Windows example - the redirect goes first so that a destination name ending in a
### digit (like "WildArms2") isn't read by cmd.exe as a stream number:
# post_upload_script: '> "%USERPROFILE%\msu.txt" echo {destination_name}'
### ...and the same thing on unix:
# post_upload_script: 'echo {destination_name} > "$HOME/msu.txt"'

### a set of rules to use to put certain ROMs in certain locations, such as your randomizer ROMs, useful for MSU1 users
rom_destination_rules:
# Unique name for your rule
Expand Down
4 changes: 4 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ Config LoadConfig(const std::filesystem::path& path)
if (default_dir) {
config.default_rom_directory = default_dir.as<std::string>();
}
auto post_upload_script = y["post_upload_script"];
if (post_upload_script) {
config.post_upload_script = post_upload_script.as<std::string>();
}
auto r = y["rom_destination_rules"];

for (auto rule_yaml : y["rom_destination_rules"]) {
Expand Down
20 changes: 19 additions & 1 deletion src/send_file.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "snfm.h"
#include "config.h"
#include "shell.h"
#include <algorithm>
#include <filesystem>

Expand Down Expand Up @@ -38,11 +39,14 @@ int main(int argc, char* argv[])

std::string device_directory = config.default_rom_directory;
std::string destination_rom_name = rom_path.filename().string();
std::string rule_name;
std::string destination_name;

auto maybe_rule = config.FindRuleForRom(rom_path.filename().string());
if (maybe_rule)
{
auto rule = maybe_rule->get();
rule_name = rule.name;
if (!rule.destinations.empty())
{
RomDestination& destination = rule.destinations[0];
Expand Down Expand Up @@ -80,7 +84,7 @@ int main(int argc, char* argv[])
std::cout << "Not a number." << std::endl;
continue;
}
if (pick < 0 || pick > rule.destinations.size())
if (pick < 0 || pick >= static_cast<int>(rule.destinations.size()))
{
std::cout << "Choice must be between 0 and " << std::to_string(rule.destinations.size() - 1) << std::endl;
continue;
Expand All @@ -91,6 +95,7 @@ int main(int argc, char* argv[])

}
std::cout << "Using " << destination.config_name << std::endl;
destination_name = destination.config_name;
device_directory = destination.path;
if (!destination.rom_name.empty()) {
destination_rom_name = destination.rom_name;
Expand Down Expand Up @@ -132,5 +137,18 @@ int main(int argc, char* argv[])
}
sni.bootFile(*uri, *put_path);

if (!config.post_upload_script.empty())
{
RunPostUploadScript(config.post_upload_script, {
{"rule_name", rule_name},
{"destination_name", destination_name},
{"destination_path", device_directory},
{"destination_rom_name", destination_rom_name},
{"destination_file", put_path->generic_string()},
{"source_path", rom_path.string()},
{"source_name", rom_path.filename().string()},
});
}

return 0;
}
141 changes: 141 additions & 0 deletions src/shell.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#include "shell.h"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <iostream>

#ifndef _WIN32
#include <sys/wait.h>
#endif

std::string ExpandTemplate(const std::string& template_string,
const std::vector<std::pair<std::string, std::string>>& variables)
{
std::string result;
size_t pos = 0;
while (pos < template_string.size())
{
const size_t open = template_string.find('{', pos);
if (open == std::string::npos)
{
result.append(template_string, pos, std::string::npos);
break;
}
const size_t close = template_string.find('}', open);
if (close == std::string::npos)
{
result.append(template_string, pos, std::string::npos);
break;
}
result.append(template_string, pos, open - pos);

const std::string name = template_string.substr(open + 1, close - open - 1);
auto variable = std::find_if(variables.begin(), variables.end(),
[&name](const auto& v) { return v.first == name; });
if (variable != variables.end())
{
result.append(variable->second);
}
else
{
// Unknown placeholder - leave it alone so it's obvious what went wrong.
std::cout << "Unknown template variable {" << name << "}" << std::endl;
result.append(template_string, open, close - open + 1);
}
pos = close + 1;
}
return result;
}

// The values we substitute in come from ROM file names and the config file, so they
// can contain characters the shell treats as syntax. Escape them so that whatever is
// in them ends up as literal text rather than as extra commands. This only works if
// the placeholder is left unquoted in the config - see snfm_config_example.yaml.
std::string EscapeForShell(const std::string& value)
{
#ifdef _WIN32
// cmd.exe has no way to escape a quote once it's inside one, so drop quotes (and
// newlines) outright - Windows file names can't contain them anyway - and escape
// everything else cmd would act on with a caret.
std::string escaped;
for (const char c : value)
{
if (c == '"' || c == '\r' || c == '\n')
{
continue;
}
if (std::strchr("^&|<>()%", c) != nullptr)
{
escaped.push_back('^');
}
escaped.push_back(c);
}
return escaped;
#else
// Single quotes protect everything except a single quote itself, which has to be
// closed, escaped, and reopened.
std::string escaped = "'";
for (const char c : value)
{
if (c == '\'')
{
escaped += "'\\''";
}
else
{
escaped.push_back(c);
}
}
escaped.push_back('\'');
return escaped;
#endif
}

void RunPostUploadScript(const std::string& script_template,
const std::vector<std::pair<std::string, std::string>>& variables)
{
std::vector<std::pair<std::string, std::string>> escaped_variables;
escaped_variables.reserve(variables.size());
for (const auto& variable : variables)
{
escaped_variables.emplace_back(variable.first, EscapeForShell(variable.second));
}

const std::string command = ExpandTemplate(script_template, escaped_variables);
std::cout << "Running post upload script: " << command << std::endl;

std::string to_run = command;
#ifdef _WIN32
// cmd.exe strips the outer quotes off a command that starts with one, which
// breaks quoted script paths - give it a spare pair to chew on.
if (!to_run.empty() && to_run.front() == '"')
{
to_run = "\"" + to_run + "\"";
}
#endif
const int result = std::system(to_run.c_str());
if (result == -1)
{
std::cout << "Couldn't run the post upload script." << std::endl;
}
else if (result != 0)
{
#ifdef _WIN32
std::cout << "Post upload script exited with code " << result << std::endl;
#else
// system() hands back a wait status, not the exit code.
if (WIFEXITED(result))
{
std::cout << "Post upload script exited with code " << WEXITSTATUS(result) << std::endl;
}
else if (WIFSIGNALED(result))
{
std::cout << "Post upload script was killed by signal " << WTERMSIG(result) << std::endl;
}
else
{
std::cout << "Post upload script failed with status " << result << std::endl;
}
#endif
}
}