gnodet-bot commented on code in PR #27850: URL: https://github.com/apache/camel/pull/27850#discussion_r4276074273
########## dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticMediaInput.java: ########## @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.camel.dsl.jbang.core.common; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; + +/** Media input shared by the CLI and TUI; independent of expert implementations. */ +public final class SemanticMediaInput { + private static final String PARTS = "$semanticInput"; + + private SemanticMediaInput() { + } + + public static boolean isMedia(Object input) { + return input instanceof Map<?, ?> map && map.get(PARTS) instanceof List<?>; + } + + public static List<?> parts(Object input) { + return isMedia(input) ? (List<?>) ((Map<?, ?>) input).get(PARTS) : List.of(); + } + + /** Read only an explicitly requested local image. This must run off the TUI render thread. */ + public static Object readImage(String value) { + if (!value.startsWith("image:")) { + return value; + } + Path path = Path.of(value.substring(6)); + String name = path.getFileName().toString().toLowerCase(Locale.ROOT); + String type = name.endsWith(".png") ? "image/png" + : name.endsWith(".jpg") || name.endsWith(".jpeg") ? "image/jpeg" + : name.endsWith(".gif") ? "image/gif" : name.endsWith(".webp") ? "image/webp" : null; + if (type == null) { + throw new IllegalArgumentException("image: requires a local PNG, JPEG, GIF or WebP file"); + } + try { + String data = "data:" + type + ";base64," + Base64.getEncoder().encodeToString(Files.readAllBytes(path)); Review Comment: ⚠️ **`Files.readAllBytes()` with no size guard** — a multi-gigabyte image file will be read fully into a `byte[]` and then base64-encoded (~1.33× amplification) before the downstream 1 GiB gate has a chance to fire. The JBang CLI process can OOM here. Consider checking the file size first: ```suggestion long fileSize = Files.size(path); if (fileSize > 100 * 1024 * 1024L) { throw new IllegalArgumentException("Image file is too large (" + fileSize + " bytes); maximum is 100 MiB"); } String data = "data:" + type + ";base64," + Base64.getEncoder().encodeToString(Files.readAllBytes(path)); ``` ########## components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticInput.java: ########## @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.semantic; + +import java.io.InputStream; +import java.util.Base64; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.camel.Exchange; +import org.apache.camel.NoTypeConversionAvailableException; +import org.apache.camel.StreamCache; +import org.apache.camel.semantic.SemanticExpert.InputType; + +/** + * Explicit, JSON-compatible input envelope for ordered text and image parts. The reserved {@value #PARTS} key + * distinguishes media from ordinary application state. Values are data: no file or URL is resolved by this class. + */ +public final class SemanticInput { + public static final String PARTS = "$semanticInput"; + + private SemanticInput() { + } + + public static Map<String, Object> ofParts(List<Map<String, Object>> parts) { + Map<String, Object> input = Map.of(PARTS, parts.stream().map(Map::copyOf).toList()); + validate(input, Set.of(InputType.TEXT, InputType.IMAGE)); + return input; + } + + public static Map<String, Object> text(String text) { + return Map.of("type", "text", "text", text); + } + + /** An image data URL, with an optional expert-specific detail setting supplied separately in the part. */ + public static Map<String, Object> image(String data) { + return Map.of("type", "image", "data", data); + } + + /** Binary image part with an explicit MIME type. No image interpretation or inference is performed. */ + public static Map<String, Object> image(byte[] data, String mediaType) { + return image("data:" + mediaType + ";base64," + Base64.getEncoder().encodeToString(data)); + } + + /** Normalize a self-describing image string; ordinary strings remain text. */ + public static Object normalize(Object input) { + return input instanceof String text && text.startsWith("data:image/") + ? ofParts(List.of(image(text))) : input; + } + + /** + * Select an image body using Content-Type. Streams become replayable Camel stream caches before reading, so later + * route steps can still consume the body. Headers do not affect explicitly selected headers or variables. + */ + public static Object body(Exchange exchange) throws NoTypeConversionAvailableException { + Object input = exchange.getMessage().getBody(); + String contentType = exchange.getMessage().getHeader(Exchange.CONTENT_TYPE, String.class); + String mediaType = contentType == null ? "" : contentType.split(";", 2)[0].trim().toLowerCase(Locale.ROOT); + if (mediaType.startsWith("audio/") || mediaType.startsWith("video/")) { + throw new IllegalArgumentException("Audio and video bodies are not supported by the selected input contract"); + } + if (!mediaType.startsWith("image/")) { + return normalize(input); + } + if (!mediaType.matches("image/[a-z0-9.+-]{1,100}")) { + throw new IllegalArgumentException("Invalid image Content-Type"); + } + if (input instanceof InputStream && !(input instanceof StreamCache)) { + input = exchange.getContext().getTypeConverter().mandatoryConvertTo(StreamCache.class, exchange, input); + exchange.getMessage().setBody(input); + } + if (input instanceof StreamCache cache) { + try { + cache.reset(); + byte[] data = exchange.getContext().getTypeConverter().mandatoryConvertTo(byte[].class, exchange, cache); + return ofParts(List.of(image(data, mediaType))); + } finally { + cache.reset(); + } + } + if (input instanceof byte[] data) { + return ofParts(List.of(image(data, mediaType))); + } + if (input instanceof String text) { + return text.startsWith("data:image/") + ? normalize(text) + : ofParts(List.of(image("data:" + mediaType + ";base64," + text))); Review Comment: ⚠️ **Unvalidated base64 string accepted as image body** — when `body()` receives a `String` with an image `Content-Type` and the text doesn't start with `data:image/`, it is concatenated verbatim into a data URL (`"data:" + mediaType + ";base64," + text`). No check is performed that `text` is actually valid base64, nor is there a length guard. A caller passing a multi-megabyte plaintext body with `Content-Type: image/png` will produce a malformed data URL that silently propagates downstream. Consider validating that `text` is non-blank and has a reasonable length before embedding it, or document clearly that callers are responsible for passing a valid base64 payload. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
