sunchao commented on code in PR #6023:
URL: https://github.com/apache/datafusion-comet/pull/6023#discussion_r4066755718


##########
spark/src/main/java/org/apache/comet/cloud/s3/CometS3CredentialDispatcher.java:
##########
@@ -74,19 +79,43 @@ public static long ensureInitialized(
         catalogProperties == null
             ? Collections.emptyMap()
             : Collections.unmodifiableMap(new HashMap<>(catalogProperties));
+    // Key on a digest of the properties, not the values themselves: the 
KEY_TO_HANDLE map is
+    // static and lives for the JVM lifetime, and the property bag may carry 
secrets (vended
+    // credentials, static keys). The full map is still handed to initialize() 
below; only the
+    // long-lived cache key is reduced to a digest. A distinct config still 
yields a distinct key.
     InstanceKey key =
-        new InstanceKey(providerClassName, dispatchKey == null ? "" : 
dispatchKey, snapshot);
+        new InstanceKey(
+            providerClassName, dispatchKey == null ? "" : dispatchKey, 
digestOf(snapshot));
     return KEY_TO_HANDLE.computeIfAbsent(
         key,
         k -> {
-          CometS3CredentialProvider provider = 
instantiate(k.providerClassName);
-          provider.initialize(k.catalogProperties);
+          CometS3CredentialProvider provider = instantiate(providerClassName);
+          provider.initialize(snapshot);
           long handle = HANDLE_SEQ.getAndIncrement();
           INSTANCES.put(handle, new RegisteredProvider(provider, k));
           return handle;
         });
   }
 
+  /** Stable SHA-256 digest of the property bag, so secret values are not 
retained in the key. */
+  private static String digestOf(Map<String, String> props) {
+    MessageDigest md;
+    try {
+      md = MessageDigest.getInstance("SHA-256");
+    } catch (NoSuchAlgorithmException e) {
+      throw new IllegalStateException("SHA-256 not available", e);
+    }
+    // Sort by key for order-independence; NUL separators avoid key/value 
boundary ambiguity.
+    for (Map.Entry<String, String> e : new TreeMap<>(props).entrySet()) {
+      md.update(e.getKey().getBytes(StandardCharsets.UTF_8));
+      md.update((byte) 0);
+      String v = e.getValue();
+      md.update(v == null ? new byte[] {1} : 
v.getBytes(StandardCharsets.UTF_8));
+      md.update((byte) 0);

Review Comment:
   ### Correctness
   
   [P2] Preserve property boundaries before hashing the cache key
   
   Could the digest input use length-prefixed fields (and an explicit null 
tag), with a regression test for embedded U+0000? If an arbitrary vendor 
property value contains NUL, the current separators are ambiguous: 
`{"fs.s3a.a":"x\u0000fs.s3a.identity\u0000TENANT-B"}` and 
`{"fs.s3a.a":"x","fs.s3a.identity":"TENANT-B"}` produce identical input bytes. 
With the same provider class and dispatch key, I reproduced both maps receiving 
the same handle and the second request returning the first map's synthetic 
identity. Reversing their order reverses which identity wins. The previous 
map-based key keeps them separate. The unfiltered Iceberg FileIO bag reaches 
this method through protobuf strings and JNI's modified UTF-8 conversion 
without rejecting U+0000. This is conditional on such vendor values, and comes 
from the serialization before SHA-256. Ordinary key changes still separate 
correctly.



##########
spark/src/main/java/org/apache/comet/cloud/s3/AdapterSupport.java:
##########
@@ -0,0 +1,151 @@
+/*
+ * 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.comet.cloud.s3;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URI;
+import java.util.Map;
+
+import org.apache.hadoop.conf.Configuration;
+
+/** Config and reflection helpers shared by the built-in S3 credential 
provider adapters. */
+final class AdapterSupport {
+
+  private AdapterSupport() {}
+
+  /**
+   * Rebuilds a Hadoop {@link Configuration} from the forwarded {@code 
fs.s3a.*} map. The adapters
+   * run on the executor without a live {@code S3AFileSystem}, so keys are 
copied onto a fresh
+   * Configuration (which still loads core-site defaults).
+   */
+  static Configuration toConfiguration(Map<String, String> props) {
+    Configuration conf = new Configuration();
+    for (Map.Entry<String, String> entry : props.entrySet()) {
+      if (entry.getValue() != null) {
+        conf.set(entry.getKey(), entry.getValue());
+      }
+    }
+    return conf;
+  }
+
+  /**
+   * Per-bucket then global lookup, mirroring Comet's native {@code fs.s3a} 
resolution: {@code
+   * fs.s3a.bucket.<bucket>.<property>} wins over {@code fs.s3a.<property>}. 
Returns null if neither
+   * is set (after trimming).
+   */
+  static String lookup(Map<String, String> props, String bucket, String 
property) {
+    String perBucket = props.get("fs.s3a.bucket." + bucket + "." + property);
+    if (perBucket != null && !perBucket.trim().isEmpty()) {
+      return perBucket.trim();
+    }
+    String global = props.get("fs.s3a." + property);
+    if (global != null && !global.trim().isEmpty()) {
+      return global.trim();
+    }
+    return null;
+  }
+
+  /** Returns the public static no-arg method {@code name} on {@code clazz}, 
or null if absent. */
+  private static Method staticMethod(Class<?> clazz, String name) {
+    try {
+      Method m = clazz.getMethod(name);
+      return Modifier.isStatic(m.getModifiers()) ? m : null;
+    } catch (NoSuchMethodException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Instantiates a credential-provider delegate, trying the same ordered 
conventions for both the
+   * v1 and v2 adapters so their {@code @Public} contract is identical: the 
Hadoop-style {@code
+   * (URI, Configuration)} and {@code (Configuration)} constructors first 
(matching {@code
+   * S3AUtils.getInstanceFromReflection}), then the SDK static factories 
{@code create()} / {@code
+   * builder().build()} / {@code getInstance()}, then a public no-arg 
constructor.
+   *
+   * <p>Factory return types must be assignable to {@code targetType} (as 
Hadoop's {@code
+   * getFactoryMethod} requires), so an unrelated {@code static String 
create()} is skipped rather
+   * than invoked and failing later with a {@code ClassCastException}. Returns 
an untyped instance;
+   * the caller casts to its SDK provider interface.
+   */
+  static Object instantiateDelegate(
+      Class<?> targetType, Class<?> clazz, URI uri, Configuration conf) throws 
Exception {
+    Constructor<?> uriConf = constructor(clazz, URI.class, 
Configuration.class);
+    if (uriConf != null) {
+      return uriConf.newInstance(uri, conf);
+    }
+    Constructor<?> confOnly = constructor(clazz, Configuration.class);
+    if (confOnly != null) {
+      return confOnly.newInstance(conf);
+    }
+    Method create = factoryMethod(clazz, "create", targetType);
+    if (create != null) {
+      return create.invoke(null);
+    }
+    Method builder = staticMethod(clazz, "builder");
+    if (builder != null) {
+      // Resolve build() off builder()'s declared (public) return type, not 
the runtime object's
+      // class, which may be a non-public implementation.
+      Method build = builder.getReturnType().getMethod("build");
+      if (targetType.isAssignableFrom(build.getReturnType())) {

Review Comment:
   ### Correctness
   
   [P2] Accept valid builders whose build return type is erased
   
   Could this account for an inherited generic `build()` return type before 
rejecting the builder? A public `Builder extends SdkBuilder<Builder, 
MyProvider>` can inherit `T build()` without redeclaring it. Reflection then 
reports `Object`, although `builder().build()` returns a valid provider. A 
synthetic builder-only v2 provider using that public AWS interface succeeds 
through the adapter at `333d8fd1`, but at this head the new check skips it and 
falls through to a missing no-arg constructor (`NoSuchMethodException`). The 
existing builder fixture redeclares a concrete return type, so it does not 
cover this case. This affects that generic builder shape, not providers with a 
concrete covariant `build()` declaration or an earlier valid factory.



-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to