661 lines
14 KiB
C
661 lines
14 KiB
C
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include "tmpasm.h"
|
|
#include "db.h"
|
|
#include "regex.h"
|
|
#include "openfiles.h"
|
|
#include "libs.h"
|
|
#include "tmpasm_scconfig.h"
|
|
#include "log.h"
|
|
#include "regex.h"
|
|
|
|
#ifndef TMPASM_PATH
|
|
#define TMPASM_PATH "/tmpasm"
|
|
#endif
|
|
|
|
#ifndef IFS_PATH
|
|
#define IFS_PATH TMPASM_PATH "/IFS"
|
|
#endif
|
|
|
|
#ifndef IFS_DEFAULT
|
|
#define IFS_DEFAULT " \t\r\n"
|
|
#endif
|
|
|
|
#ifndef OFS_PATH
|
|
#define OFS_PATH TMPASM_PATH "/OFS"
|
|
#endif
|
|
|
|
#ifndef OFS_DEFAULT
|
|
#define OFS_DEFAULT "\n"
|
|
#endif
|
|
|
|
typedef struct scc_s {
|
|
openfiles_t ofl;
|
|
FILE *fout, *default_fout;
|
|
const char *cwd;
|
|
} scc_t;
|
|
|
|
static const char *scc_runtime_error_fmts[] = {
|
|
/* -0 */ "success scc %s",
|
|
/* -1 */ "\"put\" requires exactly two arguments (got %s)",
|
|
/* -2 */ "not enough arguments for sub; should be \"sub node pattern str\"%s",
|
|
/* -3 */ "regex syntax error: %s",
|
|
/* -4 */ "not enough arguments for uniq; should be \"uniq destnode\" or \"uniq destnode src\"%s",
|
|
/* -5 */ "redir: too many arguments%s",
|
|
/* -6 */ "redir: can't open %s",
|
|
/* -7 */ "exiting due to a previous runtime error occurred in included file %s",
|
|
/* -8 */ "can't include '%s': can't open file",
|
|
/* -9 */ "\"put\" requires two or three arguments (got %s)",
|
|
/* -10 */ "\"order\" requires 4 or 5 arguments: \"order destnode word before|after word\" or \"order destnode src word before|after word\"",
|
|
/* -11 */ "\"uniq\" got too many grouping regular expressions",
|
|
NULL
|
|
};
|
|
|
|
static int print_runtime_error(tmpasm_t *ctx, const char *ifn)
|
|
{
|
|
if (ctx->runtime_error != 0) {
|
|
const char *fmt = tmpasm_runtime_error_fmt(ctx);
|
|
fprintf(stderr, "Runtime error at %s %d:%d: ", ifn, ctx->runtime_error_line, ctx->runtime_error_col);
|
|
fprintf(stderr, fmt, (ctx->runtime_error_data == NULL ? "" : ctx->runtime_error_data));
|
|
fprintf(stderr, "\n");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* allocate and build a full path using ud->cwd and fn */
|
|
static char *scc_path(scc_t *ud, const char *fn)
|
|
{
|
|
if (ud->cwd == NULL)
|
|
return strclone(fn);
|
|
return str_concat("", ud->cwd, "/", fn, NULL);
|
|
}
|
|
|
|
/******** db binding ********/
|
|
|
|
static const char *scc_get(tmpasm_t *ctx, const char *addr)
|
|
{
|
|
(void) ctx; /* not used */
|
|
|
|
if (*addr == '&') { /* return whether exists */
|
|
if (get(addr+1) != NULL)
|
|
return strue;
|
|
return sfalse;
|
|
}
|
|
|
|
if (*addr == '?') { /* safe get: return "" instead of NULL to avoid runtime error */
|
|
const char *res = get(addr+1);
|
|
if (res == NULL)
|
|
return "";
|
|
return res;
|
|
}
|
|
return get(addr);
|
|
}
|
|
|
|
static void scc_set(tmpasm_t *ctx, const char *addr, const char *data)
|
|
{
|
|
(void) ctx; /* not used */
|
|
put(addr, data);
|
|
}
|
|
|
|
static int scc_is_true(tmpasm_t *ctx, const char *data)
|
|
{
|
|
(void) ctx; /* not used */
|
|
return ((strcmp(data, "1") == 0) || istrue(data));
|
|
}
|
|
|
|
static int scc_match(tmpasm_t *ctx, const char *str, const char *pat)
|
|
{
|
|
(void) ctx; /* not used */
|
|
re_comp(pat);
|
|
return re_exec(str);
|
|
}
|
|
|
|
static const char *scc_ifs(tmpasm_t *ctx)
|
|
{
|
|
const char *ifs = get(IFS_PATH);
|
|
(void) ctx; /* not used */
|
|
if (ifs == NULL)
|
|
return IFS_DEFAULT;
|
|
return ifs;
|
|
}
|
|
|
|
static const char *scc_ofs(tmpasm_t *ctx)
|
|
{
|
|
const char *ofs = get(OFS_PATH);
|
|
(void) ctx; /* not used */
|
|
if (ofs == NULL)
|
|
return OFS_DEFAULT;
|
|
return ofs;
|
|
}
|
|
|
|
static const char *scc_next(tmpasm_t *ctx, void **state)
|
|
{
|
|
char **s = (char **)state;
|
|
char *start;
|
|
const char *IFS;
|
|
|
|
IFS = scc_ifs(ctx);
|
|
|
|
/* strip leading whitespace */
|
|
while(chr_inset(**s, IFS)) (*s)++;
|
|
|
|
/* at the end of the string, no more tokens */
|
|
if (**s == '\0')
|
|
return NULL;
|
|
|
|
start = *s;
|
|
|
|
/* skip non-whitespace */
|
|
while(!(chr_inset(**s, IFS)) && (**s != '\0')) (*s)++;
|
|
|
|
if (**s != '\0') {
|
|
**s = '\0';
|
|
(*s)++;
|
|
}
|
|
return start;
|
|
}
|
|
|
|
|
|
static const char *scc_first(tmpasm_t *ctx, void **state, char *list)
|
|
{
|
|
*state = list;
|
|
|
|
return scc_next(ctx, state);
|
|
}
|
|
|
|
|
|
/******** instructions ********/
|
|
static void instr_put(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
char *addr, *val;
|
|
(void) iname; /* not used */
|
|
if (argc != 2) {
|
|
char str[16];
|
|
sprintf(str, "%d", argc);
|
|
tmpasm_runtime_error(ctx, -1, str);
|
|
return;
|
|
}
|
|
addr = tmpasm_arg2str(ctx, argv[0], 1);
|
|
val = tmpasm_arg2str(ctx, argv[1], 0);
|
|
if (*addr != '\0')
|
|
put(addr, val);
|
|
free(addr);
|
|
free(val);
|
|
}
|
|
|
|
static void instr_resolve(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
char *dst, *srca;
|
|
const char *src;
|
|
(void) iname; /* not used */
|
|
if (argc != 2) {
|
|
char str[16];
|
|
sprintf(str, "%d", argc);
|
|
tmpasm_runtime_error(ctx, -1, str);
|
|
return;
|
|
}
|
|
|
|
dst = tmpasm_arg2str(ctx, argv[0], 1);
|
|
srca = tmpasm_arg2str(ctx, argv[1], 0);
|
|
src = scc_get(ctx, srca);
|
|
if (*dst != '\0')
|
|
put(dst, src);
|
|
free(dst);
|
|
free(srca);
|
|
}
|
|
|
|
|
|
static void instr_append(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
char *addr, *val;
|
|
char *sep;
|
|
(void) iname; /* not used */
|
|
|
|
if ((argc < 2) || (argc > 3)) {
|
|
char str[16];
|
|
sprintf(str, "%d", argc);
|
|
tmpasm_runtime_error(ctx, -9, str);
|
|
return;
|
|
}
|
|
addr = tmpasm_arg2str(ctx, argv[0], 1);
|
|
val = tmpasm_arg2str(ctx, argv[1], 0);
|
|
if (argc >= 3)
|
|
sep = tmpasm_arg2str(ctx, argv[2], 0);
|
|
else
|
|
sep = strclone(scc_ofs(ctx));
|
|
if (*addr != '\0') {
|
|
append(addr, sep);
|
|
append(addr, val);
|
|
}
|
|
free(addr);
|
|
free(val);
|
|
free(sep);
|
|
}
|
|
|
|
static void instr_report(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
int n;
|
|
(void) iname; /* not used */
|
|
for(n = 0; n < argc; n++) {
|
|
char *val;
|
|
val = tmpasm_arg2str(ctx, argv[n], 0);
|
|
report("%s", val);
|
|
free(val);
|
|
}
|
|
}
|
|
|
|
static void instr_abort(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
scc_t *ud = (scc_t *)ctx->user_data;
|
|
(void) iname; /* not used */
|
|
(void) argc; /* not used */
|
|
(void) argv; /* not used */
|
|
report("Abort requested by template.\n");
|
|
if (ud->fout) fflush(ud->fout);
|
|
fflush(stdout);
|
|
fflush(stderr);
|
|
abort();
|
|
}
|
|
|
|
static void instr_halt(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
(void) iname; /* not used */
|
|
(void) argc; /* not used */
|
|
(void) argv; /* not used */
|
|
ctx->halt = 1;
|
|
}
|
|
|
|
static void instr_sub(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
char *node, *pat, *err, *csub, *buff, *end;
|
|
const char *start;
|
|
const char *val;
|
|
int score, slen, global;
|
|
|
|
if (argc < 3) {
|
|
tmpasm_runtime_error(ctx, -2, NULL);
|
|
return;
|
|
}
|
|
|
|
node = tmpasm_arg2str(ctx, argv[0], 1);
|
|
pat = tmpasm_arg2str(ctx, argv[1], 0);
|
|
csub = tmpasm_arg2str(ctx, argv[2], 0);
|
|
global = (*iname == 'g');
|
|
|
|
val = get(node);
|
|
if (val == NULL)
|
|
val="";
|
|
err = re_comp(pat);
|
|
if (err != NULL) {
|
|
tmpasm_runtime_error(ctx, -3, err);
|
|
return;
|
|
}
|
|
|
|
slen = strlen(csub);
|
|
if (global)
|
|
buff = malloc(strlen(val)*(slen+3)+32); /* big enough for worst case, when every letter and $ and ^ are replaced with sub */
|
|
else
|
|
buff = malloc(strlen(val)+slen+32); /* only one replacement will be done */
|
|
strcpy(buff, val);
|
|
|
|
start = buff;
|
|
do {
|
|
score = re_exec(start);
|
|
if (score == 0)
|
|
break;
|
|
end = buff + strlen(buff);
|
|
if (eopat[0] - bopat[0] != slen) {
|
|
int mlen = end - eopat[0]+1;
|
|
if (mlen > 0)
|
|
memmove((char *)(bopat[0] + slen), eopat[0], mlen);
|
|
}
|
|
memcpy((char *)bopat[0], csub, slen);
|
|
start = bopat[0] + slen;
|
|
} while(global);
|
|
|
|
buff = realloc(buff, strlen(buff)+1);
|
|
put(node, buff);
|
|
free(buff);
|
|
free(node);
|
|
free(pat);
|
|
free(csub);
|
|
}
|
|
|
|
#define UNIQ_ERE_MAX 16
|
|
static char *uniq_eres[UNIQ_ERE_MAX];
|
|
|
|
static void instr_uniq(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
char *node, *strlist, *buff;
|
|
int eres = 0;
|
|
|
|
if (argc < 1) {
|
|
tmpasm_runtime_error(ctx, -4, NULL);
|
|
return;
|
|
}
|
|
node = tmpasm_arg2str(ctx, argv[0], 1);
|
|
if (argc > 1) {
|
|
int offs = 2;
|
|
|
|
strlist = tmpasm_arg2str(ctx, argv[1], 0);
|
|
if ((argc-offs) >= UNIQ_ERE_MAX) {
|
|
tmpasm_runtime_error(ctx, -11, NULL);
|
|
return;
|
|
}
|
|
while(argc > offs)
|
|
uniq_eres[eres++] = tmpasm_arg2str(ctx, argv[offs++], 0);
|
|
if (eres > 0)
|
|
uniq_eres[eres++] = ".*";
|
|
}
|
|
else
|
|
strlist = strclone(get(node));
|
|
buff = uniq_inc_str(strlist, scc_ifs(ctx), scc_ofs(ctx), (*iname == 's'), eres, uniq_eres);
|
|
put(node, buff);
|
|
free(buff);
|
|
free(strlist);
|
|
free(node);
|
|
}
|
|
|
|
static void instr_order(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
char *node, *strlist, *buff, *w1, *dirs, *w2;
|
|
int offs, dir;
|
|
|
|
if ((argc != 4) && (argc != 5)) {
|
|
tmpasm_runtime_error(ctx, -10, NULL);
|
|
return;
|
|
}
|
|
node = tmpasm_arg2str(ctx, argv[0], 1);
|
|
if (argc > 4) {
|
|
strlist = tmpasm_arg2str(ctx, argv[1], 0);
|
|
offs = 2;
|
|
}
|
|
else {
|
|
strlist = strclone(get(node));
|
|
offs = 1;
|
|
}
|
|
|
|
w1 = tmpasm_arg2str(ctx, argv[offs], 0);
|
|
dirs = tmpasm_arg2str(ctx, argv[offs+1], 0);
|
|
w2 = tmpasm_arg2str(ctx, argv[offs+2], 0);
|
|
|
|
if (strcmp(dirs, "before") == 0)
|
|
dir = -1;
|
|
else if (strcmp(dirs, "after") == 0)
|
|
dir = +1;
|
|
else {
|
|
tmpasm_runtime_error(ctx, -10, NULL);
|
|
return;
|
|
}
|
|
|
|
buff = order_inc_str(strlist, scc_ifs(ctx), w1, dir, w2);
|
|
put(node, buff);
|
|
free(buff);
|
|
free(strlist);
|
|
free(node);
|
|
}
|
|
|
|
static void instr_print(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
int n;
|
|
scc_t *ud = (scc_t *)ctx->user_data;
|
|
(void) iname; /* not used */
|
|
|
|
for(n = 0; n < argc; n++) {
|
|
char *val;
|
|
val = tmpasm_arg2str(ctx, argv[n], 0);
|
|
fprintf(ud->fout, "%s", val);
|
|
free(val);
|
|
}
|
|
}
|
|
|
|
static void instr_print_ternary(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
char *s_cond, *s;
|
|
scc_t *ud = (scc_t *)ctx->user_data;
|
|
(void) iname; /* not used */
|
|
|
|
if ((argc < 2) || (argc > 3)) {
|
|
char str[16];
|
|
sprintf(str, "%d", argc);
|
|
tmpasm_runtime_error(ctx, -1, str);
|
|
return;
|
|
}
|
|
|
|
s_cond = tmpasm_arg2str(ctx, argv[0], 0);
|
|
|
|
if (ctx->cb->is_true(ctx, s_cond))
|
|
s = tmpasm_arg2str(ctx, argv[1], 0);
|
|
else
|
|
s = tmpasm_arg2str(ctx, argv[2], 0);
|
|
|
|
fprintf(ud->fout, "%s", s);
|
|
|
|
free(s_cond);
|
|
free(s);
|
|
}
|
|
|
|
static void scc_tmpasm_parse_(tmpasm_t *ctx, const char *cwd, FILE *fin, FILE *default_fout, FILE *fout)
|
|
{
|
|
scc_t *ud = malloc(sizeof(scc_t));
|
|
memset(&ud->ofl, 0, sizeof(ud->ofl));
|
|
ctx->user_data = ud;
|
|
ud->default_fout = default_fout;
|
|
ud->fout = fout;
|
|
ud->cwd = cwd;
|
|
|
|
for(;;) {
|
|
int c;
|
|
c = fgetc(fin);
|
|
if (c == EOF)
|
|
break;
|
|
tmpasm_gotchar(ctx, c);
|
|
}
|
|
|
|
}
|
|
|
|
void scc_tmpasm_parse(tmpasm_t *ctx, const char *cwd, FILE *fin, FILE *fout)
|
|
{
|
|
scc_tmpasm_parse_(ctx, cwd, fin, fout, fout);
|
|
}
|
|
|
|
#ifndef NO_FILE_IO
|
|
static void instr_include(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
scc_t *ud = (scc_t *)ctx->user_data;
|
|
int n;
|
|
(void) iname; /* not used */
|
|
|
|
for(n = 0; n < argc; n++) {
|
|
char *fn, *path;
|
|
FILE *fin;
|
|
tmpasm_t *child;
|
|
|
|
fn = tmpasm_arg2str(ctx, argv[n], 0);
|
|
if (*fn != '/')
|
|
path = scc_path(ud, fn);
|
|
else
|
|
path = strclone(fn);
|
|
fin = fopen(path, "r");
|
|
if (fin == NULL) {
|
|
tmpasm_runtime_error(ctx, -8, path);
|
|
free(fn);
|
|
free(path);
|
|
return;
|
|
}
|
|
child = tmpasm_init(ctx->cb);
|
|
scc_tmpasm_parse_(child, ud->cwd, fin, ud->default_fout, ud->fout);
|
|
tmpasm_execute(child);
|
|
if (print_runtime_error(child, path) != 0)
|
|
tmpasm_runtime_error(ctx, -7, path);
|
|
tmpasm_uninit(child);
|
|
fclose(fin);
|
|
free(fn);
|
|
free(path);
|
|
}
|
|
}
|
|
|
|
|
|
static void instr_redir(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
char *path, *fn, *mode;
|
|
scc_t *ud = (scc_t *)ctx->user_data;
|
|
(void) iname; /* not used */
|
|
fflush(ud->fout);
|
|
switch(argc) {
|
|
case 0: ud->fout = ud->default_fout; return; /* set redirection to default */
|
|
case 1: mode = strclone("w"); break;
|
|
case 2: mode = tmpasm_arg2str(ctx, argv[1], 0); break;
|
|
default:
|
|
tmpasm_runtime_error(ctx, -5, NULL);
|
|
return;
|
|
}
|
|
|
|
fn = tmpasm_arg2str(ctx, argv[0], 0);
|
|
path = scc_path(ud, fn);
|
|
ud->fout = openfile_open(&ud->ofl, path, mode);
|
|
if (ud->fout == NULL) {
|
|
char *err = malloc(strlen(fn) + strlen(path) + strlen(mode) + 16);
|
|
sprintf(err, "%s (%s) for %s", path, fn, mode);
|
|
tmpasm_runtime_error(ctx, -6, err);
|
|
free(err);
|
|
free(path);
|
|
return;
|
|
}
|
|
free(fn);
|
|
free(mode);
|
|
free(path);
|
|
}
|
|
#endif
|
|
|
|
#ifdef TMPASM_TESTER
|
|
static void instr_unknown(tmpasm_t *ctx, char *iname, int argc, tmpasm_arg_t *argv[])
|
|
{
|
|
printf("ERROR: unknown instruction '%s'\n", iname);
|
|
}
|
|
#endif
|
|
|
|
|
|
/******** interface ********/
|
|
|
|
tmpasm_instr *scc_resolve(tmpasm_t *ctx, const char *name)
|
|
{
|
|
(void) ctx; /* not used */
|
|
/* TODO: make this a hash */
|
|
if (strcmp(name, "put") == 0)
|
|
return instr_put;
|
|
if (strcmp(name, "resolve") == 0)
|
|
return instr_resolve;
|
|
if (strcmp(name, "append") == 0)
|
|
return instr_append;
|
|
if (strcmp(name, "print") == 0)
|
|
return instr_print;
|
|
if (strcmp(name, "print_ternary") == 0)
|
|
return instr_print_ternary;
|
|
#ifndef TMPASM_NO_FILE_IO
|
|
if (strcmp(name, "redir") == 0)
|
|
return instr_redir;
|
|
if (strcmp(name, "include") == 0)
|
|
return instr_include;
|
|
#endif
|
|
if (strcmp(name, "report") == 0)
|
|
return instr_report;
|
|
if (strcmp(name, "abort") == 0)
|
|
return instr_abort;
|
|
if (strcmp(name, "halt") == 0)
|
|
return instr_halt;
|
|
if (strcmp(name, "uniq") == 0)
|
|
return instr_uniq;
|
|
if (strcmp(name, "order") == 0)
|
|
return instr_order;
|
|
if (strcmp(name, "sortuniq") == 0)
|
|
return instr_uniq;
|
|
if ((strcmp(name, "sub") == 0) || (strcmp(name, "gsub") == 0))
|
|
return instr_sub;
|
|
|
|
#ifndef TMPASM_TESTER
|
|
return NULL;
|
|
#else
|
|
return instr_unknown;
|
|
#endif
|
|
}
|
|
|
|
|
|
static const char *scc_err_fmt(tmpasm_t *ctx)
|
|
{
|
|
int code;
|
|
code = -ctx->runtime_error;
|
|
|
|
if ((code < 0) || ((size_t)code > (sizeof(scc_runtime_error_fmts)/sizeof(char *))))
|
|
return NULL;
|
|
return scc_runtime_error_fmts[code];
|
|
}
|
|
|
|
|
|
static void scc_preexec(tmpasm_t *ctx)
|
|
{
|
|
(void) ctx; /* not used */
|
|
db_mkdir(TMPASM_PATH);
|
|
}
|
|
|
|
static void scc_postexec(tmpasm_t *ctx)
|
|
{
|
|
scc_t *ud = (scc_t *)ctx->user_data;
|
|
openfile_free(&ud->ofl);
|
|
free(ud);
|
|
}
|
|
|
|
tmpasm_cb_t scc_cb = {
|
|
scc_get, scc_set, scc_is_true, scc_match, scc_first, scc_next,
|
|
scc_resolve, scc_preexec, scc_postexec, scc_err_fmt
|
|
};
|
|
|
|
int tmpasm(const char *wdir, const char *input, const char *output)
|
|
{
|
|
tmpasm_t *ctx;
|
|
FILE *fin, *fout;
|
|
int ret;
|
|
scc_t ud_tmp;
|
|
char *path;
|
|
|
|
ud_tmp.cwd = wdir;
|
|
|
|
path = scc_path(&ud_tmp, input);
|
|
fin = fopen(path, "r");
|
|
if (fin == NULL) {
|
|
fprintf(stderr, "ERROR: tmpasm: can not open script '%s' (%s in %s)\n", path, input, wdir);
|
|
free(path);
|
|
return -1;
|
|
}
|
|
free(path);
|
|
|
|
path = scc_path(&ud_tmp, output);
|
|
fout = fopen(path, "w");
|
|
if (fout == NULL) {
|
|
fprintf(stderr, "ERROR: tmpasm: can not open output '%s' (%s in %s)\n", path, output, wdir);
|
|
free(path);
|
|
return -1;
|
|
}
|
|
free(path);
|
|
|
|
ctx = tmpasm_init(&scc_cb);
|
|
scc_tmpasm_parse_(ctx, wdir, fin, fout, fout);
|
|
if (!ctx->dead)
|
|
tmpasm_execute(ctx);
|
|
fclose(fin);
|
|
fclose(fout);
|
|
|
|
ret = print_runtime_error(ctx, input);
|
|
|
|
tmpasm_uninit(ctx);
|
|
return ret;
|
|
}
|
|
|
|
FILE *tmpasm_fout(tmpasm_t *ctx)
|
|
{
|
|
scc_t *ud = (scc_t *)ctx->user_data;
|
|
return ud->fout;
|
|
}
|