diff --git a/man/rgbasm.5 b/man/rgbasm.5 index 950fff14..19f14ba4 100644 --- a/man/rgbasm.5 +++ b/man/rgbasm.5 @@ -599,6 +599,7 @@ with its corresponding argument in .Pq So %% Sc is replaced by the So % Sc character . .It Fn STRCHAR str idx Ta Returns the substring of Ar str No for the charmap entry at Ar idx No with the current charmap . Pq Ar idx No counts charmap entries, not characters. .It Fn REVCHAR vals... Ta Returns the string that is mapped to Ar vals No with the current charmap. If there is no unique charmap entry for Ar vals Ns , an error occurs. +.It Fn READFILE name max Ta Returns the contents of the file Ar name No as a string. Reads up to Ar max No bytes, or the entire contents if Ar max No is not specified. If the file isn't found in the current directory, the include-path list passed to Xr rgbasm 1 Ap s Fl I No option on the command line will be searched. .El .Pp The following functions operate on string expressions, but return integers. @@ -1814,10 +1815,9 @@ Use .Ic INCBIN to include a raw binary file as it is. If the file isn't found in the current directory, the include-path list passed to -.Xr rgbasm 1 -(see the +.Xr rgbasm 1 Ap s .Fl I -option) on the command line will be searched. +option on the command line will be searched. .Bd -literal -offset indent INCBIN "titlepic.bin" INCBIN "sprites/hero.bin" @@ -2370,11 +2370,10 @@ block, all of them but the first one are ignored. Use .Ic INCLUDE to process another assembler file and then return to the current file when done. -If the file isn't found in the current directory, the include path list (see the +If the file isn't found in the current directory, the include-path list passed to +.Xr rgbasm 1 Ap s .Fl I -option in -.Xr rgbasm 1 ) -will be searched. +option on the command line will be searched. You may nest .Ic INCLUDE calls infinitely (or until you run out of memory, whichever comes first). diff --git a/src/asm/lexer.cpp b/src/asm/lexer.cpp index 9800381f..20e17c03 100644 --- a/src/asm/lexer.cpp +++ b/src/asm/lexer.cpp @@ -239,6 +239,7 @@ static std::unordered_map ke {"TZCOUNT", T_(OP_TZCOUNT) }, {"BYTELEN", T_(OP_BYTELEN) }, + {"READFILE", T_(OP_READFILE) }, {"STRBYTE", T_(OP_STRBYTE) }, {"STRCAT", T_(OP_STRCAT) }, {"STRCHAR", T_(OP_STRCHAR) }, diff --git a/src/asm/parser.y b/src/asm/parser.y index 5a9e788d..8b20159f 100644 --- a/src/asm/parser.y +++ b/src/asm/parser.y @@ -38,6 +38,7 @@ #include #include #include + #include #include #include #include @@ -61,6 +62,7 @@ yy::parser::symbol_type yylex(); // Provided by lexer.cpp + static std::optional readFile(std::string const &name, uint32_t maxLen); static uint32_t strToNum(std::vector const &s); static void errorInvalidUTF8Byte(uint8_t byte, char const *functionName); static size_t strlenUTF8(std::string const &str, bool printErrors); @@ -303,6 +305,7 @@ %token OP_LOG "LOG" %token OP_LOW "LOW" %token OP_POW "POW" +%token OP_READFILE "READFILE" %token OP_REVCHAR "REVCHAR" %token OP_ROUND "ROUND" %token OP_SIN "SIN" @@ -1687,6 +1690,20 @@ string_literal: $$ = std::move($1); $$.append($3); } + | OP_READFILE LPAREN string RPAREN { + if (std::optional contents = readFile($3, UINT32_MAX); contents) { + $$ = std::move(*contents); + } else { + YYACCEPT; + } + } + | OP_READFILE LPAREN string COMMA uconst RPAREN { + if (std::optional contents = readFile($3, $5); contents) { + $$ = std::move(*contents); + } else { + YYACCEPT; + } + } | OP_STRSLICE LPAREN string COMMA iconst COMMA iconst RPAREN { size_t len = strlenUTF8($3, false); uint32_t start = adjustNegativeIndex($5, len, "STRSLICE"); @@ -2728,6 +2745,44 @@ void yy::parser::error(std::string const &str) { ::error("%s", str.c_str()); } +static std::optional readFile(std::string const &name, uint32_t maxLen) { + FILE *file = nullptr; + if (std::optional fullPath = fstk_FindFile(name); fullPath) { + file = fopen(fullPath->c_str(), "rb"); + } + if (!file) { + if (fstk_FileError(name, "READFILE")) { + // If `fstk_FileError` returned true due to `-MG`, we should abort due to a + // missing file, so return `std::nullopt`, which tells the caller to `YYACCEPT` + return std::nullopt; + } + return ""; + } + Defer closeFile{[&] { fclose(file); }}; + + size_t readSize = maxLen; + if (fseek(file, 0, SEEK_END) == 0) { + // If the file is seekable and shorter than the max length, + // just read as many bytes as there are + if (long fileSize = ftell(file); static_cast(fileSize) < readSize) { + readSize = fileSize; + } + fseek(file, 0, SEEK_SET); + } else if (errno != ESPIPE) { + error("Error determining size of READFILE file '%s': %s", name.c_str(), strerror(errno)); + } + + std::string contents; + contents.resize(readSize); + + if (fread(&contents[0], 1, readSize, file) < readSize || ferror(file)) { + error("Error reading READFILE file '%s': %s", name.c_str(), strerror(errno)); + return ""; + } + + return contents; +} + static uint32_t strToNum(std::vector const &s) { uint32_t length = s.size(); diff --git a/src/asm/section.cpp b/src/asm/section.cpp index da796ced..146226e0 100644 --- a/src/asm/section.cpp +++ b/src/asm/section.cpp @@ -888,7 +888,7 @@ bool sect_BinaryFile(std::string const &name, uint32_t startPos) { } Defer closeFile{[&] { fclose(file); }}; - if (fseek(file, 0, SEEK_END) != -1) { + if (fseek(file, 0, SEEK_END) == 0) { if (startPos > ftell(file)) { error("Specified start position is greater than length of file '%s'", name.c_str()); return false; @@ -935,7 +935,7 @@ bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t l } Defer closeFile{[&] { fclose(file); }}; - if (fseek(file, 0, SEEK_END) != -1) { + if (fseek(file, 0, SEEK_END) == 0) { if (long fsize = ftell(file); startPos > fsize) { error("Specified start position is greater than length of file '%s'", name.c_str()); return false; diff --git a/test/asm/readfile-binary.asm b/test/asm/readfile-binary.asm new file mode 100644 index 00000000..98788037 --- /dev/null +++ b/test/asm/readfile-binary.asm @@ -0,0 +1,24 @@ +section "tilemap", rom0 +/* +input: +$20 +$01 $03 $05 $07 $09 +$02 $04 $06 $08 $10 +$00 $de $01 $df $80 +5 +*/ +def tilemap equs readfile("readfile-binary.inc.bin") +def area = bytelen(#tilemap) - 2 +def offset = strbyte(#tilemap, 0) +def width = strbyte(#tilemap, area + 1) +db width, area / width +for idx, area + db strbyte(#tilemap, idx + 1) + offset +endr +/* +output: +5, 3 +$21 $23 $25 $27 $29 +$22 $24 $26 $28 $30 +$20 $fe $21 $ff $a0 +*/ diff --git a/test/asm/readfile-binary.inc.bin b/test/asm/readfile-binary.inc.bin new file mode 100644 index 00000000..c1ecacd8 Binary files /dev/null and b/test/asm/readfile-binary.inc.bin differ diff --git a/test/asm/readfile-binary.out.bin b/test/asm/readfile-binary.out.bin new file mode 100644 index 00000000..1b01d7ec --- /dev/null +++ b/test/asm/readfile-binary.out.bin @@ -0,0 +1 @@ +!#%')"$&(0 þ!ÿ  \ No newline at end of file diff --git a/test/asm/readfile-nonexist.asm b/test/asm/readfile-nonexist.asm new file mode 100644 index 00000000..2ce00f87 --- /dev/null +++ b/test/asm/readfile-nonexist.asm @@ -0,0 +1,2 @@ +def s equs readfile("readfile-nonexist.inc") +assert strlen(#s) == 0 diff --git a/test/asm/readfile-nonexist.err b/test/asm/readfile-nonexist.err new file mode 100644 index 00000000..5aad2de5 --- /dev/null +++ b/test/asm/readfile-nonexist.err @@ -0,0 +1,3 @@ +error: readfile-nonexist.asm(1): + Error opening READFILE file 'readfile-nonexist.inc': No such file or directory +Assembly aborted with 1 error! diff --git a/test/asm/readfile.asm b/test/asm/readfile.asm new file mode 100644 index 00000000..8ea1ccfc --- /dev/null +++ b/test/asm/readfile.asm @@ -0,0 +1,5 @@ +def s equs readfile("readfile.inc") +println strupr(#s) ++ "!" + +redef s equs readfile("readfile.inc", 5) +println strrpl(#s, "l", "w") ++ "?" diff --git a/test/asm/readfile.inc b/test/asm/readfile.inc new file mode 100644 index 00000000..95d09f2b --- /dev/null +++ b/test/asm/readfile.inc @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/test/asm/readfile.out b/test/asm/readfile.out new file mode 100644 index 00000000..442ede3a --- /dev/null +++ b/test/asm/readfile.out @@ -0,0 +1,2 @@ +HELLO WORLD! +hewwo?