LanceErrorMessages.java

// 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.doris.datasource.lance;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/** Sanitizes provider errors with the credentials captured by the failing operation. */
final class LanceErrorMessages {
    private LanceErrorMessages() {
    }

    private static final int MAX_PROVIDER_MESSAGE_BYTES = 1024;
    private static final String[] RUNTIME_SENSITIVE_OPTION_KEYS = {
            "aws_access_key_id", "aws_secret_access_key", "aws_session_token",
            // OSS credentials only reach these options because of this change, so they have to be
            // recognized here too. The emitted spelling is the only one that occurs: the map read
            // below is the merged one, where a vended alias has already been normalized onto it.
            "oss_access_key_id", "oss_secret_access_key", "oss_security_token"
    };

    static RuntimeException failure(String prefix, Throwable error, String uri,
            Map<String, String> options, List<String> catalogSecrets) {
        String message = sanitize(error, uri, options, catalogSecrets);
        RuntimeException cause = error instanceof IllegalArgumentException
                ? new IllegalArgumentException(message) : new RuntimeException(message);
        return new RuntimeException(prefix + ": " + message, cause);
    }

    static String sanitize(Throwable throwable, String datasetUri,
            Map<String, String> runtimeStorageOptions, List<String> catalogSecrets) {
        String message = ExceptionUtils.getRootCauseMessage(throwable);
        Map<String, String> nonNullStorageOptions = runtimeStorageOptions == null
                ? Collections.emptyMap() : runtimeStorageOptions;
        List<String> sensitiveValues = new ArrayList<>(catalogSecrets);
        for (String sensitiveKey : RUNTIME_SENSITIVE_OPTION_KEYS) {
            sensitiveValues.add(nonNullStorageOptions.getOrDefault(sensitiveKey, ""));
        }
        sensitiveValues.add(datasetUri);
        sensitiveValues.removeIf(StringUtils::isEmpty);
        sensitiveValues.sort((left, right) -> Integer.compare(right.length(), left.length()));
        for (String sensitiveValue : sensitiveValues) {
            message = message.replace(sensitiveValue, "***");
        }
        return truncateUtf8(removeControlCharacters(message), MAX_PROVIDER_MESSAGE_BYTES);
    }

    private static String removeControlCharacters(String value) {
        StringBuilder sanitized = new StringBuilder(value.length());
        value.codePoints().filter(codePoint -> !Character.isISOControl(codePoint))
                .forEach(sanitized::appendCodePoint);
        return sanitized.toString();
    }

    private static String truncateUtf8(String value, int maxBytes) {
        if (value.getBytes(StandardCharsets.UTF_8).length <= maxBytes) {
            return value;
        }
        int end = 0;
        int bytes = 0;
        while (end < value.length()) {
            int codePoint = value.codePointAt(end);
            int codePointBytes = new String(Character.toChars(codePoint))
                    .getBytes(StandardCharsets.UTF_8).length;
            if (bytes + codePointBytes > maxBytes) {
                break;
            }
            bytes += codePointBytes;
            end += Character.charCount(codePoint);
        }
        return value.substring(0, end);
    }

}