293 lines
9.1 KiB
C
293 lines
9.1 KiB
C
/*
|
|
* BUD-08 NIP-94 File Metadata Tags Implementation
|
|
* Handles NIP-94 metadata generation for blob descriptors
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sqlite3.h>
|
|
#include <stdint.h>
|
|
#include "ginxsom.h"
|
|
|
|
// Database path
|
|
#define DB_PATH "db/ginxsom.db"
|
|
|
|
// Check if NIP-94 metadata emission is enabled
|
|
int nip94_is_enabled(void) {
|
|
sqlite3* db;
|
|
sqlite3_stmt* stmt;
|
|
int rc, enabled = 1; // Default enabled
|
|
|
|
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc) {
|
|
return 1; // Default enabled on DB error
|
|
}
|
|
|
|
const char* sql = "SELECT value FROM config WHERE key = 'nip94_enabled'";
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_step(stmt);
|
|
if (rc == SQLITE_ROW) {
|
|
const char* value = (const char*)sqlite3_column_text(stmt, 0);
|
|
enabled = (value && strcmp(value, "true") == 0) ? 1 : 0;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
sqlite3_close(db);
|
|
|
|
return enabled;
|
|
}
|
|
|
|
// Get CDN origin for blob URLs
|
|
int nip94_get_origin(char* out, size_t out_size) {
|
|
if (!out || out_size == 0) {
|
|
return 0;
|
|
}
|
|
|
|
// Check database config first for custom origin
|
|
sqlite3* db;
|
|
sqlite3_stmt* stmt;
|
|
int rc;
|
|
|
|
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_step(stmt);
|
|
if (rc == SQLITE_ROW) {
|
|
const char* value = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (value) {
|
|
strncpy(out, value, out_size - 1);
|
|
out[out_size - 1] = '\0';
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_close(db);
|
|
return 1;
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
sqlite3_close(db);
|
|
}
|
|
|
|
// Check if request came over HTTPS (nginx sets HTTPS=on for SSL requests)
|
|
const char* https_env = getenv("HTTPS");
|
|
const char* server_name = getenv("SERVER_NAME");
|
|
|
|
// Use production domain if SERVER_NAME is set and not localhost
|
|
if (server_name && strcmp(server_name, "localhost") != 0) {
|
|
if (https_env && strcmp(https_env, "on") == 0) {
|
|
snprintf(out, out_size, "https://%s", server_name);
|
|
} else {
|
|
snprintf(out, out_size, "http://%s", server_name);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// Fallback to localhost for development
|
|
if (https_env && strcmp(https_env, "on") == 0) {
|
|
strncpy(out, "https://localhost:9443", out_size - 1);
|
|
} else {
|
|
strncpy(out, "http://localhost:9001", out_size - 1);
|
|
}
|
|
out[out_size - 1] = '\0';
|
|
return 1;
|
|
}
|
|
|
|
// Centralized MIME type to file extension mapping
|
|
const char* mime_to_extension(const char* mime_type) {
|
|
if (!mime_type) {
|
|
return ".bin";
|
|
}
|
|
|
|
if (strstr(mime_type, "image/jpeg")) {
|
|
return ".jpg";
|
|
} else if (strstr(mime_type, "image/webp")) {
|
|
return ".webp";
|
|
} else if (strstr(mime_type, "image/png")) {
|
|
return ".png";
|
|
} else if (strstr(mime_type, "image/gif")) {
|
|
return ".gif";
|
|
} else if (strstr(mime_type, "video/mp4")) {
|
|
return ".mp4";
|
|
} else if (strstr(mime_type, "video/webm")) {
|
|
return ".webm";
|
|
} else if (strstr(mime_type, "audio/mpeg")) {
|
|
return ".mp3";
|
|
} else if (strstr(mime_type, "audio/ogg")) {
|
|
return ".ogg";
|
|
} else if (strstr(mime_type, "text/plain")) {
|
|
return ".txt";
|
|
} else if (strstr(mime_type, "application/pdf")) {
|
|
return ".pdf";
|
|
} else {
|
|
return ".bin";
|
|
}
|
|
}
|
|
|
|
// Build canonical blob URL from origin + sha256 + extension
|
|
void nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size) {
|
|
if (!origin || !sha256 || !out || out_size == 0) {
|
|
return;
|
|
}
|
|
|
|
const char* extension = mime_to_extension(mime_type);
|
|
snprintf(out, out_size, "%s/%s%s", origin, sha256, extension);
|
|
}
|
|
|
|
// Parse PNG dimensions from IHDR chunk
|
|
int parse_png_dimensions(const unsigned char* data, size_t size, int* width, int* height) {
|
|
if (!data || size < 24 || !width || !height) {
|
|
return 0;
|
|
}
|
|
|
|
// Verify PNG signature
|
|
if (memcmp(data, "\x89PNG\r\n\x1a\n", 8) != 0) {
|
|
return 0;
|
|
}
|
|
|
|
// IHDR chunk should start at offset 8
|
|
// Skip chunk length (4 bytes) and chunk type "IHDR" (4 bytes)
|
|
// Width is at offset 16 (4 bytes, big-endian)
|
|
// Height is at offset 20 (4 bytes, big-endian)
|
|
if (size >= 24) {
|
|
*width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19];
|
|
*height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23];
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Parse JPEG dimensions from SOF0 or SOF2 markers
|
|
int parse_jpeg_dimensions(const unsigned char* data, size_t size, int* width, int* height) {
|
|
if (!data || size < 10 || !width || !height) {
|
|
return 0;
|
|
}
|
|
|
|
// Verify JPEG signature
|
|
if (size < 3 || memcmp(data, "\xff\xd8\xff", 3) != 0) {
|
|
return 0;
|
|
}
|
|
|
|
size_t pos = 2;
|
|
while (pos < size - 1) {
|
|
// Look for marker
|
|
if (data[pos] != 0xff) {
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
unsigned char marker = data[pos + 1];
|
|
pos += 2;
|
|
|
|
// SOF0 (0xc0) or SOF2 (0xc2)
|
|
if (marker == 0xc0 || marker == 0xc2) {
|
|
// Skip length (2 bytes) and precision (1 byte)
|
|
if (pos + 5 < size) {
|
|
pos += 3;
|
|
// Height (2 bytes, big-endian)
|
|
*height = (data[pos] << 8) | data[pos + 1];
|
|
pos += 2;
|
|
// Width (2 bytes, big-endian)
|
|
*width = (data[pos] << 8) | data[pos + 1];
|
|
return 1;
|
|
}
|
|
return 0;
|
|
} else if ((marker >= 0xe0 && marker <= 0xef) ||
|
|
(marker >= 0xc4 && marker <= 0xdf && marker != 0xc8)) {
|
|
// Skip over other segments
|
|
if (pos + 1 < size) {
|
|
size_t seg_len = (data[pos] << 8) | data[pos + 1];
|
|
pos += seg_len;
|
|
} else {
|
|
break;
|
|
}
|
|
} else {
|
|
pos++;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Parse WebP dimensions from VP8/VP8L/VP8X chunks
|
|
int parse_webp_dimensions(const unsigned char* data, size_t size, int* width, int* height) {
|
|
if (!data || size < 20 || !width || !height) {
|
|
return 0;
|
|
}
|
|
|
|
// Verify RIFF/WEBP header
|
|
if (memcmp(data, "RIFF", 4) != 0 || memcmp(data + 8, "WEBP", 4) != 0) {
|
|
return 0;
|
|
}
|
|
|
|
// Check chunk type at offset 12
|
|
if (memcmp(data + 12, "VP8 ", 4) == 0) {
|
|
// VP8 lossy format
|
|
if (size >= 30) {
|
|
// Skip to frame header (offset 26)
|
|
*width = ((data[28] | (data[29] << 8)) & 0x3fff);
|
|
*height = ((data[26] | (data[27] << 8)) & 0x3fff);
|
|
return 1;
|
|
}
|
|
} else if (memcmp(data + 12, "VP8L", 4) == 0) {
|
|
// VP8L lossless format
|
|
if (size >= 25) {
|
|
// Width and height are in bits 0-13 and 14-27 of a 32-bit value at offset 21
|
|
uint32_t dim_data = data[21] | (data[22] << 8) | (data[23] << 16) | (data[24] << 24);
|
|
*width = (dim_data & 0x3fff) + 1;
|
|
*height = ((dim_data >> 14) & 0x3fff) + 1;
|
|
return 1;
|
|
}
|
|
} else if (memcmp(data + 12, "VP8X", 4) == 0) {
|
|
// VP8X extended format
|
|
if (size >= 30) {
|
|
// Width (3 bytes, little-endian) at offset 24
|
|
// Height (3 bytes, little-endian) at offset 27
|
|
*width = (data[24] | (data[25] << 8) | (data[26] << 16)) + 1;
|
|
*height = (data[27] | (data[28] << 8) | (data[29] << 16)) + 1;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Get file dimensions based on MIME type
|
|
int nip94_get_dimensions(const unsigned char* data, size_t size, const char* mime_type, int* width, int* height) {
|
|
if (!data || !mime_type || !width || !height) {
|
|
return 0;
|
|
}
|
|
|
|
if (strstr(mime_type, "image/png")) {
|
|
return parse_png_dimensions(data, size, width, height);
|
|
} else if (strstr(mime_type, "image/jpeg")) {
|
|
return parse_jpeg_dimensions(data, size, width, height);
|
|
} else if (strstr(mime_type, "image/webp")) {
|
|
return parse_webp_dimensions(data, size, width, height);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Emit NIP-94 metadata field to stdout
|
|
void nip94_emit_field(const char* url, const char* mime, const char* sha256, long size, int width, int height) {
|
|
if (!url || !mime || !sha256) {
|
|
return;
|
|
}
|
|
|
|
printf(" \"nip94\": [\n");
|
|
printf(" [\"url\", \"%s\"],\n", url);
|
|
printf(" [\"m\", \"%s\"],\n", mime);
|
|
printf(" [\"x\", \"%s\"],\n", sha256);
|
|
printf(" [\"size\", \"%ld\"]", size);
|
|
|
|
// Add dim tag if dimensions are available
|
|
if (width > 0 && height > 0) {
|
|
printf(",\n [\"dim\", \"%dx%d\"]", width, height);
|
|
}
|
|
|
|
printf("\n ]");
|
|
} |