package cloud.lunarsky.store; import com.sun.net.httpserver.Headers; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.TreeMap; final class ObjectAttributes { private ObjectAttributes() {} static Map userMetadata(Headers headers) { Map values = new TreeMap<>(); int total = 0; for (String name : headers.keySet()) { if (!name.toLowerCase(java.util.Locale.ROOT).startsWith("x-amz-meta-")) continue; String key = name.substring(11).toLowerCase(java.util.Locale.ROOT); if (!key.matches("[a-z0-9][a-z0-9._-]{0,127}")) throw new StoreException(400, "InvalidArgument", "Invalid user metadata key"); String value = SigV4.single(headers, name); if (value == null || !value.chars().allMatch(c -> c >= 32 && c <= 126)) throw new StoreException(400, "InvalidArgument", "Invalid user metadata value"); total += key.getBytes(StandardCharsets.UTF_8).length + value.getBytes(StandardCharsets.UTF_8).length; values.put(key, value); } if (total > 2048) throw new StoreException(400, "MetadataTooLarge", "User metadata exceeds 2 KiB"); return Map.copyOf(values); } static Map tagsHeader(String raw) { if (raw == null) return Map.of(); Map tags = new TreeMap<>(); if (raw.isEmpty()) return tags; for (String pair : raw.split("&", -1)) { String[] parts = pair.split("=", 2); if (parts.length != 2) throw new StoreException(400, "InvalidTag", "Invalid tagging header"); String key = SigV4.decode(parts[0]); String value = SigV4.decode(parts[1]); if (tags.put(key, value) != null) throw new StoreException(400, "InvalidTag", "Duplicate tag key"); } validateTags(tags); return Map.copyOf(tags); } static void validateTags(Map tags) { if (tags.size() > 10) throw new StoreException(400, "InvalidTag", "Too many object tags"); for (var entry : tags.entrySet()) { if (entry.getKey().isEmpty() || entry.getKey().length() > 128 || entry.getValue().length() > 256 || !xmlText(entry.getKey()) || !xmlText(entry.getValue())) throw new StoreException(400, "InvalidTag", "Invalid tag key or value"); } } private static boolean xmlText(String value) { for (int i = 0; i < value.length();) { int point = value.codePointAt(i); if (point < 32 || point > 0x10ffff || point >= 0xd800 && point <= 0xdfff || point >= 0xfffe && point <= 0xffff) return false; i += Character.charCount(point); } return true; } static byte[] encode(Map values, int limit) { try { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(bytes); output.writeShort(values.size()); for (var entry : new TreeMap<>(values).entrySet()) { byte[] key = entry.getKey().getBytes(StandardCharsets.UTF_8); byte[] value = entry.getValue().getBytes(StandardCharsets.UTF_8); output.writeShort(key.length); output.writeShort(value.length); output.write(key); output.write(value); } if (bytes.size() > limit) throw new StoreException(400, "InvalidArgument", "Object attributes are too large"); return bytes.toByteArray(); } catch (IOException error) { throw new IllegalStateException(error); } } static Map decode(byte[] bytes) throws IOException { if (bytes == null || bytes.length == 0) return Map.of(); try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes))) { int count = input.readUnsignedShort(); if (count > 128) throw new IOException("Invalid object attributes"); Map values = new TreeMap<>(); for (int i = 0; i < count; i++) { int keyLength = input.readUnsignedShort(), valueLength = input.readUnsignedShort(); String key = decodeUtf8(input.readNBytes(keyLength), keyLength); String value = decodeUtf8(input.readNBytes(valueLength), valueLength); if (values.put(key, value) != null) throw new IOException("Duplicate object attribute"); } if (input.available() != 0) throw new IOException("Trailing object attributes"); return Map.copyOf(values); } } private static String decodeUtf8(byte[] bytes, int length) throws IOException { if (bytes.length != length) throw new IOException("Truncated object attributes"); try { return StandardCharsets.UTF_8.newDecoder() .onMalformedInput(java.nio.charset.CodingErrorAction.REPORT) .decode(java.nio.ByteBuffer.wrap(bytes)).toString(); } catch (java.nio.charset.CharacterCodingException error) { throw new IOException("Invalid object attributes", error); } } }