Compare commits

...
27 Commits
Author SHA1 Message Date
josh d8a37c6e9a Add shapes to grammar 2026-11-03 22:14:41 -05:00
josh fa77c0af29 Build an Image and initialize to background color; write to output file 2026-11-03 19:51:25 -05:00
josh 6f8e3714da Rename RenderParams -> Options 2026-11-03 19:34:20 -05:00
josh de2c51376a Reintroduce options block to scene file grammar; remove scene block 2026-11-03 19:25:58 -05:00
josh 0ba0ebfe71 Load scene render params 2026-11-02 22:28:18 -05:00
josh a3684b20b6 Add Parser::build_scene() 2026-11-02 22:12:34 -05:00
josh c1c8c46ac7 Add background RenderParams entry 2026-11-02 22:07:22 -05:00
josh 0a979c487c Invoke propane to parse input file 2026-11-02 22:05:01 -05:00
josh 88bfc29323 Add CLI option to set number of render threads 2026-11-02 21:47:42 -05:00
josh e964908e46 Add vector to common.h 2026-11-02 21:09:07 -05:00
josh bf09c094e1 Make RenderParams fields optional 2026-11-02 21:04:11 -05:00
josh 1475434c4c Add read_file 2026-11-02 20:19:20 -05:00
josh e13608b287 Add Parser and Scene classes 2026-11-02 18:43:39 -05:00
josh c8f6d36bdc Verify that output file can be written before rendering 2026-11-01 20:38:25 -05:00
josh 6b77b1a666 Build an ImageWriter 2026-11-01 20:20:31 -05:00
josh d7394027b1 default the output file name 2026-11-01 16:05:50 -05:00
josh 68d5b39cfd Error if no input file given 2026-11-01 15:43:30 -05:00
josh 0d36f13b17 Add RenderParams constructor 2026-11-01 15:39:57 -05:00
josh 88d482dcb3 Move input file / output file out of RenderParams 2026-11-01 15:26:46 -05:00
josh d71be68fae Add auto-included fart/common.h 2026-11-01 14:18:02 -05:00
josh 1947ea553c Rename Options to RenderParams 2026-11-01 13:20:59 -05:00
josh 7eaf5f9555 Add background grammar rule 2026-11-01 13:17:32 -05:00
josh d761433382 Add Number and Vector to grammar 2026-10-30 19:57:52 -04:00
josh e3d4c88f66 Add propane and initial skeleton grammar 2026-10-30 19:36:59 -04:00
josh 94105a883f Pass options to Engine; add Engine::run() 2026-10-30 19:06:22 -04:00
josh c05f2b1b59 Cli: validate that width and height arguments are numbers 2026-10-30 13:55:08 -04:00
josh cc041168a1 Parse command-line arguments 2026-10-30 13:50:11 -04:00
22 changed files with 825 additions and 22 deletions

No files matched your search

