summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GNUmakefile.am1
-rw-r--r--src/fl_load.c377
-rw-r--r--src/fl_util.c308
-rw-r--r--src/ncdc.h1
4 files changed, 380 insertions, 307 deletions
diff --git a/GNUmakefile.am b/GNUmakefile.am
index 70d6f68..83d9abc 100644
--- a/GNUmakefile.am
+++ b/GNUmakefile.am
@@ -25,6 +25,7 @@ ncdc_SOURCES=\
src/commands.c\
src/db.c\
src/dl.c\
+ src/fl_load.c\
src/fl_local.c\
src/fl_save.c\
src/fl_util.c\
diff --git a/src/fl_load.c b/src/fl_load.c
new file mode 100644
index 0000000..8729473
--- /dev/null
+++ b/src/fl_load.c
@@ -0,0 +1,377 @@
+/* ncdc - NCurses Direct Connect client
+
+ Copyright (c) 2011-2012 Yoran Heling
+
+ 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.
+
+*/
+
+
+#include "ncdc.h"
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <bzlib.h>
+
+
+#define S_START 0 // waiting for <FileListing>
+#define S_FLOPEN 1 // In a <FileListing ..>
+#define S_DIROPEN 2 // In a <Directory ..>
+#define S_INDIR 3 // In a <Directory>..</Directory> or <FileListing>..</FileListing>
+#define S_FILEOPEN 4 // In a <File ..>
+#define S_INFILE 5 // In a <File>..</File>
+#define S_UNKNOWN 6 // In some tag we didn't recognize
+#define S_END 7 // Received </FileListing>
+
+struct ctx {
+ BZFILE *fh_bz;
+ FILE *fh_f;
+ gboolean eof;
+
+ gboolean local;
+ int state;
+ char *name;
+ char filetth[24];
+ gboolean filehastth;
+ guint64 filesize;
+ gboolean dirincomplete;
+ struct fl_list *root;
+ struct fl_list *cur;
+ int unknown_level;
+};
+
+
+static int readcb(void *context, char *buf, int len, GError **err) {
+ struct ctx *x = context;
+
+ if(x->fh_bz) {
+ if(x->eof)
+ return 0;
+ int bzerr;
+ int r = BZ2_bzRead(&bzerr, x->fh_bz, buf, len);
+ if(bzerr != BZ_OK && bzerr != BZ_STREAM_END) {
+ g_set_error(err, 1, 0, "bzip2 decompression error (%d): %s", bzerr, g_strerror(errno));
+ return -1;
+ }
+ if(bzerr == BZ_STREAM_END)
+ x->eof = TRUE;
+ return r;
+
+ }
+
+ int r = fread(buf, 1, len, x->fh_f);
+ if(r < 0 && feof(x->fh_f))
+ r = 0;
+ if(r < 0)
+ g_set_error(err, 1, 0, "Read error: %s", g_strerror(errno));
+ return r;
+}
+
+
+static int entitycb(void *context, int type, const char *arg1, const char *arg2, GError **err) {
+ struct ctx *x = context;
+ //printf("%d,%d: %s, %s\n", x->state, type, arg1, arg2);
+ switch(x->state) {
+
+ // The first token must always be a <FileListing>
+ case S_START:
+ if(type == XMLT_OPEN && strcmp(arg1, "FileListing") == 0) {
+ x->state = S_FLOPEN;
+ return 0;
+ }
+ break;
+
+ // Any attributes in a <FileListing> are currently ignored.
+ case S_FLOPEN:
+ if(type == XMLT_ATTR)
+ return 0;
+ if(type == XMLT_ATTDONE) {
+ x->state = S_INDIR;
+ return 0;
+ }
+ break;
+
+ // Handling the attributes of a Directory element.
+ case S_DIROPEN:
+ if(type == XMLT_ATTR && strcmp(arg1, "Name") == 0 && !x->name) {
+ if(!g_utf8_validate(arg2, -1, NULL)) {
+ g_set_error(err, 1, 0, "Invalid UTF-8");
+ return -1;
+ }
+ x->name = g_strdup(arg2);
+ return 0;
+ }
+ if(type == XMLT_ATTDONE) {
+ if(!x->name) {
+ g_set_error(err, 1, 0, "Missing Name attribute in Directory element");
+ return -1;
+ }
+ // Create the directory entry
+ struct fl_list *new = fl_list_create(x->name, FALSE);
+ new->isfile = FALSE;
+ new->sub = g_ptr_array_new_with_free_func(fl_list_free);
+ fl_list_add(x->cur, new, -1);
+ x->cur = new;
+
+ g_free(x->name);
+ x->name = NULL;
+ x->state = S_INDIR;
+ return 0;
+ }
+ // Ignore unknown or duplicate attributes.
+ if(type == XMLT_ATTR)
+ return 0;
+ break;
+
+ // In a directory listing.
+ case S_INDIR:
+ if(type == XMLT_OPEN && strcmp(arg1, "Directory") == 0) {
+ x->state = S_DIROPEN;
+ return 0;
+ }
+ if(type == XMLT_OPEN && strcmp(arg1, "File") == 0) {
+ x->state = S_FILEOPEN;
+ return 0;
+ }
+ if(type == XMLT_OPEN) {
+ x->state = S_UNKNOWN;
+ x->unknown_level = 1;
+ return 0;
+ }
+ if(type == XMLT_CLOSE) {
+ char *expect = x->root == x->cur ? "FileListing" : "Directory";
+ if(arg1 && strcmp(arg1, expect) != 0) {
+ g_set_error(err, 1, 0, "Invalid close tag, expected </%s> but got </%s>", expect, arg1);
+ return -1;
+ }
+ fl_list_sort(x->cur);
+ if(x->cur == x->root)
+ x->state = S_END;
+ else
+ x->cur = x->cur->parent;
+ return 0;
+ }
+ break;
+
+ // Handling the attributes of a File element. (If there are multiple
+ // attributes with the same name, only the first is used.)
+ case S_FILEOPEN:
+ if(type == XMLT_ATTR && strcmp(arg1, "Name") == 0 && !x->name) {
+ if(!g_utf8_validate(arg2, -1, NULL)) {
+ g_set_error(err, 1, 0, "Invalid UTF-8");
+ return -1;
+ }
+ x->name = g_strdup(arg2);
+ return 0;
+ }
+ if(type == XMLT_ATTR && strcmp(arg1, "TTH") == 0 && !x->filehastth) {
+ if(!istth(arg2)) {
+ g_set_error(err, 1, 0, "Invalid TTH");
+ return -1;
+ }
+ base32_decode(arg2, x->filetth);
+ x->filehastth = TRUE;
+ return 0;
+ }
+ if(type == XMLT_ATTR && strcmp(arg1, "Size") == 0 && x->filesize == G_MAXUINT64) {
+ char *end = NULL;
+ x->filesize = g_ascii_strtoull(arg2, &end, 10);
+ if(!end || *end) {
+ g_set_error(err, 1, 0, "Invalid file size");
+ return -1;
+ }
+ return 0;
+ }
+ if(type == XMLT_ATTDONE) {
+ if(!x->name || !x->filehastth || x->filesize == G_MAXUINT64) {
+ g_set_error(err, 1, 0, "Missing %s attribute in File element",
+ !x->name ? "Name" : !x->filehastth ? "TTH" : "Size");
+ return -1;
+ }
+ // Create the file entry
+ struct fl_list *new = fl_list_create(x->name, x->local);
+ new->isfile = TRUE;
+ new->size = x->filesize;
+ new->hastth = TRUE;
+ memcpy(new->tth, x->filetth, 24);
+ fl_list_add(x->cur, new, -1);
+
+ x->filehastth = FALSE;
+ x->filesize = G_MAXUINT64;
+ g_free(x->name);
+ x->name = NULL;
+ x->state = S_INFILE;
+ return 0;
+ }
+ // Ignore unknown or duplicate attributes.
+ if(type == XMLT_ATTR)
+ return 0;
+ break;
+
+ // In a File element. Nothing is allowed here exept a close of the File
+ // element. (Really?)
+ case S_INFILE:
+ if(type == XMLT_CLOSE && (!arg1 || strcmp(arg1, "File") == 0)) {
+ x->state = S_INDIR;
+ return 0;
+ }
+ break;
+
+ // No idea in what kind of tag we are, just count start/end tags so we can
+ // continue parsing when we're out of this unknown tag.
+ case S_UNKNOWN:
+ if(type == XMLT_OPEN)
+ x->unknown_level++;
+ else if(type == XMLT_CLOSE && !--x->unknown_level)
+ x->state = S_INDIR;
+ return 0;
+ }
+
+ g_set_error(err, 1, 0, "Unexpected token in state %s: %s, %s",
+ x->state == S_START ? "START" :
+ x->state == S_FLOPEN ? "FLOPEN" :
+ x->state == S_DIROPEN ? "DIROPEN" :
+ x->state == S_INDIR ? "INDIR" :
+ x->state == S_FILEOPEN ? "FILEOPEN" :
+ x->state == S_INFILE ? "INFILE" :
+ x->state == S_END ? "END" : "UNKNOWN",
+ type == XMLT_OPEN ? "OPEN" :
+ type == XMLT_CLOSE ? "CLOSE" :
+ type == XMLT_ATTR ? "ATTR" :
+ type == XMLT_ATTDONE ? "ATTDONE" : "???",
+ arg1 ? arg1 : "<NULL>");
+ return -1;
+}
+
+
+static int ctx_open(struct ctx *x, const char *file, GError **err) {
+ memset(x, 0, sizeof(struct ctx));
+
+ // open file
+ x->fh_f = fopen(file, "r");
+ if(!x->fh_f) {
+ g_set_error_literal(err, 1, 0, g_strerror(errno));
+ return -1;
+ }
+
+ // open BZ2 decompression
+ if(strlen(file) > 4 && strcmp(file+(strlen(file)-4), ".bz2") == 0) {
+ int bzerr;
+ x->fh_bz = BZ2_bzReadOpen(&bzerr, x->fh_f, 0, 0, NULL, 0);
+ if(bzerr != BZ_OK) {
+ g_set_error(err, 1, 0, "Unable to open bzip2 file (%d): %s", bzerr, g_strerror(errno));
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+
+static void ctx_close(struct ctx *x) {
+ if(x->fh_bz) {
+ int bzerr;
+ BZ2_bzReadClose(&bzerr, x->fh_bz);
+ }
+
+ if(x->fh_f)
+ fclose(x->fh_f);
+
+ if(x->name)
+ g_free(x->name);
+}
+
+
+struct fl_list *fl_load(const char *file, GError **err, gboolean local) {
+ g_return_val_if_fail(err == NULL || *err == NULL, NULL);
+
+ struct ctx x;
+ GError *ierr = NULL;
+ if(ctx_open(&x, file, &ierr))
+ goto end;
+
+ x.state = S_START;
+ x.root = fl_list_create("", FALSE);
+ x.root->sub = g_ptr_array_new_with_free_func(fl_list_free);
+ x.cur = x.root;
+ x.filesize = G_MAXUINT64;
+ x.local = local;
+
+ if(xml_parse(entitycb, readcb, &x, &ierr))
+ goto end;
+
+end:
+ g_return_val_if_fail(ierr || x.state == S_END, NULL);
+ ctx_close(&x);
+ if(ierr) {
+ g_propagate_error(err, ierr);
+ if(x.root)
+ fl_list_free(x.root);
+ x.root = NULL;
+ }
+ return x.root;
+}
+
+
+
+
+
+// Async version of fl_load(). Performs the load in a background thread. Only
+// used for non-local filelists.
+
+struct async_dat {
+ char *file;
+ void (*cb)(struct fl_list *, GError *, void *);
+ void *dat;
+ GError *err;
+ struct fl_list *fl;
+};
+
+
+static gboolean async_d(gpointer dat) {
+ struct async_dat *arg = dat;
+ arg->cb(arg->fl, arg->err, arg->dat);
+ g_free(arg->file);
+ g_slice_free(struct async_dat, arg);
+ return FALSE;
+}
+
+
+static void async_f(gpointer dat, gpointer udat) {
+ struct async_dat *arg = dat;
+ arg->fl = fl_load(arg->file, &arg->err, FALSE);
+ g_idle_add(async_d, arg);
+}
+
+
+// Ownership of both the file list and the error is passed to the callback
+// function.
+void fl_load_async(const char *file, void (*cb)(struct fl_list *, GError *, void *), void *dat) {
+ static GThreadPool *pool = NULL;
+ if(!pool)
+ pool = g_thread_pool_new(async_f, NULL, 2, FALSE, NULL);
+ struct async_dat *arg = g_slice_new0(struct async_dat);
+ arg->file = g_strdup(file);
+ arg->dat = dat;
+ arg->cb = cb;
+ g_thread_pool_push(pool, arg, NULL);
+}
+
diff --git a/src/fl_util.c b/src/fl_util.c
index 909a997..74d38e6 100644
--- a/src/fl_util.c
+++ b/src/fl_util.c
@@ -25,12 +25,7 @@
#include "ncdc.h"
-#include <errno.h>
-#include <stdio.h>
-#include <unistd.h>
-#include <libxml/xmlreader.h>
-#include <libxml/xmlwriter.h>
-#include <bzlib.h>
+#include <string.h>
#if INTERFACE
@@ -476,304 +471,3 @@ gboolean fl_search_match_full(struct fl_list *fl, struct fl_search *s) {
return r;
}
-
-
-
-
-
-
-// Internal structure used by fl_load()
-
-struct fl_load_context {
- char *file; // some name, for debugging purposes
- BZFILE *fh_bz; // if BZ2 compression is enabled (implies fh_h!=NULL)
- FILE *fh_f; // if we're working with a file
- GError **err;
- gboolean stream_end;
-};
-
-
-
-
-// Read filelist from an xml file
-
-// Tries to "fix" invalid byte UTF-8 sequences, as present in some FlylinkDC++
-// generated lists.
-static void fl_load_fixinput(char *buf, int len) {
- while(len > 1) {
- if(*buf == 0x1d)
- *buf = '?';
- buf++;
- len--;
- }
-}
-
-static int fl_load_input(void *context, char *buf, int len) {
- struct fl_load_context *xc = context;
- if(xc->stream_end)
- return 0;
- int bzerr;
- if(xc->fh_bz) {
- int r = BZ2_bzRead(&bzerr, xc->fh_bz, buf, len);
- if(bzerr != BZ_OK && bzerr != BZ_STREAM_END) {
- g_set_error(xc->err, 1, 0, "bzip2 decompression error. (%d)", bzerr);
- return -1;
- } else {
- xc->stream_end = bzerr == BZ_STREAM_END;
- fl_load_fixinput(buf, r);
- return r;
- }
- } else if(xc->fh_f) {
- int r = fread(buf, 1, len, xc->fh_f);
- if(r < 0)
- g_set_error(xc->err, 1, 0, "Read error: %s", g_strerror(errno));
- fl_load_fixinput(buf, r);
- xc->stream_end = r <= 0;
- return r;
- } else
- g_return_val_if_reached(-1);
-}
-
-
-static int fl_load_close(void *context) {
- struct fl_load_context *xc = context;
- int bzerr;
- if(xc->fh_bz)
- BZ2_bzReadClose(&bzerr, xc->fh_bz);
- fclose(xc->fh_f);
- g_free(xc->file);
- g_slice_free(struct fl_load_context, xc);
- return 0;
-}
-
-
-static void fl_load_error(void *arg, const char *msg, xmlParserSeverities severity, xmlTextReaderLocatorPtr locator) {
- struct fl_load_context *xc = arg;
- if(severity == XML_PARSER_SEVERITY_VALIDITY_WARNING || severity == XML_PARSER_SEVERITY_WARNING)
- g_warning("XML parse warning in %s line %d: %s", xc->file, xmlTextReaderLocatorLineNumber(locator), msg);
- else if(xc->err && !*(xc->err))
- g_set_error(xc->err, 1, 0, "XML parse error on input line %d: %s", xmlTextReaderLocatorLineNumber(locator), msg);
-}
-
-
-static int fl_load_handle(xmlTextReaderPtr reader, gboolean *havefl, gboolean *newdir, gboolean *flo, struct fl_list **cur, gboolean local) {
- struct fl_list *tmp;
- char *attr[3];
- char name[50], *tmpname;
-
- if(!(tmpname = (char *)xmlTextReaderName(reader)))
- return -1;
- strncpy(name, tmpname, 50);
- name[49] = 0;
- free(tmpname);
-
- switch(xmlTextReaderNodeType(reader)) {
-
- case XML_READER_TYPE_ELEMENT:
- // No open elements in a <File> allowed
- if(*flo)
- return -1;
- // <FileListing ..>
- // We ignore its attributes (for now)
- if(strcmp(name, "FileListing") == 0) {
- if(*havefl)
- return -1;
- *havefl = TRUE;
- // <Directory ..>
- } else if(strcmp(name, "Directory") == 0) {
- if(!*havefl)
- return -1;
- if(!(attr[0] = (char *)xmlTextReaderGetAttribute(reader, (xmlChar *)"Name")))
- return -1;
- attr[1] = (char *)xmlTextReaderGetAttribute(reader, (xmlChar *)"Incomplete");
- if(attr[1] && strcmp(attr[1], "0") != 0 && strcmp(attr[1], "1") != 0) {
- free(attr[0]);
- return -1;
- }
- tmp = fl_list_create(attr[0], FALSE);
- tmp->isfile = FALSE;
- tmp->sub = g_ptr_array_new_with_free_func(fl_list_free);
- fl_list_add(*newdir ? *cur : (*cur)->parent, tmp, -1);
- *cur = tmp;
- *newdir = !xmlTextReaderIsEmptyElement(reader);
- free(attr[0]);
- free(attr[1]);
- // <File .. />
- } else if(strcmp(name, "File") == 0) {
- if(!*havefl)
- return -1;
- if(!(attr[0] = (char *)xmlTextReaderGetAttribute(reader, (xmlChar *)"Name")))
- return -1;
- attr[1] = (char *)xmlTextReaderGetAttribute(reader, (xmlChar *)"Size");
- if(!attr[1] || strspn(attr[1], "0123456789") != strlen(attr[1])) {
- free(attr[0]);
- return -1;
- }
- attr[2] = (char *)xmlTextReaderGetAttribute(reader, (xmlChar *)"TTH");
- if(!attr[2] || !istth(attr[2])) {
- free(attr[0]);
- free(attr[1]);
- return -1;
- }
- tmp = fl_list_create(attr[0], local);
- tmp->isfile = TRUE;
- tmp->size = g_ascii_strtoull(attr[1], NULL, 10);
- tmp->hastth = TRUE;
- base32_decode(attr[2], tmp->tth);
- fl_list_add(*newdir ? *cur : (*cur)->parent, tmp, -1);
- *cur = tmp;
- *newdir = FALSE;
- free(attr[0]);
- free(attr[1]);
- free(attr[2]);
- if(!xmlTextReaderIsEmptyElement(reader))
- *flo = TRUE;
- }
- break;
-
- case XML_READER_TYPE_END_ELEMENT:
- // </File>
- if(strcmp(name, "File") == 0)
- *flo = FALSE;
- // </Directory>
- else if(strcmp(name, "Directory") == 0) {
- if(!*newdir)
- *cur = (*cur)->parent;
- else
- *newdir = FALSE;
- fl_list_sort(*cur);
- // </FileListing>
- } else if(strcmp(name, "FileListing") == 0) {
- return 0; // stop reading
- }
- break;
- }
- return 1;
-}
-
-
-static xmlTextReaderPtr fl_load_open(const char *file, GError **err) {
- gboolean isbz2 = strlen(file) > 4 && strcmp(file+(strlen(file)-4), ".bz2") == 0;
-
- // open file
- FILE *f = fopen(file, "r");
- if(!f) {
- g_set_error_literal(err, 1, 0, g_strerror(errno));
- return NULL;
- }
-
- // open BZ2 decompression
- BZFILE *bzf = NULL;
- if(isbz2) {
- int bzerr;
- bzf = BZ2_bzReadOpen(&bzerr, f, 0, 0, NULL, 0);
- if(bzerr != BZ_OK) {
- g_set_error(err, 1, 0, "Unable to open BZ2 file (%d)", bzerr);
- fclose(f);
- return NULL;
- }
- }
-
- // create reader
- struct fl_load_context *xc = g_slice_new0(struct fl_load_context);
- xc->err = err;
- xc->file = g_strdup(file);
- xc->fh_f = f;
- xc->fh_bz = bzf;
- xmlTextReaderPtr reader = xmlReaderForIO(fl_load_input, fl_load_close, xc, NULL, NULL, XML_PARSE_NOENT);
-
- if(!reader) {
- fl_load_close(xc);
- if(err && !*err)
- g_set_error_literal(err, 1, 0, "Failed to open file.");
- return NULL;
- }
-
- xmlTextReaderSetErrorHandler(reader, fl_load_error, xc);
- return reader;
-}
-
-
-struct fl_list *fl_load(const char *file, GError **err, gboolean local) {
- g_return_val_if_fail(err == NULL || *err == NULL, NULL);
-
- // open the file
- xmlTextReaderPtr reader = fl_load_open(file, err);
- if(!reader)
- return NULL;
-
- // parse & read
- struct fl_list *cur, *root;
- gboolean havefl = FALSE, newdir = TRUE, flo = FALSE;
- int ret;
-
- root = fl_list_create("", FALSE);
- root->sub = g_ptr_array_new_with_free_func(fl_list_free);
- cur = root;
-
- while((ret = xmlTextReaderRead(reader)) == 1)
- if((ret = fl_load_handle(reader, &havefl, &newdir, &flo, &cur, local)) <= 0)
- break;
-
- if(ret < 0 || !havefl) {
- if(err && !*err) // rather uninformative error message as fallback
- g_set_error_literal(err, 1, 0, !havefl ? "No <FileListing> tag found." : "Error parsing or validating XML.");
- fl_list_free(root);
- root = NULL;
- } else
- fl_list_sort(root);
-
- // close (ignoring errors)
- xmlTextReaderClose(reader);
- xmlFreeTextReader(reader);
-
- return root;
-}
-
-
-
-
-
-// Async version of fl_load(). Performs the load in a background thread. Only
-// used for non-local filelists.
-
-static GThreadPool *fl_load_pool = NULL;
-
-struct fl_load_async_dat {
- char *file;
- void (*cb)(struct fl_list *, GError *, void *);
- void *dat;
- GError *err;
- struct fl_list *fl;
-};
-
-
-static gboolean fl_load_async_d(gpointer dat) {
- struct fl_load_async_dat *arg = dat;
- arg->cb(arg->fl, arg->err, arg->dat);
- g_free(arg->file);
- g_slice_free(struct fl_load_async_dat, arg);
- return FALSE;
-}
-
-
-static void fl_load_async_f(gpointer dat, gpointer udat) {
- struct fl_load_async_dat *arg = dat;
- arg->fl = fl_load(arg->file, &arg->err, FALSE);
- g_idle_add(fl_load_async_d, arg);
-}
-
-
-// Ownership of both the file list and the error is passed to the callback
-// function.
-void fl_load_async(const char *file, void (*cb)(struct fl_list *, GError *, void *), void *dat) {
- if(!fl_load_pool)
- fl_load_pool = g_thread_pool_new(fl_load_async_f, NULL, 2, FALSE, NULL);
- struct fl_load_async_dat *arg = g_slice_new0(struct fl_load_async_dat);
- arg->file = g_strdup(file);
- arg->dat = dat;
- arg->cb = cb;
- g_thread_pool_push(fl_load_pool, arg, NULL);
-}
-
-
diff --git a/src/ncdc.h b/src/ncdc.h
index ddf18b3..84e3fd2 100644
--- a/src/ncdc.h
+++ b/src/ncdc.h
@@ -76,6 +76,7 @@ struct var;
#include "commands.h"
#include "db.h"
#include "dl.h"
+#include "fl_load.h"
#include "fl_local.h"
#include "fl_save.h"
#include "fl_util.h"