#include "commands.h" #include "cmd_util.h" #include #include #include #include "audit.h" #include "config.h" #include "db.h" #include "sha256.h" #include "sie.h" #include "util.h" static int hash_file(const char *path, char hex[65], int64_t *size) { FILE *f = fopen(path, "rb"); if (!f) return -1; SHA256_CTX ctx; sha256_init(&ctx); unsigned char chunk[65536]; size_t rn; int64_t total = 0; while ((rn = fread(chunk, 1, sizeof chunk, f)) > 0) { sha256_update(&ctx, (const BYTE *)chunk, rn); total += (int64_t)rn; } int bad = ferror(f); fclose(f); if (bad) return -1; unsigned char digest[32]; sha256_final(&ctx, digest); util_hex(digest, 32, hex); if (size) *size = total; return 0; } static yyjson_mut_val *h_sie_export(struct req *r) { int64_t fy = 0; if (req_fy(r, &fy) != 0) return NULL; int inline_out = 0; arg_bool(r->args, "inline", &inline_out); if (mkdir_p(g_cfg.export_dir, 0700) != 0) return fail(r, "INTERNAL", "cannot create export directory"); char label[64] = ""; sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2( r->db, "SELECT label FROM fiscal_years WHERE org_id=?1 AND id=?2", -1, &st, NULL) != SQLITE_OK) return fail(r, "INTERNAL", "database error"); sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_int64(st, 2, fy); if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return fail(r, "NOT_FOUND", "fiscal year not found"); } snprintf(label, sizeof label, "%s", sqlite3_column_text(st, 0)); sqlite3_finalize(st); for (char *p = label; *p; p++) if (*p == '/') *p = '-'; char path[4096]; snprintf(path, sizeof path, "%s/%lld_%s.se", g_cfg.export_dir, (long long)r->org_id, label); char *err = NULL; if (sie_export_file(r->db, r->org_id, fy, path, &err) != 0) { yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "export failed"); free(err); return e; } char hex[65]; int64_t size = 0; if (hash_file(path, hex, &size) != 0) return fail(r, "INTERNAL", "could not read exported file"); audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, "sie.export", "{}", "OK", NULL); yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_id", fy); yyjson_mut_obj_add_strcpy(r->rdoc, o, "path", path); yyjson_mut_obj_add_strcpy(r->rdoc, o, "sha256", hex); yyjson_mut_obj_add_int(r->rdoc, o, "size", size); if (inline_out) { size_t flen = 0; unsigned char *content = read_file(path, &flen); if (!content) return fail(r, "INTERNAL", "could not read exported file"); char *b64 = util_b64(content, flen); yyjson_mut_obj_add_strcpy(r->rdoc, o, "content_base64", b64); free(b64); free(content); } return o; } static yyjson_mut_val *h_sie_import(struct req *r) { const char *b64 = arg_str(r->args, "content_base64"); const char *path = arg_str(r->args, "path"); unsigned char *content = NULL; size_t len = 0; if (b64) { if (util_b64_decode(b64, strlen(b64), &content, &len) != 0) return fail(r, "INVALID_ARGS", "content_base64 is not valid base64"); } else if (path) { content = read_file(path, &len); if (!content) return fail(r, "NOT_FOUND", "could not read file"); } else { return fail(r, "INVALID_ARGS", "content_base64 or path is required"); } if (len > 64u * 1024u * 1024u) { free(content); return fail(r, "TOO_LARGE", "SIE file is too large"); } int64_t fy = 0, vouchers = 0, accounts = 0; char *err = NULL; int rc = sie_import_content(r->db, r->org_id, r->sess->user_id, content, len, r->dry_run, &fy, &vouchers, &accounts, &err); free(content); if (rc != 0) { yyjson_mut_val *e = fail(r, "INVALID_ARGS", err ? err : "import failed"); free(err); return e; } if (!r->dry_run) { char *reqjson = audit_args_json(r->args); audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, "sie.import", reqjson, "OK", NULL); free(reqjson); } yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_id", fy); yyjson_mut_obj_add_int(r->rdoc, o, "vouchers", vouchers); yyjson_mut_obj_add_int(r->rdoc, o, "accounts_created", accounts); if (r->dry_run) yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); return o; } static const struct cmd_arg args_sie_export[] = { { "fiscal_year", ARG_INT, 0, NULL, NULL, "Fiscal year id; defaults to the latest" }, { "inline", ARG_BOOL, 0, "false", NULL, "Also return content_base64" }, }; static const struct cmd_arg args_sie_import[] = { { "content_base64", ARG_STR, 0, NULL, NULL, "SIE file content, base64" }, { "path", ARG_STR, 0, NULL, NULL, "Server-side path; alternative to content_base64" }, }; const struct command g_cmd_sie[] = { { "sie.export", "Export SIE 4 (PC8)", PERM_READ, 1, 0, 0, h_sie_export, CMD_ARGS(args_sie_export) }, { "sie.import", "Import SIE 4 into an empty fiscal year", PERM_WRITE, 1, 1, 1, h_sie_import, CMD_ARGS(args_sie_import) }, }; const struct cmd_table g_cmd_table_sie = { g_cmd_sie, sizeof g_cmd_sie / sizeof g_cmd_sie[0] };