zeroshade commented on code in PR #1259:
URL: https://github.com/apache/iceberg-go/pull/1259#discussion_r3493439697


##########
io/gocloud/blob.go:
##########
@@ -69,33 +68,138 @@ func (f *blobOpenFile) Sys() any                   { 
return f.b }
 func (f *blobOpenFile) IsDir() bool                { return false }
 func (f *blobOpenFile) Stat() (fs.FileInfo, error) { return f, nil }
 
-// KeyExtractor extracts the object key from an input path
+// KeyExtractor extracts the object key from an input path.
 type KeyExtractor func(path string) (string, error)
 
-// defaultKeyExtractor extracts the object key by removing the scheme and 
bucket name from the URI
-// e.g., s3://bucket/path/file -> path/file
-func defaultKeyExtractor(bucketName string) KeyExtractor {
+var errEmptyObjectKey = errors.New("object key is empty")
+
+type objectLocation struct {
+       authority    string
+       key          string
+       uriKey       string
+       uriPrefix    string
+       hasAuthority bool
+}
+
+func splitObjectLocation(location string) (objectLocation, error) {
+       scheme, rest, ok := strings.Cut(location, "://")
+       if !ok {
+               return objectLocation{key: location, uriKey: location}, nil
+       }
+
+       authorityEnd := strings.IndexAny(rest, "/?#")
+       if authorityEnd == -1 {
+               authorityEnd = len(rest)
+       }
+
+       authority := rest[:authorityEnd]
+       if authority == "" {
+               return objectLocation{}, fmt.Errorf("URI authority is empty: 
%s", location)
+       }
+
+       key := ""
+       if authorityEnd < len(rest) {
+               if rest[authorityEnd] != '/' {
+                       return objectLocation{}, fmt.Errorf("URI authority %q 
must be followed by an object path: %s",
+                               authority, location)
+               }
+
+               key = strings.TrimPrefix(rest[authorityEnd:], "/")
+       }
+
+       return objectLocation{
+               authority:    authority,
+               key:          key,
+               uriKey:       key,
+               uriPrefix:    scheme + "://" + authority + "/",
+               hasAuthority: true,
+       }, nil
+}
+
+type keyExtractorConfig struct {
+       strictAuthorityValidation bool
+}
+
+type keyExtractorOption func(*keyExtractorConfig)
+
+func withStrictAuthorityValidation() keyExtractorOption {
+       return func(cfg *keyExtractorConfig) {
+               cfg.strictAuthorityValidation = true
+       }
+}
+
+type objectLocationExtractor func(location string) (objectLocation, error)
+
+func keyExtractorFromObjectLocation(extract objectLocationExtractor) 
KeyExtractor {
        return func(location string) (string, error) {
-               _, after, found := strings.Cut(location, "://")
-               if found {
-                       location = after
+               parsed, err := extract(location)
+               if err != nil {
+                       return "", err
+               }
+
+               return parsed.key, nil
+       }
+}
+
+// Non-strict cross-authority paths still fold into a storage key so existing
+// manifests keep loading until the FileIO can open per-authority buckets.
+func legacyAuthorityKey(parsed objectLocation) string {
+       if parsed.key == "" {
+               return parsed.authority + "/"
+       }
+
+       return parsed.authority + "/" + parsed.key
+}
+
+func defaultObjectLocationExtractor(bucketName string, opts 
...keyExtractorOption) objectLocationExtractor {
+       var cfg keyExtractorConfig
+       for _, opt := range opts {
+               opt(&cfg)
+       }
+
+       return func(location string) (objectLocation, error) {
+               parsed, err := splitObjectLocation(location)
+               if err != nil {
+                       return objectLocation{}, err
                }
 
-               key := strings.TrimPrefix(location, bucketName+"/")
+               if parsed.hasAuthority {
+                       if parsed.authority != bucketName {
+                               if cfg.strictAuthorityValidation {
+                                       return objectLocation{}, 
fmt.Errorf("URI authority %q does not match configured authority %q",
+                                               parsed.authority, bucketName)
+                               }
+
+                               // Preserve the legacy fold-by-authority 
behavior on the default
+                               // path so existing cross-authority manifests 
keep loading until
+                               // the FileIO can open per-authority buckets.
+                               parsed.key = legacyAuthorityKey(parsed)
+                       }
+               } else {
+                       parsed.key = strings.TrimPrefix(location, 
bucketName+"/")
+                       parsed.uriKey = parsed.key
+               }
 
-               if key == "" {
-                       return "", fmt.Errorf("URI path is empty: %s", location)
+               if parsed.key == "" {
+                       return parsed, fmt.Errorf("%w: %s", errEmptyObjectKey, 
location)
                }
 
-               return key, nil
+               return parsed, nil
        }
 }
 
+// defaultKeyExtractor extracts the object key by removing the scheme and 
bucket name from the URI.
+// e.g., s3://bucket/path/file -> path/file.
+func defaultKeyExtractor(bucketName string, opts ...keyExtractorOption) 
KeyExtractor {
+       return 
keyExtractorFromObjectLocation(defaultObjectLocationExtractor(bucketName, 
opts...))
+}
+
 type blobFileIO struct {
        *blob.Bucket
 
-       keyExtractor KeyExtractor
-       ctx          context.Context
+       keyExtractor  KeyExtractor

Review Comment:
   `blobFileIO` stores both `keyExtractor` and `extractObject`, but 
`keyExtractor` is always derived from `extractObject` via 
`keyExtractorFromObjectLocation` in every path. Storing both invites drift — 
consider keeping only `extractObject` and deriving the extractor where needed 
(and dropping it from the `createBlobFS` signature).



##########
io/gocloud/register.go:
##########
@@ -30,6 +31,14 @@ func init() {
        registerAzureSchemes()
 }
 
+func keyExtractorOptions(props map[string]string) []keyExtractorOption {
+       if enabled, err := 
strconv.ParseBool(props[icebergio.ObjectStoreStrictAuthorityValidation]); err 
== nil && enabled {

Review Comment:
   Strict-mode opt-in silently swallows `ParseBool` errors — a typo like 
`"ture"`/`"yes"` disables validation invisibly. Since the prop only accepts the 
two bool spellings, consider returning the parse error to the factory instead 
of ignoring it.



##########
io/gocloud/blob.go:
##########
@@ -198,25 +302,64 @@ func (bfs *blobFileIO) NewWriter(ctx context.Context, 
path string, overwrite boo
                nil
 }
 
-func createBlobFS(ctx context.Context, bucket *blob.Bucket, keyExtractor 
KeyExtractor) icebergio.IO {
-       return &blobFileIO{Bucket: bucket, keyExtractor: keyExtractor, ctx: ctx}
+func createBlobFS(ctx context.Context, bucket *blob.Bucket, keyExtractor 
KeyExtractor, extractObject objectLocationExtractor) icebergio.IO {
+       return &blobFileIO{Bucket: bucket, keyExtractor: keyExtractor, 
extractObject: extractObject, ctx: ctx}
+}
+
+func (bfs *blobFileIO) objectLocation(root string) (objectLocation, error) {
+       if bfs.extractObject == nil {
+               return objectLocation{}, errors.New("blob file IO missing 
object location extractor")
+       }
+
+       return bfs.extractObject(root)
+}
+
+func walkedURIPath(location objectLocation, walked string) string {
+       if walked == "." {
+               return location.uriPrefix
+       }
+
+       uriKey := walked
+       if location.key != location.uriKey {
+               storageRoot := strings.TrimSuffix(location.key, "/")
+               if walked == location.key || walked == storageRoot {
+                       uriKey = location.uriKey
+               } else {
+                       prefix := storageRoot + "/"
+                       if suffix, ok := strings.CutPrefix(walked, prefix); ok {

Review Comment:
   If `walked` matches none of `location.key` / `storageRoot` / 
`storageRoot+"/"`, this `ok=false` path leaves `uriKey = walked`, producing 
`uriPrefix + walked` with the bucket doubled (e.g. 
`s3://other-bucket/other-bucket/foo`). Unreachable today since `WalkDir` only 
emits descendants, but a silent fallback — consider an explicit error or an 
invariant comment.



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