upspinfs: corrects link size handling

Correctly reports the symlink size as the size of the content
printed out by Readlink.

Old code reported size 0 for symlinks.  This is correct per
upspin specification, but not correct when upspin is viewed
through a filesystem mount.  Reporting incorrect size led
to weird artifacts when using git on top of upspinfs, as
git would interpret file size of 0 as file content removal,
and would generate spurious diffs.

Fixes #619.

Change-Id: I17d3ebd04998f7167d060cb90256936a75cc8d7f
Reviewed-on: https://upspin-review.googlesource.com/c/upspin/+/19280
Reviewed-by: David Presotto <presotto@gmail.com>
diff --git a/cmd/upspinfs/fs.go b/cmd/upspinfs/fs.go
index 1bb9b66..9849ea1 100644
--- a/cmd/upspinfs/fs.go
+++ b/cmd/upspinfs/fs.go
@@ -424,8 +424,10 @@
 			continue
 		}
 		cn.Lock()
-		if sz, err := child.Size(); err == nil {
-			cn.attr.Size = uint64(sz)
+		if sz, err := lstatSize(child, n); err == nil {
+			cn.attr.Size = sz
+		} else {
+			return nil, e2e(errors.E(op, err, n.uname))
 		}
 		cn.attr.Mtime = child.Time.Go()
 		cn.Unlock()
@@ -568,6 +570,25 @@
 	return nil
 }
 
+// lstatSize returns a lstat-compatible size for the dir entry.  The size only
+// differs for symlinks.  Upspin's DirEntry size for a link is zero, but for
+// lstat, the size of the link is the size of the link content.
+func lstatSize(de *upspin.DirEntry, n *node) (uint64, error) {
+	if !de.IsLink() {
+		s, err := de.Size()
+		return uint64(s), err
+	}
+	// It seems that upspin treats all symlinks that don't leave the filesystem
+	// as relative, so replicate that approach here.  This may have interesting
+	// side effects for programs that care about precise link content, like
+	// version control systems.
+	p, err := n.upspinPathToHostPath(de.Link)
+	if err != nil {
+		return 0, err
+	}
+	return uint64(len(p)), nil
+}
+
 // Lookup implements fs.NodeStringLookuper.Lookup. 'n' must be a directory.
 // We do not use cached knowledge of 'n's contents.
 func (n *node) Lookup(context gContext.Context, name string) (fs.Node, error) {
@@ -616,12 +637,12 @@
 	if de.IsLink() {
 		mode |= os.ModeSymlink
 	}
-	size, err := de.Size()
+	size, err := lstatSize(de, n)
 	if err != nil {
 		f.removeMapping(uname)
 		return nil, e2e(errors.E(op, n.uname, err))
 	}
-	nn := n.f.allocNode(n, name, mode, uint64(size), de.Time.Go())
+	nn := n.f.allocNode(n, name, mode, size, de.Time.Go())
 	if de.IsLink() {
 		nn.link = upspin.PathName(de.Link)
 	}
diff --git a/cmd/upspinfs/upspinfs_test.go b/cmd/upspinfs/upspinfs_test.go
index 20e074a..d39c596 100644
--- a/cmd/upspinfs/upspinfs_test.go
+++ b/cmd/upspinfs/upspinfs_test.go
@@ -465,6 +465,13 @@
 	if val != relative {
 		fatalf(t, "%s: Readlink returned %s, expected %s:]", link, val, relative)
 	}
+	s, err := os.Lstat(link)
+	if err != nil {
+		fatal(t, err)
+	}
+	if s.Size() != int64(len(relative)) {
+		fatalf(t, "%s(%v): Lstat returned size %v, expected %v, relative: %q, rooted: %q:]", link, len(link), s.Size(), len(relative), relative, rooted)
+	}
 	remove(t, link)
 
 	// Create and test using relative name.
@@ -478,6 +485,13 @@
 	if val != relative {
 		fatalf(t, "%s: Readlink returned %s, expected %s", link, val, relative)
 	}
+	s, err = os.Lstat(link)
+	if err != nil {
+		fatal(t, err)
+	}
+	if s.Size() != int64(len(relative)) {
+		fatalf(t, "%s(%v): Lstat returned size %v, expected %v, relative: %q, rooted: %q:]", link, len(link), s.Size(), len(relative), relative, rooted)
+	}
 }
 
 // TestRename tests renaming a file.
diff --git a/cmd/upspinfs/watch.go b/cmd/upspinfs/watch.go
index 0b315af..b53ef6d 100644
--- a/cmd/upspinfs/watch.go
+++ b/cmd/upspinfs/watch.go
@@ -203,12 +203,12 @@
 	if de.IsLink() {
 		mode |= os.ModeSymlink
 	}
-	size, err := de.Size()
+	size, err := lstatSize(de, n)
 	if err != nil {
 		n.f.removeMapping(n.uname)
 		return e2e(errors.E(op, err))
 	}
-	n.attr.Size = uint64(size)
+	n.attr.Size = size
 	n.attr.Mode = mode
 	if de.IsLink() {
 		n.link = upspin.PathName(de.Link)
@@ -400,9 +400,9 @@
 		if e.Entry.IsLink() {
 			mode |= os.ModeSymlink
 		}
-		size, err := e.Entry.Size()
+		size, err := lstatSize(e.Entry, n)
 		if err == nil {
-			n.attr.Size = uint64(size)
+			n.attr.Size = size
 		} else {
 			log.Debug.Printf("upspinfs.watch: %s", err)
 		}