+2 -1
View File
@@ -1,5 +1,6 @@
/fart
*.bmp
*.png
/.propane*
/.rscons*
/build/
/fart
+19 -4
View File
@@ -3,10 +3,25 @@ configure do
check_cfg package: "libpng"
end
env do |env|
env["CCFLAGS"] += %w[-Wall -O2 -std=c++23]
env["CPPPATH"] += %w[src]
class Propane < Builder
def run(options)
@sources += ["propane"] unless @sources.include?("propane")
if @command
finalize_command
else
command = %W[./propane -w --log #{@target}.log #{@sources.first} #{@target}]
standard_command("Propane <target>#{@target}<reset>", command, options)
end
end
end
sources = glob("src/**/*.cpp")
env do |env|
env.add_builder(Propane)
env["CCFLAGS"] += %w[-Wall -O2 -std=c++23 -include fart/common.h]
env["CPPPATH"] += %W[src #{env.build_root}/src]
env.Propane("^/src/fart/propane.cpp", "src/fart.propane")
env.barrier
sources = glob("src/**/*.cpp") + ["#{env.build_root}/src/fart/propane.cpp"]
env.Program("fart", sources)
end
Executable
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env ruby
# The MIT License (MIT)
#
# Copyright (c) 2010-2026 Josh Holtrop
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
script = File.join(File.dirname(__FILE__), ".propane-5.2.0-58c963344c9bc893689cbae5e4c49b1b.rb")
unless File.exist?(script)
if File.read(__FILE__, mode: "rb") =~ /^#==>(.*)/
require "zlib"
require "base64"
encoded_compressed = $1
unescaped_compressed = Base64.decode64(encoded_compressed)
inflated = Zlib::Inflate.inflate(unescaped_compressed)
File.open(script, "wb") do |fh|
fh.write(inflated)
end
else
raise "Could not decompress."
end
end
load script
if __FILE__ == $0
Propane::CLI.new.run(ARGV)
end
#==>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 truncated
+134
View File
@@ -0,0 +1,134 @@
tree;
tree_prefix P;
ptype double;
<<header
/* User terminate codes. */
#define PARSER_TERMINATE_NUMBER_RANGE 1u
>>
<<
#include <charconv>
/* Store the numeric value of the matched text in *out. Returns false if the
* value cannot be represented. */
static bool parse_number(uint8_t const * match_text, size_t match_length, double * out)
{
const char * first = (const char *)match_text;
const char * last = first + match_length;
std::from_chars_result r = std::from_chars(first, last, *out);
return (r.ec == std::errc()) && (r.ptr == last);
}
>>
drop /\s+/;
drop /#.*/;
token background;
token box;
token camera;
token color;
token comma /,/;
token cyl;
token gt />/;
token height;
token intersect;
token lcurly /\{/;
token lt /</;
token material;
token options;
token plane;
token position;
token rcurly /\}/;
token rotate;
token scale;
token size;
token sphere;
token subtract;
token target;
token translate;
token union;
token up;
token width;
token uint /\d+/ <<
if (!parse_number(match_text, match_length, &$$))
{
$terminate(PARSER_TERMINATE_NUMBER_RANGE);
}
>>
token number /-?\d+(\.\d+)?/ <<
if (!parse_number(match_text, match_length, &$$))
{
$terminate(PARSER_TERMINATE_NUMBER_RANGE);
}
>>
start Scene;
Background -> background Vector;
Color -> color Vector;
Height -> height uint;
Number -> uint;
Number -> number;
Options -> options lcurly OptionsItems rcurly;
OptionsItems -> ;
OptionsItems -> OptionsItem OptionsItems;
OptionsItem -> Background;
OptionsItem -> Height;
OptionsItem -> Width;
Position -> position Vector;
Rotate -> rotate Number comma Vector;
Rotate -> rotate Number comma Vector lcurly SceneItems rcurly;
Scale -> scale Number;
Scale -> scale Number lcurly SceneItems rcurly;
Scale -> scale Vector;
Scale -> scale Vector lcurly SceneItems rcurly;
SceneItemBlock -> SceneItemName lcurly SceneItems rcurly;
Scene -> Options? SceneItems;
SceneItems -> ;
SceneItems -> SceneItem SceneItems;
SceneItem -> Color;
SceneItem -> Position;
SceneItem -> Rotate;
SceneItem -> Scale;
SceneItem -> SceneItemBlock;
SceneItem -> Size;
SceneItem -> Target;
SceneItem -> Translate;
SceneItem -> Up;
SceneItemName -> box;
SceneItemName -> camera;
SceneItemName -> cyl;
SceneItemName -> material;
SceneItemName -> plane;
SceneItemName -> sphere;
Size -> size Number;
Size -> size Vector;
Target -> target Vector;
Translate -> translate Vector;
Translate -> translate Vector lcurly SceneItems rcurly;
Up -> up Vector;
Vector -> lt Number comma Number comma Number gt;
Vector -> lt Number comma Number comma Number comma Number gt;
Width -> width uint;
+222 -1
View File
@@ -1,5 +1,226 @@
#include <fart/Cli.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <stdint.h>
Cli::Cli()
#define FLAGS \
FLAG(height, "h", true) \
FLAG(height_l, "height", true) \
FLAG(help, "help", false) \
FLAG(output, "o", true) \
FLAG(output_l, "output", true) \
FLAG(threads, "t", true) \
FLAG(threads_l, "threads", true) \
FLAG(version, "version", false) \
FLAG(width, "w", true) \
FLAG(width_l, "width", true) \
#define FLAG(name, flag, arg) flag_##name,
typedef enum
{
FLAGS
flag__count
} flag_id_t;
#undef FLAG
#define FLAG(name, flag, arg) static const char flag_##name##_str[] = flag;
FLAGS
#undef FLAG
typedef struct
{
const char * flag;
size_t flag_length;
bool arg;
} flag_def_t;
#define FLAG(name, flag, arg) { \
flag_##name##_str, \
sizeof(flag_##name##_str) - 1u, \
arg},
static const flag_def_t flag_defs[] = {
FLAGS
};
#undef FLAG
int Cli::run(int argc, char * argv[])
{
for (size_t i = 1; i < argc; i++)
{
size_t dashes = 0u;
if (strncmp(argv[i], "--", 2) == 0)
{
dashes = 2u;
}
else if (strncmp(argv[i], "-", 1) == 0)
{
dashes = 1u;
}
if (dashes > 0u)
{
const char * flag = &argv[i][dashes];
size_t flag_idx;
for (flag_idx = 0u; flag_idx < flag__count; flag_idx++)
{
size_t flag_length = flag_defs[flag_idx].flag_length;
size_t flag_dashes = flag_length == 1u ? 1u : 2u;
if (flag_dashes != dashes)
{
continue;
}
if (strncmp(flag, flag_defs[flag_idx].flag, flag_length) != 0)
{
continue;
}
if (dashes == 2 && flag[flag_length] != '\0' && flag[flag_length] != '=')
{
continue;
}
bool arg = flag_defs[flag_idx].arg;
const char * arg_value = nullptr;
if (arg)
{
if (flag[flag_length] == '\0')
{
if (i >= (argc - 1))
{
fprintf(stderr, "Error: option %s requires an argument\n", flag_defs[flag_idx].flag);
return 2;
}
i++;
arg_value = argv[i];
}
else if (dashes == 1u)
{
arg_value = &flag[flag_length];
}
else if (flag[flag_length + 1] != '\0')
{
arg_value = &flag[flag_length + 1];
}
else
{
fprintf(stderr, "Error: option %s requires an argument\n", flag_defs[flag_idx].flag);
return 2;
}
}
else if (dashes == 2u && flag[flag_length] == '=')
{
fprintf(stderr, "Error: option %s does not take an argument\n", flag_defs[flag_idx].flag);
return 2;
}
else
{
/* TODO: support combining short flags, e.g. -cofoo */
}
int r = handle_option(flag_idx, arg_value);
if (r != 0)
{
return r;
}
break;
}
if (flag_idx == flag__count)
{
fprintf(stderr, "Unknown flag: %s\n", flag);
return 2;
}
}
else
{
if (!this->input_file.empty())
{
fprintf(stderr, "Extraneous argument: %s\n", argv[i]);
return 2;
}
this->input_file = argv[i];
}
}
return 0;
}
static bool parse_size(const char * flag, const char * value, opt<size_t> * out)
{
bool valid = value[0] != '\0';
for (const char * c = value; *c != '\0'; c++)
{
if (*c < '0' || *c > '9')
{
valid = false;
break;
}
}
if (valid)
{
errno = 0;
unsigned long long v = strtoull(value, nullptr, 10);
if (errno == 0 && v <= SIZE_MAX)
{
*out = (size_t)v;
return true;
}
}
fprintf(stderr, "Error: invalid value for option %s: '%s'\n", flag, value);
return false;
}
int Cli::handle_option(size_t flag_idx, const char * arg_value)
{
switch (flag_idx)
{
case flag_height:
case flag_height_l:
if (!parse_size(flag_defs[flag_idx].flag, arg_value, &this->options.height))
{
return 2;
}
return 0;
case flag_help:
printf("Usage: fart [options] input-file\n");
printf("\n");
printf(" Options:\n");
printf("\n");
printf(" -h, --height H Set render height to H\n");
printf(" --help Show this help and exit\n");
printf(" -o, --output F Write rendered image to file F\n");
printf(" -t, --threads N Use N render threads (default: number of CPUs)\n");
printf(" -w, --width W Set render width to W\n");
printf(" --version Show fart version and exit\n");
return 1;
case flag_output:
case flag_output_l:
this->output_file = arg_value;
return 0;
case flag_threads:
case flag_threads_l:
if (!parse_size(flag_defs[flag_idx].flag, arg_value, &this->options.threads))
{
return 2;
}
if (*this->options.threads == 0u)
{
fprintf(stderr, "Error: option %s must be at least 1\n", flag_defs[flag_idx].flag);
return 2;
}
return 0;
case flag_width:
case flag_width_l:
if (!parse_size(flag_defs[flag_idx].flag, arg_value, &this->options.width))
{
return 2;
}
return 0;
case flag_version:
printf("fart, version 0.0.0.0.0.1\n");
return 1;
}
fprintf(stderr, "Internal error: unhandled flag_idx=%zu\n", flag_idx);
return 2;
}
+10 -1
View File
@@ -1,7 +1,16 @@
#pragma once
#include <fart/Options.h>
class Cli
{
public:
Cli();
Options options;
string_view input_file;
opt<string_view> output_file;
int run(int argc, char * argv[]);
private:
int handle_option(size_t option_idx, const char * arg_value);
};
+80 -1
View File
@@ -1,5 +1,84 @@
#include <fart/Engine.h>
#include <fart/Parser.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
Engine::Engine()
Engine::Engine(const Options & options)
: options(options)
{
}
Engine::~Engine()
{
delete scene;
}
int Engine::run(string_view input_file, opt<string_view> output_file)
{
string output_path;
if (output_file)
{
output_path = *output_file;
}
else
{
string_view base = input_file;
if (base.ends_with(".fart"))
{
base.remove_suffix(5u);
}
output_path = string(base) + ".png";
}
image_writer.reset(ImageWriter::build(output_path));
if (!image_writer)
{
return 1;
}
/* Verify the output file can be written before rendering. */
FILE * fh = fopen(output_path.c_str(), "wb");
if (fh == nullptr)
{
fprintf(stderr, "Error opening output file '%s': %s\n", output_path.c_str(), strerror(errno));
return 1;
}
if (fclose(fh) != 0)
{
fprintf(stderr, "Error closing output file '%s': %s\n", output_path.c_str(), strerror(errno));
return 1;
}
if (remove(output_path.c_str()) != 0)
{
fprintf(stderr, "Error removing output file '%s': %s\n", output_path.c_str(), strerror(errno));
return 1;
}
delete scene;
scene = Parser::load_scene(input_file);
if (!scene)
{
return 1;
}
/* CLI options take precedence over scene options; defaults fill the rest. */
options.merge(scene->options);
options.apply_defaults();
Image image(*options.width, *options.height);
for (size_t y = 0u; y < *options.height; y++)
{
for (size_t x = 0u; x < *options.width; x++)
{
image[x, y] = *options.background;
}
}
if (!image_writer->write(image))
{
return 1;
}
return 0;
}
+13 -1
View File
@@ -1,7 +1,19 @@
#pragma once
#include <fart/ImageWriter.h>
#include <fart/Options.h>
#include <fart/Scene.h>
#include <memory>
class Engine
{
public:
Engine();
Engine(const Options & options);
~Engine();
int run(string_view input_file, opt<string_view> output_file);
protected:
Options options;
std::unique_ptr<ImageWriter> image_writer;
Scene * scene = nullptr;
};
+1 -2
View File
@@ -2,13 +2,12 @@
#include <fart/Color.h>
#include <stddef.h>
#include <vector>
struct Image
{
size_t width;
size_t height;
std::vector<Color> pixels;
vector<Color> pixels;
Image(size_t width, size_t height);
Color & operator[](size_t x, size_t y)
+2 -2
View File
@@ -3,9 +3,9 @@
#include <stdio.h>
#include <filesystem>
ImageWriter * ImageWriter::build(const std::string & fname)
ImageWriter * ImageWriter::build(const string & fname)
{
std::string ext = std::filesystem::path(fname).extension().string();
string ext = std::filesystem::path(fname).extension().string();
if (ext == ".png")
{
return new PngImageWriter(fname);
+3 -3
View File
@@ -6,11 +6,11 @@
class ImageWriter
{
public:
static ImageWriter * build(const std::string & fname);
static ImageWriter * build(const string & fname);
virtual ~ImageWriter() = default;
virtual bool write(const Image & image) = 0;
protected:
ImageWriter(const std::string & fname) : fname(fname) {}
std::string fname;
ImageWriter(const string & fname) : fname(fname) {}
string fname;
};
+44
View File
@@ -0,0 +1,44 @@
#include <fart/Options.h>
#include <thread>
void Options::merge(const Options & other)
{
if (!width)
{
width = other.width;
}
if (!height)
{
height = other.height;
}
if (!threads)
{
threads = other.threads;
}
if (!background)
{
background = other.background;
}
}
void Options::apply_defaults()
{
if (!width)
{
width = 800u;
}
if (!height)
{
height = 600u;
}
if (!threads)
{
/* hardware_concurrency() may return 0 if it cannot be determined. */
size_t n = std::thread::hardware_concurrency();
threads = n > 0u ? n : 1u;
}
if (!background)
{
background = Color{0.0, 0.0, 0.0, 1.0};
}
}
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <fart/Color.h>
#include <stddef.h>
struct Options
{
opt<size_t> width;
opt<size_t> height;
opt<size_t> threads;
opt<Color> background;
/* Set each field that is not already set to the value from other. */
void merge(const Options & other);
/* Fill in any unset fields with default values. */
void apply_defaults();
};
+147
View File
@@ -0,0 +1,147 @@
#include <fart/Parser.h>
#include <fart/propane.h>
#include <fart/read_file.h>
#include <stdint.h>
#include <stdio.h>
Scene * Parser::load_scene(const string_view & input_file)
{
opt<vector<uint8_t>> input = read_file(input_file);
if (!input)
{
return nullptr;
}
p_context_t * context = p_context_new(input->data(), input->size());
size_t result = p_parse(context);
p_position_t pos = p_position(context);
int fname_len = (int)input_file.size();
const char * fname = input_file.data();
bool success = false;
switch (result)
{
case P_SUCCESS:
success = true;
break;
case P_DECODE_ERROR:
fprintf(stderr, "%.*s:%u:%u: error: invalid UTF-8 encoding\n",
fname_len, fname, pos.row, pos.col);
break;
case P_UNEXPECTED_INPUT:
fprintf(stderr, "%.*s:%u:%u: error: unexpected input\n",
fname_len, fname, pos.row, pos.col);
break;
case P_UNEXPECTED_TOKEN:
{
p_token_t token = p_token(context);
if (token == TOKEN___EOF)
{
fprintf(stderr, "%.*s:%u:%u: error: unexpected end of input\n",
fname_len, fname, pos.row, pos.col);
}
else
{
fprintf(stderr, "%.*s:%u:%u: error: unexpected token '%s'\n",
fname_len, fname, pos.row, pos.col, p_token_names[token]);
}
break;
}
case P_USER_TERMINATED:
if (p_user_terminate_code(context) == PARSER_TERMINATE_NUMBER_RANGE)
{
fprintf(stderr, "%.*s:%u:%u: error: number out of range\n",
fname_len, fname, pos.row, pos.col);
}
else
{
fprintf(stderr, "%.*s:%u:%u: internal error: unknown terminate code %zu\n",
fname_len, fname, pos.row, pos.col, p_user_terminate_code(context));
}
break;
default:
fprintf(stderr, "%.*s:%u:%u: internal error: unknown parser result %zu\n",
fname_len, fname, pos.row, pos.col, result);
break;
}
if (!success)
{
p_context_delete(context);
return nullptr;
}
Scene * scene = build_scene(input_file, p_result(context));
p_context_delete(context);
return scene;
}
/* Convert a uint token value to a size_t, reporting an error if it does not
* fit. */
static bool token_to_size(const string_view & input_file, PToken token, opt<size_t> * out)
{
/* SIZE_MAX + 1, computed so that it is exactly representable as a double. */
const double size_limit = (double)(SIZE_MAX / 2u + 1u) * 2.0;
double value = token.pvalue();
if (value >= size_limit)
{
p_position_t pos = token.position();
fprintf(stderr, "%.*s:%u:%u: error: value out of range\n",
(int)input_file.size(), input_file.data(), pos.row, pos.col);
return false;
}
*out = (size_t)value;
return true;
}
static Color vector_to_color(PVector vector)
{
Color color{
vector.pNumber2().pToken().pvalue(),
vector.pNumber4().pToken().pvalue(),
vector.pNumber6().pToken().pvalue(),
};
if (PNumber a = vector.pNumber8(); p_node_valid(a))
{
color.a = a.pToken().pvalue();
}
return color;
}
Scene * Parser::build_scene(const string_view & input_file, PScene pscene)
{
Scene * scene = new Scene();
/* If the options block is absent, pOptions() returns a null node and so
* does pOptionsItems() on it, so the loop body is skipped. */
for (POptionsItems items = pscene.pOptions().pOptionsItems();
p_node_valid(items);
items = items.pOptionsItems())
{
POptionsItem item = items.pOptionsItem();
bool ok = true;
if (PBackground background = item.pBackground(); p_node_valid(background))
{
scene->options.background = vector_to_color(background.pVector());
}
else if (PHeight height = item.pHeight(); p_node_valid(height))
{
ok = token_to_size(input_file, height.pToken2(), &scene->options.height);
}
else if (PWidth width = item.pWidth(); p_node_valid(width))
{
ok = token_to_size(input_file, width.pToken2(), &scene->options.width);
}
if (!ok)
{
delete scene;
return nullptr;
}
}
return scene;
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <fart/Scene.h>
struct PScene;
class Parser
{
public:
static Scene * load_scene(const string_view & input_file);
private:
static Scene * build_scene(const string_view & input_file, PScene pscene);
};
+1 -2
View File
@@ -4,7 +4,6 @@
#include <stdio.h>
#include <algorithm>
#include <cmath>
#include <vector>
static uint8_t to_byte(double v)
{
@@ -13,7 +12,7 @@ static uint8_t to_byte(double v)
bool PngImageWriter::write(const Image & image)
{
std::vector<uint8_t> buffer(image.width * image.height * 4u);
vector<uint8_t> buffer(image.width * image.height * 4u);
for (size_t i = 0u; i < image.pixels.size(); i++)
{
const Color & c = image.pixels[i];
+1 -1
View File
@@ -5,6 +5,6 @@
class PngImageWriter : public ImageWriter
{
public:
PngImageWriter(const std::string & fname) : ImageWriter(fname) {}
PngImageWriter(const string & fname) : ImageWriter(fname) {}
bool write(const Image & image) override;
};
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <fart/Options.h>
class Scene
{
public:
Options options;
};
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <vector>
using string = std::string;
using string_view = std::string_view;
template <typename T> using opt = std::optional<T>;
template <typename T> using vector = std::vector<T>;
+12 -3
View File
@@ -5,7 +5,16 @@
int main(int argc, char * argv[])
{
Cli cli;
Engine engine;
printf("Hello\n");
return 0;
int rc = cli.run(argc, argv);
if (rc > 0)
{
return rc - 1;
}
if (cli.input_file.empty())
{
fprintf(stderr, "Error: no input file specified\n");
return 1;
}
Engine engine(cli.options);
return engine.run(cli.input_file, cli.output_file);
}
+32
View File
@@ -0,0 +1,32 @@
#include <fart/read_file.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
opt<vector<uint8_t>> read_file(string_view fname)
{
string path(fname);
FILE * fh = fopen(path.c_str(), "rb");
if (fh == nullptr)
{
fprintf(stderr, "Error opening input file '%s': %s\n", path.c_str(), strerror(errno));
return std::nullopt;
}
vector<uint8_t> buffer;
uint8_t chunk[4096];
size_t n;
while ((n = fread(chunk, 1u, sizeof(chunk), fh)) > 0u)
{
buffer.insert(buffer.end(), chunk, chunk + n);
}
if (ferror(fh))
{
fprintf(stderr, "Error reading input file '%s': %s\n", path.c_str(), strerror(errno));
fclose(fh);
return std::nullopt;
}
fclose(fh);
return buffer;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <stdint.h>
opt<vector<uint8_t>> read_file(string_view fname);