From 455bb66534702e94a77001e14e8312879db6c87e Mon Sep 17 00:00:00 2001
From: Alex Aizman <alex.aizman@gmail.com>
Date: Sat, 11 Jan 2025 11:59:09 -0500
Subject: [PATCH] core: remove "file name too long" limitation (ref)

* limit mountpath length
* refactor
* part six, prev. commit: 4d75c7d65bcb

Signed-off-by: Alex Aizman <alex.aizman@gmail.com>
---
 cmn/config.go     | 10 +++++++++-
 core/lom.go       | 14 +++++---------
 fs/content.go     | 24 +++---------------------
 fs/fntl.go        | 44 ++++++++++++++++++++++++++++++++++++++++++++
 xact/xs/wi_lso.go |  2 +-
 5 files changed, 62 insertions(+), 32 deletions(-)
 create mode 100644 fs/fntl.go

diff --git a/cmn/config.go b/cmn/config.go
index 23f80ea327f..3f1f1c94f17 100644
--- a/cmn/config.go
+++ b/cmn/config.go
@@ -1535,7 +1535,7 @@ func (c *FSPConf) MarshalJSON() ([]byte, error) {
 
 func (c *FSPConf) Validate(contextConfig *Config) error {
 	debug.Assertf(cos.StringInSlice(contextConfig.role, []string{apc.Proxy, apc.Target}),
-		"unexpected role: %q", contextConfig.role)
+		"unexpected node type: %q", contextConfig.role)
 
 	// Don't validate in testing environment.
 	if contextConfig.TestingEnv() || contextConfig.role != apc.Target {
@@ -1637,6 +1637,11 @@ func (c *TestFSPConf) ValidateMpath(p string) (err error) {
 }
 
 // common mountpath validation (NOTE: calls filepath.Clean() every time)
+
+const (
+	maxLenMountpath = 255
+)
+
 func ValidateMpath(mpath string) (string, error) {
 	cleanMpath := filepath.Clean(mpath)
 
@@ -1646,6 +1651,9 @@ func ValidateMpath(mpath string) (string, error) {
 	if cleanMpath == cos.PathSeparator {
 		return "", NewErrInvalidaMountpath(mpath, "root directory is not a valid mountpath")
 	}
+	if len(cleanMpath) > maxLenMountpath {
+		return "", NewErrInvalidaMountpath(mpath, "mountpath length cannot exceed "+strconv.Itoa(maxLenMountpath))
+	}
 	return cleanMpath, nil
 }
 
diff --git a/core/lom.go b/core/lom.go
index 23f2414fd42..1cf10da5836 100644
--- a/core/lom.go
+++ b/core/lom.go
@@ -675,15 +675,11 @@ func (lom *LOM) IsFntl() bool {
 	return lom.md.lid.haslmfl(lmflFntl)
 }
 
-func (lom *LOM) HasFntlPrefix() bool {
-	return strings.HasPrefix(lom.ObjName, fs.PrefixFntl)
-}
-
 // (compare with fs/content Gen())
 func (lom *LOM) ShortenFntl() []string {
-	debug.Assert(fs.FnameTooLong(lom.ObjName), lom.FQN)
+	debug.Assert(fs.IsFntl(lom.ObjName), lom.FQN)
 
-	noname := fs.PrefixFntl + cos.ChecksumB2S(cos.UnsafeB(lom.FQN), cos.ChecksumSHA256)
+	noname := fs.ShortenFntl(lom.FQN)
 	nfqn := lom.mi.MakePathFQN(lom.Bucket(), fs.ObjectType, noname)
 
 	debug.Assert(len(nfqn) < 4096, "PATH_MAX /usr/include/limits.h", len(nfqn))
@@ -691,11 +687,11 @@ func (lom *LOM) ShortenFntl() []string {
 }
 
 func (lom *LOM) fixupFntl() {
-	if !fs.FnameTooLong(lom.ObjName) {
+	if !fs.IsFntl(lom.ObjName) {
 		return
 	}
-	lom.ObjName = fs.PrefixFntl + cos.ChecksumB2S(cos.UnsafeB(lom.FQN), cos.ChecksumSHA256) // noname
-	lom.FQN = lom.mi.MakePathFQN(lom.Bucket(), fs.ObjectType, lom.ObjName)                  // nfqn
+	lom.ObjName = fs.ShortenFntl(lom.FQN)                                  // noname
+	lom.FQN = lom.mi.MakePathFQN(lom.Bucket(), fs.ObjectType, lom.ObjName) // nfqn
 	lom.HrwFQN = &lom.FQN
 }
 
diff --git a/fs/content.go b/fs/content.go
index d4b35bada4b..d675ba3828a 100644
--- a/fs/content.go
+++ b/fs/content.go
@@ -40,15 +40,6 @@ const (
 	ECMetaType   = "mt"
 )
 
-// file name too long (0x24)
-// see FnameTooLong below
-const (
-	PrefixFntl = ".x"
-
-	maxLenBasename = 255
-	maxLenPath     = 4000 // ref: PATH_MAX = 4096 in /usr/include/limits.h
-)
-
 type (
 	ContentResolver interface {
 		// Generates unique base name for original one. This function may add
@@ -123,15 +114,6 @@ func (f *contentSpecMgr) _reg(contentType string, spec ContentResolver) error {
 	return nil
 }
 
-func FnameTooLong(objName string) bool {
-	if l := len(objName); l > maxLenBasename {
-		if l > maxLenPath || len(filepath.Base(objName)) > maxLenBasename {
-			return true
-		}
-	}
-	return false
-}
-
 // Gen returns a new FQN generated from given parts.
 func (f *contentSpecMgr) Gen(parts PartsFQN, contentType, prefix string) (fqn string) {
 	var (
@@ -143,9 +125,9 @@ func (f *contentSpecMgr) Gen(parts PartsFQN, contentType, prefix string) (fqn st
 	// override caller-provided  `objName` to prevent "file name too long" errno 0x24
 	// - full pathname should be fine as (validated) bucket name <= 64
 	// - see related: core/lom and "fixup fntl"
-	if FnameTooLong(objName) {
-		nlog.Warningln("file name too long (0x24):", objName)
-		objName = PrefixFntl + cos.ChecksumB2S(cos.UnsafeB(objName), cos.ChecksumSHA256)
+	if IsFntl(objName) {
+		nlog.Warningln("fntl:", objName)
+		objName = ShortenFntl(objName)
 	}
 
 	return parts.Mountpath().MakePathFQN(parts.Bucket(), contentType, objName)
diff --git a/fs/fntl.go b/fs/fntl.go
new file mode 100644
index 00000000000..0bfd38f1e7b
--- /dev/null
+++ b/fs/fntl.go
@@ -0,0 +1,44 @@
+// Package fs provides mountpath and FQN abstractions and methods to resolve/map stored content
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
+ */
+package fs
+
+import (
+	"path/filepath"
+	"strings"
+
+	"github.com/NVIDIA/aistore/cmn/cos"
+)
+
+// file name too long (0x24)
+const (
+	prefixFntl = ".x"
+
+	// a typical local FS limitation
+	maxLenBasename = 255
+
+	// a.k.a. maximum FQN length before we decide to shorten it
+	// given that Linux PATH_MAX = 4096 (see /usr/include/limits.h)
+	// this number also takes into account:
+	// - maxLenMountpath = 255 (see cmn/config)
+	// - max bucket name = 64  (see cos.CheckAlphaPlus)
+	maxLenPath = 3072
+)
+
+func HasPrefixFntl(s string) bool {
+	return strings.HasPrefix(s, prefixFntl)
+}
+
+func ShortenFntl(s string) string {
+	return prefixFntl + cos.ChecksumB2S(cos.UnsafeB(s), cos.ChecksumSHA256)
+}
+
+func IsFntl(objName string) bool {
+	if l := len(objName); l > maxLenBasename {
+		if l > maxLenPath || len(filepath.Base(objName)) > maxLenBasename {
+			return true
+		}
+	}
+	return false
+}
diff --git a/xact/xs/wi_lso.go b/xact/xs/wi_lso.go
index 31db472d397..f55da8fe495 100644
--- a/xact/xs/wi_lso.go
+++ b/xact/xs/wi_lso.go
@@ -162,7 +162,7 @@ func (wi *walkInfo) _cb(lom *core.LOM, fqn string) (*cmn.LsoEnt, error) {
 	}
 
 	// shortcut #1: name-only optimizes-out loading md (NOTE: won't show misplaced and copies)
-	if wi.msg.IsFlagSet(apc.LsNameOnly) && !lom.HasFntlPrefix() {
+	if wi.msg.IsFlagSet(apc.LsNameOnly) && !fs.HasPrefixFntl(lom.ObjName) {
 		if !isOK(status) {
 			return nil, nil
 		}