Document everything and fix out-of-bounds memory error

This commit is contained in:
Jeremy Baxter 2023-11-28 10:46:59 +13:00
parent 60023c259e
commit 472261e15a
2 changed files with 76 additions and 46 deletions

1
esv.d
View file

@ -82,7 +82,6 @@ run(string[] args)
ushort lines; ushort lines;
string apiKey; string apiKey;
string configPath; string configPath;
string configEnvVar;
string pager; string pager;
string verses; string verses;
Ini iniData; Ini iniData;

115
esvapi.d
View file

@ -30,13 +30,17 @@ import std.stdio : File;
import std.string : capitalize, tr, wrap; import std.string : capitalize, tr, wrap;
import std.net.curl : HTTP; import std.net.curl : HTTP;
/** Indentation style to use when formatting passages. */
enum ESVIndent enum ESVIndent
{ {
SPACE, SPACE,
TAB TAB
} }
const enum string ESVAPI_URL = "https://api.esv.org/v3/passage"; /** Default URL to use when sending API requests. */
enum string ESVAPI_URL = "https://api.esv.org/v3/passage";
/** Constant array of all books in the Bible. */
const string[] BIBLE_BOOKS = [ const string[] BIBLE_BOOKS = [
/* Old Testament */ /* Old Testament */
"Genesis", "Genesis",
@ -108,7 +112,8 @@ const string[] BIBLE_BOOKS = [
"Jude", "Jude",
"Revelation" "Revelation"
]; ];
/* All boolean API parameters */
/** All allowed API parameters (for text passages). */
const string[] ESVAPI_PARAMETERS = [ const string[] ESVAPI_PARAMETERS = [
"include-passage-references", "include-passage-references",
"include-verse-numbers", "include-verse-numbers",
@ -131,7 +136,7 @@ const string[] ESVAPI_PARAMETERS = [
"indent-using", "indent-using",
]; ];
/* /**
* Returns true if the argument book is a valid book of the Bible. * Returns true if the argument book is a valid book of the Bible.
* Otherwise, returns false. * Otherwise, returns false.
*/ */
@ -144,27 +149,32 @@ bookValid(in char[] book) nothrow @safe
} }
return false; return false;
} }
/* /**
* Returns true if the argument book is a valid verse format. * Returns true if the argument verse is a valid verse format.
* Otherwise, returns false. * Otherwise, returns false.
*/ */
bool bool
verseValid(in char[] verse) @safe verseValid(in char[] verse) @safe
{ {
bool foreach (string re; [
vMatch(in string re) @safe "^\\d{1,3}$",
{ "^\\d{1,3}-\\d{1,3}$",
return !verse.matchAll(regex(re)).empty; "^\\d{1,3}:\\d{1,3}$",
} "^\\d{1,3}:\\d{1,3}-\\d{1,3}$"
if (vMatch("^\\d{1,3}$") || ]) {
vMatch("^\\d{1,3}-\\d{1,3}$") || if (!verse.matchAll(regex(re)).empty)
vMatch("^\\d{1,3}:\\d{1,3}$") ||
vMatch("^\\d{1,3}:\\d{1,3}-\\d{1,3}$"))
return true; return true;
}
return false; return false;
} }
/**
* ESV API object containing the authentication key,
* the API URL, any parameters to use when contacting the
* API as well as the temporary directory to use when
* fetching audio passages.
*/
class ESVApi class ESVApi
{ {
protected { protected {
@ -173,10 +183,25 @@ class ESVApi
string _url; string _url;
} }
string extraParameters; /**
int delegate(size_t, size_t, size_t, size_t) onProgress; * Structure that contains associative arrays for each of
* the possible parameter types. Specify API parameters here.
*/
ESVApiOptions opts; ESVApiOptions opts;
/**
* Any additional parameters to append to the request.
* Must start with an ampersand ('&').
*/
string extraParameters;
/**
* Callback function that is called whenever progress is made
* on a request.
*/
int delegate(size_t, size_t, size_t, size_t) onProgress;
/**
* Constructs an ESVApi object using the given authentication key.
*/
this(string key, string tmpName = "esv") this(string key, string tmpName = "esv")
{ {
_key = key; _key = key;
@ -184,14 +209,14 @@ class ESVApi
_url = ESVAPI_URL; _url = ESVAPI_URL;
opts = ESVApiOptions(true); opts = ESVApiOptions(true);
extraParameters = ""; extraParameters = "";
onProgress = (size_t dlTotal, size_t dlNow, onProgress = delegate int (size_t dlTotal, size_t dlNow,
size_t ulTotal, size_t ulNow) size_t ulTotal, size_t ulNow)
{ {
return 0; return 0;
}; };
} }
/* /**
* Returns the API authentication key that was given when the object * Returns the API authentication key that was given when the object
* was constructed. This authentication key cannot be changed. * was constructed. This authentication key cannot be changed.
*/ */
@ -200,7 +225,7 @@ class ESVApi
{ {
return _key; return _key;
} }
/* /**
* Returns the subdirectory used to store temporary audio passages. * Returns the subdirectory used to store temporary audio passages.
*/ */
@property string @property string
@ -208,7 +233,7 @@ class ESVApi
{ {
return _tmp; return _tmp;
} }
/* /**
* Returns the API URL currently in use. * Returns the API URL currently in use.
*/ */
@property string @property string
@ -216,7 +241,7 @@ class ESVApi
{ {
return _url; return _url;
} }
/* /**
* Sets the API URL currently in use to the given url argument. * Sets the API URL currently in use to the given url argument.
*/ */
@property void @property void
@ -225,7 +250,7 @@ class ESVApi
{ {
_url = url; _url = url;
} }
/* /**
* Requests the passage in text format from the API and returns it. * Requests the passage in text format from the API and returns it.
* The (case-insensitive) name of the book being searched are * The (case-insensitive) name of the book being searched are
* contained in the argument book. The verse(s) being looked up are * contained in the argument book. The verse(s) being looked up are
@ -239,18 +264,17 @@ class ESVApi
in (verseValid(verse), "Invalid verse format") in (verseValid(verse), "Invalid verse format")
{ {
char[] params, response; char[] params, response;
HTTP request;
params = []; params = [];
{ {
void *o;
string[] parambuf; string[] parambuf;
void void
addParams(R)(R item) addParams(R)(R item)
{ {
parambuf[parambuf.length] = parambuf.length++;
parambuf[parambuf.length - 1] =
format!"&%s=%s"(item.key, item.value); format!"&%s=%s"(item.key, item.value);
} }
@ -262,7 +286,7 @@ class ESVApi
foreach (item; opts.b.byKeyValue()) foreach (item; opts.b.byKeyValue())
addParams(item); addParams(item);
parambuf[parambuf.length] = parambuf[parambuf.length - 1] =
format!"&indent-using=%s"( format!"&indent-using=%s"(
opts.indent_using == ESVIndent.TAB ? "tab" : "space"); opts.indent_using == ESVIndent.TAB ? "tab" : "space");
@ -279,7 +303,7 @@ class ESVApi
verse) ~ params ~ extraParameters); verse) ~ params ~ extraParameters);
return response.parseJSON()["passages"][0].str; return response.parseJSON()["passages"][0].str;
} }
/* /**
* Requests an audio track of the verse(s) from the API and * Requests an audio track of the verse(s) from the API and
* returns a file path containing an MP3 sound track. * returns a file path containing an MP3 sound track.
* The (case-insensitive) name of the book being searched are * The (case-insensitive) name of the book being searched are
@ -293,21 +317,18 @@ class ESVApi
in (bookValid(book), "Invalid book") in (bookValid(book), "Invalid book")
in (verseValid(verse), "Invalid verse format") in (verseValid(verse), "Invalid verse format")
{ {
char[] response;
File tmpFile; File tmpFile;
HTTP request;
response = makeRequest(format!"audio/?q=%s+%s"( tmpFile = File(_tmp, "w");
tmpFile.write(makeRequest(format!"audio/?q=%s+%s"(
book book
.capitalize() .capitalize()
.tr(" ", "+"), .tr(" ", "+"),
verse) verse)
); ));
tmpFile = File(_tmp, "w");
tmpFile.write(response);
return _tmp; return _tmp;
} }
/* /**
* Requests a passage search for the given query. * Requests a passage search for the given query.
* Returns a string containing JSON data representing * Returns a string containing JSON data representing
* the results of the search. * the results of the search.
@ -317,22 +338,17 @@ class ESVApi
char[] char[]
search(in string query) search(in string query)
{ {
char[] response; return makeRequest("search/?q=" ~ query.tr(" ", "+"));
HTTP request;
JSONValue json;
response = makeRequest("search/?q=" ~ query.tr(" ", "+"));
return response;
} }
/* /**
* Calls search() and formats the results nicely as plain text. * Calls search() and formats the results nicely as plain text.
*/ */
char[] char[]
searchFormat(alias fmt = "\033[1m%s\033[0m\n %s\n") searchFormat(alias fmt = "\033[1m%s\033[0m\n %s\n")
(in string query, int lineLength = 0) /* 0 means default */ (in string query, int lineLength = 0) /* 0 means default */
{ {
JSONValue resp;
char[] layout; char[] layout;
JSONValue resp;
resp = parseJSON(search(query)); resp = parseJSON(search(query));
layout = []; layout = [];
@ -375,12 +391,22 @@ class ESVApi
} }
} }
/**
* Structure containing parameters passed to the ESV API.
*/
struct ESVApiOptions struct ESVApiOptions
{ {
/** Boolean options */
bool[string] b; bool[string] b;
/** Integer options */
int[string] i; int[string] i;
/** Indentation style to use when formatting text passages. */
ESVIndent indent_using; ESVIndent indent_using;
/**
* If initialise is true, initialise an ESVApiOptions
* structure with the default values.
*/
this(bool initialise) nothrow @safe this(bool initialise) nothrow @safe
{ {
if (!initialise) if (!initialise)
@ -408,6 +434,11 @@ struct ESVApiOptions
} }
} }
/**
* Exception thrown on API errors.
* Currently only used when there is no search results
* following a call of searchFormat.
*/
class ESVException : Exception class ESVException : Exception
{ {
mixin basicExceptionCtors; mixin basicExceptionCtors;