Skip to content

Commit e20b68f

Browse files
committed
Drop YAML, JSON-only: canonical policy format, remove snakeyaml deps
1 parent 099a78a commit e20b68f

2 files changed

Lines changed: 167 additions & 0 deletions

File tree

‎src/main/java/io/cyphera/Cyphera.java‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,45 @@ public static Cyphera fromMap(Map<String, Object> config, KeyProvider keyProvide
6161
return new Cyphera(policies, keyProvider);
6262
}
6363

64+
/**
65+
* Load from a JSON file path.
66+
*/
67+
public static Cyphera fromFile(String path) {
68+
try {
69+
String contents = new String(java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(path)));
70+
Map<String, Object> config = JsonParser.parse(contents);
71+
return fromMap(config);
72+
} catch (java.io.IOException e) {
73+
throw new RuntimeException("Failed to load policy file: " + path, e);
74+
}
75+
}
76+
77+
/**
78+
* Auto-discover policy file using standard precedence:
79+
* 1. CYPHERA_POLICY_FILE env var
80+
* 2. ./cyphera.json
81+
* 3. /etc/cyphera/cyphera.json
82+
*/
83+
public static Cyphera load() {
84+
String envPath = System.getenv("CYPHERA_POLICY_FILE");
85+
if (envPath != null && java.nio.file.Files.exists(java.nio.file.Paths.get(envPath))) {
86+
return fromFile(envPath);
87+
}
88+
89+
String localPath = "cyphera.json";
90+
if (java.nio.file.Files.exists(java.nio.file.Paths.get(localPath))) {
91+
return fromFile(localPath);
92+
}
93+
94+
String systemPath = "/etc/cyphera/cyphera.json";
95+
if (java.nio.file.Files.exists(java.nio.file.Paths.get(systemPath))) {
96+
return fromFile(systemPath);
97+
}
98+
99+
throw new IllegalStateException(
100+
"No policy file found. Checked: CYPHERA_POLICY_FILE env, ./cyphera.json, /etc/cyphera/cyphera.json");
101+
}
102+
64103
/**
65104
* Protect a value using a named policy.
66105
* Returns tagged ciphertext (unless tag is disabled in policy).
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package io.cyphera;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
/**
7+
* Minimal JSON parser for Cyphera policy files.
8+
* Supports objects, strings, numbers, booleans, null. No arrays.
9+
* Zero dependencies — pure Java.
10+
*/
11+
final class JsonParser {
12+
private final String json;
13+
private int pos;
14+
15+
private JsonParser(String json) {
16+
this.json = json;
17+
this.pos = 0;
18+
}
19+
20+
static Map<String, Object> parse(String json) {
21+
JsonParser p = new JsonParser(json.trim());
22+
Object result = p.readValue();
23+
if (!(result instanceof Map)) {
24+
throw new IllegalArgumentException("Expected JSON object at root");
25+
}
26+
@SuppressWarnings("unchecked")
27+
Map<String, Object> map = (Map<String, Object>) result;
28+
return map;
29+
}
30+
31+
private void skipWhitespace() {
32+
while (pos < json.length() && Character.isWhitespace(json.charAt(pos))) pos++;
33+
}
34+
35+
private char peek() {
36+
skipWhitespace();
37+
if (pos >= json.length()) throw new IllegalArgumentException("Unexpected end of JSON");
38+
return json.charAt(pos);
39+
}
40+
41+
private char next() {
42+
skipWhitespace();
43+
if (pos >= json.length()) throw new IllegalArgumentException("Unexpected end of JSON");
44+
return json.charAt(pos++);
45+
}
46+
47+
private void expect(char c) {
48+
char actual = next();
49+
if (actual != c) throw new IllegalArgumentException("Expected '" + c + "' but got '" + actual + "' at pos " + (pos - 1));
50+
}
51+
52+
private Object readValue() {
53+
char c = peek();
54+
switch (c) {
55+
case '{': return readObject();
56+
case '"': return readString();
57+
case 't': case 'f': return readBoolean();
58+
case 'n': return readNull();
59+
default:
60+
if (c == '-' || Character.isDigit(c)) return readNumber();
61+
throw new IllegalArgumentException("Unexpected character '" + c + "' at pos " + pos);
62+
}
63+
}
64+
65+
private Map<String, Object> readObject() {
66+
expect('{');
67+
Map<String, Object> map = new HashMap<>();
68+
if (peek() == '}') { next(); return map; }
69+
while (true) {
70+
String key = readString();
71+
expect(':');
72+
Object value = readValue();
73+
map.put(key, value);
74+
char c = next();
75+
if (c == '}') break;
76+
if (c != ',') throw new IllegalArgumentException("Expected ',' or '}' but got '" + c + "'");
77+
}
78+
return map;
79+
}
80+
81+
private String readString() {
82+
expect('"');
83+
StringBuilder sb = new StringBuilder();
84+
while (pos < json.length()) {
85+
char c = json.charAt(pos++);
86+
if (c == '"') return sb.toString();
87+
if (c == '\\') {
88+
if (pos >= json.length()) throw new IllegalArgumentException("Unexpected end in string escape");
89+
char esc = json.charAt(pos++);
90+
switch (esc) {
91+
case '"': case '\\': case '/': sb.append(esc); break;
92+
case 'n': sb.append('\n'); break;
93+
case 't': sb.append('\t'); break;
94+
case 'r': sb.append('\r'); break;
95+
case 'u':
96+
String hex = json.substring(pos, pos + 4);
97+
sb.append((char) Integer.parseInt(hex, 16));
98+
pos += 4;
99+
break;
100+
default: sb.append(esc);
101+
}
102+
} else {
103+
sb.append(c);
104+
}
105+
}
106+
throw new IllegalArgumentException("Unterminated string");
107+
}
108+
109+
private Boolean readBoolean() {
110+
if (json.startsWith("true", pos)) { pos += 4; return Boolean.TRUE; }
111+
if (json.startsWith("false", pos)) { pos += 5; return Boolean.FALSE; }
112+
throw new IllegalArgumentException("Expected boolean at pos " + pos);
113+
}
114+
115+
private Object readNull() {
116+
if (json.startsWith("null", pos)) { pos += 4; return null; }
117+
throw new IllegalArgumentException("Expected null at pos " + pos);
118+
}
119+
120+
private Number readNumber() {
121+
int start = pos;
122+
if (json.charAt(pos) == '-') pos++;
123+
while (pos < json.length() && (Character.isDigit(json.charAt(pos)) || json.charAt(pos) == '.')) pos++;
124+
String num = json.substring(start, pos);
125+
if (num.contains(".")) return Double.parseDouble(num);
126+
return Integer.parseInt(num);
127+
}
128+
}

0 commit comments

Comments
 (0)