Files
stephen schuresko bd40b4f850 Add darktable REST API implementation
- Introduced a new Lua-based REST API for darktable, allowing external applications to interact with the darktable library.
- Implemented core functionalities including image listing, retrieval, and preview serving.
- Added support for OAuth 2.0 authentication with token generation.
- Created a simple HTTP router to handle API requests.
- Included JSON encoding/decoding utilities for API responses.
- Developed a script to launch the API server alongside darktable, ensuring seamless integration.
- Added necessary configuration and setup for the API server, including CORS support and error handling.
2026-06-10 15:02:19 -04:00

156 lines
5.0 KiB
Lua

-- Minimal JSON encoder/decoder (pure Lua, no dependencies)
local M = {}
-- ── Encoder ──────────────────────────────────────────────────────────────────
-- Iterate byte-by-byte to avoid null-byte issues in Lua pattern ranges.
local function encode_str(s)
local buf = {'"'}
for i = 1, #s do
local b = s:byte(i)
if b == 34 then buf[#buf+1] = '\\"'
elseif b == 92 then buf[#buf+1] = '\\\\'
elseif b == 10 then buf[#buf+1] = '\\n'
elseif b == 13 then buf[#buf+1] = '\\r'
elseif b == 9 then buf[#buf+1] = '\\t'
elseif b < 32 or b == 127 then
buf[#buf+1] = string.format('\\u%04x', b)
else
buf[#buf+1] = s:sub(i, i)
end
end
buf[#buf+1] = '"'
return table.concat(buf)
end
local function enc(v, depth)
if depth > 64 then error("json.encode: table too deeply nested") end
local t = type(v)
if t == "nil" then return "null"
elseif t == "boolean" then return tostring(v)
elseif t == "number" then
if v ~= v or math.abs(v) == math.huge then return "null" end
return v == math.floor(v) and string.format("%d", v) or string.format("%.10g", v)
elseif t == "string" then
return encode_str(v)
elseif t == "table" then
-- Treat as array when it has a consecutive integer key sequence from 1
if #v > 0 then
local parts = {}
for i = 1, #v do parts[i] = enc(v[i], depth + 1) end
return "[" .. table.concat(parts, ",") .. "]"
end
local parts = {}
for k, x in pairs(v) do
if type(k) == "string" then
parts[#parts+1] = encode_str(k) .. ":" .. enc(x, depth + 1)
end
end
return "{" .. table.concat(parts, ",") .. "}"
end
return "null"
end
function M.encode(v) return enc(v, 0) end
-- ── Decoder ───────────────────────────────────────────────────────────────────
function M.decode(src)
local pos = 1
local function skip()
while pos <= #src and src:byte(pos) <= 32 do pos = pos + 1 end
end
local parse -- forward declaration
local function parse_string()
pos = pos + 1 -- consume opening "
local buf = {}
while pos <= #src do
local c = src:sub(pos, pos)
if c == '"' then pos = pos + 1; return table.concat(buf) end
if c == '\\' then
pos = pos + 1
c = src:sub(pos, pos)
if c == '"' then buf[#buf+1] = '"'
elseif c == '\\' then buf[#buf+1] = '\\'
elseif c == '/' then buf[#buf+1] = '/'
elseif c == 'n' then buf[#buf+1] = '\n'
elseif c == 'r' then buf[#buf+1] = '\r'
elseif c == 't' then buf[#buf+1] = '\t'
elseif c == 'b' then buf[#buf+1] = '\b'
elseif c == 'f' then buf[#buf+1] = '\f'
elseif c == 'u' then
local hex = tonumber(src:sub(pos+1, pos+4), 16) or 0
-- Only emit ASCII; multi-byte unicode emits a placeholder
buf[#buf+1] = hex < 128 and string.char(hex) or '\xEF\xBF\xBD'
pos = pos + 4
end
else
buf[#buf+1] = c
end
pos = pos + 1
end
error("json.decode: unterminated string near pos " .. pos)
end
local function parse_object()
pos = pos + 1 -- consume '{'
local obj = {}
skip()
if src:sub(pos, pos) == '}' then pos = pos + 1; return obj end
while true do
skip()
local key = parse_string()
skip()
if src:sub(pos, pos) ~= ':' then error("json.decode: expected ':' near pos " .. pos) end
pos = pos + 1
skip()
obj[key] = parse()
skip()
local ch = src:sub(pos, pos); pos = pos + 1
if ch == '}' then return obj end
if ch ~= ',' then error("json.decode: expected ',' or '}' near pos " .. pos) end
end
end
local function parse_array()
pos = pos + 1 -- consume '['
local arr = {}
skip()
if src:sub(pos, pos) == ']' then pos = pos + 1; return arr end
while true do
skip()
arr[#arr+1] = parse()
skip()
local ch = src:sub(pos, pos); pos = pos + 1
if ch == ']' then return arr end
if ch ~= ',' then error("json.decode: expected ',' or ']' near pos " .. pos) end
end
end
parse = function()
skip()
local c = src:sub(pos, pos)
if c == '"' then return parse_string()
elseif c == '{' then return parse_object()
elseif c == '[' then return parse_array()
elseif c == 't' then pos = pos + 4; return true
elseif c == 'f' then pos = pos + 5; return false
elseif c == 'n' then pos = pos + 4; return nil
else
-- Parentheses create the capture; without them find returns only start/end.
local _, e, n = src:find("^(-?%d+%.?%d*[eE]?[+-]?%d*)", pos)
if n then pos = e + 1; return tonumber(n) end
error("json.decode: unexpected token '" .. c .. "' at pos " .. pos)
end
end
local ok, result = pcall(parse)
if not ok then return nil, result end
return result
end
return M