Repository: juicedata/juicefs
Pull request #2593 (2022-08-24 08:28:47)

CI: add sync test with hypothesis


Patch:
diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml
index 99802e3b..d6d4d475 100644
--- a/.github/workflows/sync.yml
+++ b/.github/workflows/sync.yml
@@ -99,6 +99,56 @@ jobs:
           slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
 
 
+  compare-with-rsync-randomly:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 1
+
+      - name: Set up Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: '1.18.x'
+
+      - name: Build linux target
+        run: make juicefs
+
+      - name: Checkout source code for test
+        uses: actions/checkout@v2
+        with:
+          repository: juicedata/juicefs
+          path: jfs_source
+      
+      - name: install hypothesis
+        run: | 
+          sudo pip install hypothesis
+
+      - name: Test Sync
+        run: |
+          python3 .github/scripts/testSync.py
+
+      - name: Log
+        if: ${{ always() }}
+        run: |
+          echo "juicefs log"
+          sudo tail -n 1000 ~/.juicefs/juicefs.log
+          grep "<FATAL>:" ~/.juicefs/juicefs.log && exit 1 || true
+          # grep "database is locked" ~/.juicefs/juicefs.log && exit 1 || true
+
+      - name: Send Slack Notification
+        if: ${{ failure() }}
+        uses: juicedata/slack-notify-action@main
+        with:
+          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
+          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
+
+      # - name: Setup upterm session
+      #   if: ${{ failure() }}
+      #   minutes-timeout: 30
+      #   uses: lhotari/action-upterm@v1
+
   file-head-test:
     runs-on: ubuntu-latest
     steps:
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index aff691dd..a8acb865 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -3,18 +3,16 @@ name: verify
 on:
   push:
     branches:
-    - main
-    - 'release-**'
-    paths:
-      - '**.go'
+      - main
+      - 'release-**'
+    paths-ignore:
+      - 'docs/**'
+      - '**.md'
+      - '.github/**'
   pull_request:
     branches:
       - 'main'
       - 'release-**'
-    paths:
-      - '**.yml'
-      - '**.go'
-      - 'Makefile'
     paths-ignore:
       - 'docs/**'
       - '**.md'
diff --git a/go.mod b/go.mod
index f3c36f65..9750ee86 100644
--- a/go.mod
+++ b/go.mod
@@ -233,7 +233,7 @@ require (
 
 replace github.com/minio/minio v0.0.0-20210206053228-97fe57bba92c => github.com/juicedata/minio v0.0.0-20220613143934-cee0571a1b03
 
-replace github.com/hanwen/go-fuse/v2 v2.1.1-0.20210611132105-24a1dfe6b4f8 => github.com/juicedata/go-fuse/v2 v2.1.1-0.20220720062817-183cb227b353
+replace github.com/hanwen/go-fuse/v2 v2.1.1-0.20210611132105-24a1dfe6b4f8 => github.com/juicedata/go-fuse/v2 v2.1.1-0.20220822092405-ece777deb343
 
 replace github.com/dgrijalva/jwt-go v3.2.0+incompatible => github.com/golang-jwt/jwt v3.2.1+incompatible
 
diff --git a/go.sum b/go.sum
index 6180d4c8..aa49fa07 100644
--- a/go.sum
+++ b/go.sum
@@ -598,8 +598,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/juicedata/go-fuse/v2 v2.1.1-0.20220720062817-183cb227b353 h1:cIwY+HZQiRQAYoCItVNCkpFDyPahgZzJeNM3rMgZSOE=
-github.com/juicedata/go-fuse/v2 v2.1.1-0.20220720062817-183cb227b353/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
+github.com/juicedata/go-fuse/v2 v2.1.1-0.20220822092405-ece777deb343 h1:fbMz7gSlX5eYwuWNB3Z1+dK2q2Ekg5tw6uSYYTOdZrw=
+github.com/juicedata/go-fuse/v2 v2.1.1-0.20220822092405-ece777deb343/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
 github.com/juicedata/godaemon v0.0.0-20210629045518-3da5144a127d h1:kpQMvNZJKGY3PTt7OSoahYc4nM0HY67SvK0YyS0GLwA=
 github.com/juicedata/godaemon v0.0.0-20210629045518-3da5144a127d/go.mod h1:dlxKkLh3qAIPtgr2U/RVzsZJDuXA1ffg+Njikfmhvgw=
 github.com/juicedata/minio v0.0.0-20220613143934-cee0571a1b03 h1:+0jiEkr6qE5Zm2+LilAv5pV/+V+V1XZ5KUv0pmXZHhQ=
diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go
index b3ce3fc6..2f0336cc 100644
--- a/pkg/fuse/fuse.go
+++ b/pkg/fuse/fuse.go
@@ -421,6 +421,13 @@ func (fs *fileSystem) StatFs(cancel <-chan struct{}, in *fuse.InHeader, out *fus
 	return 0
 }
 
+func (fs *fileSystem) Ioctl(cancel <-chan struct{}, in *fuse.IoctlIn, out *fuse.IoctlOut, bufIn, bufOut []byte) (status fuse.Status) {
+	ctx := newContext(cancel, &in.InHeader)
+	defer releaseContext(ctx)
+	out.Result = int32(fs.v.Ioctl(ctx, Ino(in.NodeId), in.Cmd, in.Arg, bufIn, bufOut))
+	return 0
+}
+
 // Serve starts a server to serve requests from FUSE.
 func Serve(v *vfs.VFS, options string, xattrs bool) error {
 	if err := syscall.Setpriority(syscall.PRIO_PROCESS, os.Getpid(), -19); err != nil {
diff --git a/pkg/meta/base.go b/pkg/meta/base.go
index 85439cc5..5d89d835 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -834,6 +834,19 @@ func (m *baseMeta) Open(ctx Context, inode Ino, flags uint32, attr *Attr) syscal
 	if attr != nil && !attr.Full {
 		err = m.GetAttr(ctx, inode, attr)
 	}
+	if attr.Flags&FlagImmutable != 0 {
+		if flags&(syscall.O_WRONLY|syscall.O_RDWR) != 0 {
+			return syscall.EPERM
+		}
+	}
+	if attr.Flags&FlagAppend != 0 {
+		if (flags&(syscall.O_WRONLY|syscall.O_RDWR)) != 0 && (flags&syscall.O_APPEND) == 0 {
+			return syscall.EPERM
+		}
+		if flags&syscall.O_TRUNC != 0 {
+			return syscall.EPERM
+		}
+	}
 	if err == 0 {
 		m.of.Open(inode, attr)
 	}
diff --git a/pkg/meta/interface.go b/pkg/meta/interface.go
index 3cbea3a3..e67e17b1 100644
--- a/pkg/meta/interface.go
+++ b/pkg/meta/interface.go
@@ -75,6 +75,12 @@ const (
 	SetAttrCtime
 	SetAttrAtimeNow
 	SetAttrMtimeNow
+	SetAttrFlag = 1 << 15
+)
+
+const (
+	FlagImmutable = 1 << iota
+	FlagAppend
 )
 
 const MaxName = 255
@@ -99,7 +105,7 @@ type MsgCallback func(...interface{}) error
 
 // Attr represents attributes of a node.
 type Attr struct {
-	Flags     uint8  // reserved flags
+	Flags     uint8  // flags
 	Typ       uint8  // type of a node
 	Mode      uint16 // permission mode
 	Uid       uint32 // owner id
diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go
index 99114455..8aa1640a 100644
--- a/pkg/meta/redis.go
+++ b/pkg/meta/redis.go
@@ -930,6 +930,12 @@ func (m *redisMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, si
 		if t.Typ != TypeFile {
 			return syscall.EPERM
 		}
+		if (t.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
+		if (t.Flags&FlagAppend) != 0 && (mode&^fallocKeepSize) != 0 {
+			return syscall.EPERM
+		}
 		length := t.Length
 		if off+size > t.Length {
 			if mode&fallocKeepSize == 0 {
@@ -1037,6 +1043,10 @@ func (m *redisMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode u
 			cur.Mtimensec = uint32(now.Nanosecond())
 			changed = true
 		}
+		if set&SetAttrFlag != 0 {
+			cur.Flags = attr.Flags
+			changed = true
+		}
 		if !changed {
 			*attr = cur
 			return nil
@@ -1106,6 +1116,9 @@ func (m *redisMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, m
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		buf, err := tx.HGet(ctx, m.entryKey(parent), name).Bytes()
 		if err != nil && err != redis.Nil {
@@ -1229,6 +1242,9 @@ func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags&FlagAppend) != 0 || (pattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		var updateParent bool
 		now := time.Now()
 		if !isTrash(parent) && now.Sub(time.Unix(pattr.Mtime, int64(pattr.Mtimensec))) >= minUpdateTime {
@@ -1243,6 +1259,9 @@ func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno
 			if ctx.Uid() != 0 && pattr.Mode&01000 != 0 && ctx.Uid() != pattr.Uid && ctx.Uid() != attr.Uid {
 				return syscall.EACCES
 			}
+			if (attr.Flags&FlagAppend) != 0 || (attr.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			attr.Ctime = now.Unix()
 			attr.Ctimensec = uint32(now.Nanosecond())
 			if trash == 0 {
@@ -1349,6 +1368,9 @@ func (m *redisMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags&FlagAppend) != 0 || (pattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		now := time.Now()
 		pattr.Nlink--
 		pattr.Mtime = now.Unix()
@@ -1482,6 +1504,9 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 			return syscall.ENOTDIR
 		}
 		m.parseAttr([]byte(rs[2].(string)), &iattr)
+		if (sattr.Flags&FlagAppend) != 0 || (sattr.Flags&FlagImmutable) != 0 || (dattr.Flags&FlagImmutable) != 0 || (iattr.Flags&FlagAppend) != 0 || (iattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		var supdate, dupdate bool
 		now := time.Now()
@@ -1491,6 +1516,9 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 				trash = 0
 			}
 			m.parseAttr([]byte(rs[3].(string)), &tattr)
+			if (tattr.Flags&FlagAppend) != 0 || (tattr.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			tattr.Ctime = now.Unix()
 			tattr.Ctimensec = uint32(now.Nanosecond())
 			if exchange {
@@ -1666,6 +1694,9 @@ func (m *redisMeta) doLink(ctx Context, inode, parent Ino, name string, attr *At
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if pattr.Flags&FlagImmutable != 0 {
+			return syscall.EPERM
+		}
 		var updateParent bool
 		now := time.Now()
 		if now.Sub(time.Unix(pattr.Mtime, int64(pattr.Mtimensec))) >= minUpdateTime {
@@ -1679,6 +1710,9 @@ func (m *redisMeta) doLink(ctx Context, inode, parent Ino, name string, attr *At
 		if iattr.Typ == TypeDirectory {
 			return syscall.EPERM
 		}
+		if (iattr.Flags&FlagAppend) != 0 || (iattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		oldParent := iattr.Parent
 		iattr.Parent = 0
 		iattr.Ctime = now.Unix()
@@ -2053,6 +2087,9 @@ func (m *redisMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino,
 		if attr.Typ != TypeFile {
 			return syscall.EINVAL
 		}
+		if (attr.Flags&FlagImmutable) != 0 || (attr.Flags&FlagAppend) != 0 {
+			return syscall.EPERM
+		}
 
 		newleng := offOut + size
 		if newleng > attr.Length {
diff --git a/pkg/meta/sql.go b/pkg/meta/sql.go
index bd25d6b4..a41f28ae 100644
--- a/pkg/meta/sql.go
+++ b/pkg/meta/sql.go
@@ -867,12 +867,16 @@ func (m *dbMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint
 			cur.Mtime = now
 			changed = true
 		}
+		if set&SetAttrFlag != 0 {
+			cur.Flags = attr.Flags
+			changed = true
+		}
 		m.parseAttr(&cur, attr)
 		if !changed {
 			return nil
 		}
 		cur.Ctime = now
-		_, err = s.Cols("mode", "uid", "gid", "atime", "mtime", "ctime").Update(&cur, &node{Inode: inode})
+		_, err = s.Cols("flags", "mode", "uid", "gid", "atime", "mtime", "ctime").Update(&cur, &node{Inode: inode})
 		if err == nil {
 			m.parseAttr(&cur, attr)
 		}
@@ -1013,6 +1017,12 @@ func (m *dbMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 		if n.Type != TypeFile {
 			return syscall.EPERM
 		}
+		if (n.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
+		if (n.Flags&FlagAppend) != 0 && (mode&^fallocKeepSize) != 0 {
+			return syscall.EPERM
+		}
 		length := n.Length
 		if off+size > n.Length {
 			if mode&fallocKeepSize == 0 {
@@ -1120,6 +1130,9 @@ func (m *dbMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 		if pn.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pn.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		var e = edge{Parent: parent, Name: []byte(name)}
 		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
@@ -1219,6 +1232,9 @@ func (m *dbMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 		if pn.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pn.Flags&FlagAppend) != 0 || (pn.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		var e = edge{Parent: parent, Name: []byte(name)}
 		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
@@ -1249,6 +1265,9 @@ func (m *dbMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 			if ctx.Uid() != 0 && pn.Mode&01000 != 0 && ctx.Uid() != pn.Uid && ctx.Uid() != n.Uid {
 				return syscall.EACCES
 			}
+			if (n.Flags&FlagAppend) != 0 || (n.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			n.Ctime = now
 			if trash == 0 {
 				n.Nlink--
@@ -1350,6 +1369,9 @@ func (m *dbMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
 		if pn.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if pn.Flags&FlagImmutable != 0 || pn.Flags&FlagAppend != 0 {
+			return syscall.EPERM
+		}
 		var e = edge{Parent: parent, Name: []byte(name)}
 		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
@@ -1465,6 +1487,9 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 		if spn.Type != TypeDirectory || dpn.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (spn.Flags&FlagAppend) != 0 || (spn.Flags&FlagImmutable) != 0 || (dpn.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		var se = edge{Parent: parentSrc, Name: []byte(nameSrc)}
 		ok, err := s.ForUpdate().Get(&se)
 		if err != nil {
@@ -1495,6 +1520,9 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 		if !ok {
 			return syscall.ENOENT
 		}
+		if (sn.Flags&FlagAppend) != 0 || (sn.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		var de = edge{Parent: parentDst, Name: []byte(nameDst)}
 		ok, err = s.ForUpdate().Get(&de)
@@ -1525,6 +1553,9 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				logger.Warnf("no attribute for inode %d (%d, %s)", dino, parentDst, de.Name)
 				trash = 0
 			}
+			if (dn.Flags&FlagAppend) != 0 || (dn.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			dn.Ctime = now
 			if exchange {
 				if parentSrc != parentDst {
@@ -1708,6 +1739,9 @@ func (m *dbMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr)
 		if pn.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if pn.Flags&FlagImmutable != 0 {
+			return syscall.EPERM
+		}
 		var e = edge{Parent: parent, Name: []byte(name)}
 		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
@@ -1728,6 +1762,9 @@ func (m *dbMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr)
 		if n.Type == TypeDirectory {
 			return syscall.EPERM
 		}
+		if (n.Flags&FlagAppend) != 0 || (n.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		var updateParent bool
 		now := time.Now().UnixNano() / 1e3
@@ -2059,6 +2096,9 @@ func (m *dbMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 		if nout.Type != TypeFile {
 			return syscall.EINVAL
 		}
+		if (nout.Flags&FlagImmutable) != 0 || (nout.Flags&FlagAppend) != 0 {
+			return syscall.EPERM
+		}
 
 		newleng := offOut + size
 		if newleng > nout.Length {
diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go
index 25b43ef5..f3827cef 100644
--- a/pkg/meta/tkv.go
+++ b/pkg/meta/tkv.go
@@ -794,6 +794,10 @@ func (m *kvMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint
 			cur.Mtimensec = uint32(now.Nanosecond())
 			changed = true
 		}
+		if set&SetAttrFlag != 0 {
+			cur.Flags = attr.Flags
+			changed = true
+		}
 		if !changed {
 			*attr = cur
 			return nil
@@ -911,6 +915,12 @@ func (m *kvMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 		if t.Typ != TypeFile {
 			return syscall.EPERM
 		}
+		if (t.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
+		if (t.Flags&FlagAppend) != 0 && (mode&^fallocKeepSize) != 0 {
+			return syscall.EPERM
+		}
 		length := t.Length
 		if off+size > t.Length {
 			if mode&fallocKeepSize == 0 {
@@ -1007,6 +1017,9 @@ func (m *kvMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags & FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		buf := tx.get(m.entryKey(parent, name))
 		var foundIno Ino
@@ -1114,12 +1127,20 @@ func (m *kvMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags&FlagAppend) != 0 || (pattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
+		attr = Attr{}
+		opened = false
 		now := time.Now()
 		if rs[1] != nil {
 			m.parseAttr(rs[1], &attr)
 			if ctx.Uid() != 0 && pattr.Mode&01000 != 0 && ctx.Uid() != pattr.Uid && ctx.Uid() != attr.Uid {
 				return syscall.EACCES
 			}
+			if (attr.Flags&FlagAppend) != 0 || (attr.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			attr.Ctime = now.Unix()
 			attr.Ctimensec = uint32(now.Nanosecond())
 			if trash == 0 {
@@ -1223,6 +1244,9 @@ func (m *kvMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if (pattr.Flags&FlagAppend) != 0 || (pattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		if tx.exist(m.entryKey(inode, "")) {
 			return syscall.ENOTEMPTY
 		}
@@ -1314,6 +1338,9 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 			return syscall.ENOTDIR
 		}
 		m.parseAttr(rs[2], &iattr)
+		if (sattr.Flags&FlagAppend) != 0 || (sattr.Flags&FlagImmutable) != 0 || (dattr.Flags&FlagImmutable) != 0 || (iattr.Flags&FlagAppend) != 0 || (iattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 
 		dbuf := tx.get(m.entryKey(parentDst, nameDst))
 		if dbuf == nil && m.conf.CaseInsensi {
@@ -1335,6 +1362,9 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				trash = 0
 			}
 			m.parseAttr(a, &tattr)
+			if (tattr.Flags&FlagAppend) != 0 || (tattr.Flags&FlagImmutable) != 0 {
+				return syscall.EPERM
+			}
 			tattr.Ctime = now.Unix()
 			tattr.Ctimensec = uint32(now.Nanosecond())
 			if exchange {
@@ -1497,10 +1527,16 @@ func (m *kvMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr)
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		if pattr.Flags&FlagImmutable != 0 {
+			return syscall.EPERM
+		}
 		m.parseAttr(rs[1], &iattr)
 		if iattr.Typ == TypeDirectory {
 			return syscall.EPERM
 		}
+		if (iattr.Flags&FlagAppend) != 0 || (iattr.Flags&FlagImmutable) != 0 {
+			return syscall.EPERM
+		}
 		buf := tx.get(m.entryKey(parent, name))
 		if buf != nil || buf == nil && m.conf.CaseInsensi && m.resolveCase(ctx, parent, name) != nil {
 			return syscall.EEXIST
@@ -1744,6 +1780,9 @@ func (m *kvMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 		if attr.Typ != TypeFile {
 			return syscall.EINVAL
 		}
+		if (attr.Flags&FlagImmutable) != 0 || (attr.Flags&FlagAppend) != 0 {
+			return syscall.EPERM
+		}
 
 		newleng := offOut + size
 		if newleng > attr.Length {
diff --git a/pkg/utils/buffer.go b/pkg/utils/buffer.go
index 4cc7e79d..46986ade 100644
--- a/pkg/utils/buffer.go
+++ b/pkg/utils/buffer.go
@@ -145,11 +145,11 @@ func (b *Buffer) Bytes() []byte {
 	return b.buf
 }
 
-var nativeEndian binary.ByteOrder
+var NativeEndian binary.ByteOrder
 
 // NewNativeBuffer utility to create *Buffer of given size with nativeEndian
 func NewNativeBuffer(buf []byte) *Buffer {
-	return &Buffer{nativeEndian, 0, buf}
+	return &Buffer{NativeEndian, 0, buf}
 }
 
 func init() {
@@ -158,9 +158,9 @@ func init() {
 
 	switch buf {
 	case [2]byte{0xCD, 0xAB}:
-		nativeEndian = binary.LittleEndian
+		NativeEndian = binary.LittleEndian
 	case [2]byte{0xAB, 0xCD}:
-		nativeEndian = binary.BigEndian
+		NativeEndian = binary.BigEndian
 	default:
 		panic("Could not determine native endianness.")
 	}
diff --git a/pkg/vfs/vfs_unix.go b/pkg/vfs/vfs_unix.go
index 62d6a6fb..291a90ce 100644
--- a/pkg/vfs/vfs_unix.go
+++ b/pkg/vfs/vfs_unix.go
@@ -26,6 +26,7 @@ import (
 	"syscall"
 
 	"github.com/juicedata/juicefs/pkg/meta"
+	"github.com/juicedata/juicefs/pkg/utils"
 
 	"golang.org/x/sys/unix"
 )
@@ -290,3 +291,64 @@ func (v *VFS) Flock(ctx Context, ino Ino, fh uint64, owner uint64, typ uint32, b
 	}
 	return
 }
+
+func (v *VFS) Ioctl(ctx Context, ino Ino, cmd uint32, arg uint64, bufIn, bufOut []byte) (err syscall.Errno) {
+	const (
+		FS_IOC_GETFLAGS    = 0x80086601
+		FS_IOC_SETFLAGS    = 0x40086602
+		FS_IOC_GETFLAGS_32 = 0x80046601
+		FS_IOC_SETFLAGS_32 = 0x40046602
+	)
+	const (
+		FS_IMMUTABLE_FL = 0x00000010
+		FS_APPEND_FL    = 0x00000020
+	)
+	defer func() { logit(ctx, "ioctl (%d,0x%X,0x%X,%v,%v): %s", ino, cmd, arg, bufIn, bufOut, strerr(err)) }()
+	switch cmd {
+	default:
+		err = syscall.ENOTTY
+		return
+	case FS_IOC_SETFLAGS, FS_IOC_GETFLAGS, FS_IOC_SETFLAGS_32, FS_IOC_GETFLAGS_32:
+	}
+	if IsSpecialNode(ino) {
+		err = syscall.EPERM
+		return
+	}
+	var attr = &Attr{}
+	if cmd>>30 == 1 { // set
+		var iflag uint64
+		if len(bufIn) == 8 {
+			iflag = utils.NativeEndian.Uint64(bufIn)
+		} else if len(bufIn) == 4 {
+			iflag = uint64(utils.NativeEndian.Uint32(bufIn))
+		} else {
+			err = syscall.EINVAL
+			return
+		}
+		if (iflag & FS_IMMUTABLE_FL) != 0 {
+			attr.Flags |= meta.FlagImmutable
+		}
+		if (iflag & FS_APPEND_FL) != 0 {
+			attr.Flags |= meta.FlagAppend
+		}
+		err = v.Meta.SetAttr(ctx, ino, meta.SetAttrFlag, 0, attr)
+	} else {
+		err = v.Meta.GetAttr(ctx, ino, attr)
+		var iflag uint64
+		if (attr.Flags & meta.FlagImmutable) != 0 {
+			iflag |= FS_IMMUTABLE_FL
+		}
+		if (attr.Flags & meta.FlagAppend) != 0 {
+			iflag |= FS_APPEND_FL
+		}
+		if len(bufOut) == 8 {
+			utils.NativeEndian.PutUint64(bufOut, iflag)
+		} else if len(bufOut) == 4 {
+			utils.NativeEndian.PutUint32(bufOut, uint32(iflag))
+		} else {
+			err = syscall.EINVAL
+			return
+		}
+	}
+	return
+}
diff --git a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
index d66487c6..4663e4cf 100644
--- a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
+++ b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
@@ -116,7 +116,7 @@ public class JuiceFileSystemImpl extends FileSystem {
 
     int jfs_pread(long pid, int fd, @Out ByteBuffer b, int len, long offset);
 
-    int jfs_write(long pid, int fd, @In byte[] b, int len);
+    int jfs_write(long pid, int fd, @In ByteBuffer b, int len);
 
     int jfs_flush(long pid, int fd);
 
@@ -954,7 +954,7 @@ public class JuiceFileSystemImpl extends FileSystem {
       if (b.length - off < len) {
         throw new IndexOutOfBoundsException();
       }
-      int done = lib.jfs_write(Thread.currentThread().getId(), fd, b, len);
+      int done = lib.jfs_write(Thread.currentThread().getId(), fd, ByteBuffer.wrap(b, off, len), len);
       if (done == EINVAL)
         throw new IOException("stream was closed");
       if (done < 0)
@@ -966,7 +966,7 @@ public class JuiceFileSystemImpl extends FileSystem {
 
     @Override
     public void write(int b) throws IOException {
-      int done = lib.jfs_write(Thread.currentThread().getId(), fd, new byte[]{(byte) b}, 1);
+      int done = lib.jfs_write(Thread.currentThread().getId(), fd, ByteBuffer.wrap(new byte[]{(byte) b}), 1);
       if (done == EINVAL)
         throw new IOException("stream was closed");
       if (done < 0)
@@ -1010,6 +1010,10 @@ public class JuiceFileSystemImpl extends FileSystem {
       flush();
       ((FSOutputStream) out).fsync();
     }
+
+    public OutputStream getOutputStream() {
+      return out;
+    }
   }
 
   static class BufferedFSOutputStreamWithStreamCapabilities extends BufferedFSOutputStream


Test patch:
diff --git a/.github/scripts/testSync.py b/.github/scripts/testSync.py
new file mode 100644
index 00000000..76c71e24
--- /dev/null
+++ b/.github/scripts/testSync.py
@@ -0,0 +1,131 @@
+import subprocess
+import random
+import shutil
+from hypothesis import given, strategies as st, settings, example
+import os
+
+JFS_SOURCE_DIR='/Users/chengzhou/Documents/juicefs/pkg/'
+JFS_SOURCE_DIR='jfs_source/pkg/'
+MOUNT_POINT='/tmp/sync-test/'
+JFS_BIN='./juicefs-1.0.0-beta1'
+JFS_BIN='./juicefs-1.0.0-beta2'
+JFS_BIN='./juicefs-1.0.0-beta3'
+JFS_BIN='./juicefs'
+MAX_EXAMPLE=100
+
+def setup():
+    meta_url = 'sqlite3://abc.db'
+    volume_name='sync-test'
+    if os.path.isfile('abc.db'):
+        os.remove('abc.db')
+    if os.path.exists(MOUNT_POINT):
+        os.system('umount %s'%MOUNT_POINT)
+    cache_dir = os.path.expanduser('~/.juicefs/local/%s/'%volume_name)
+    if os.path.exists(cache_dir):
+        try:
+            shutil.rmtree(cache_dir)
+        except OSError as e:
+            print("Error: %s : %s" % (cache_dir, e.strerror))
+    subprocess.check_call([JFS_BIN, 'format', meta_url, volume_name])
+    subprocess.check_call([JFS_BIN, 'mount', '-d', meta_url, MOUNT_POINT])
+    subprocess.check_call([JFS_BIN, 'sync', JFS_SOURCE_DIR, MOUNT_POINT+'jfs_source/'])
+
+def generate_all_entries(root_dir):
+    entries = set()
+    for root, dirs, files in os.walk(root_dir):
+        # print(root)
+        for d in dirs:
+            entries.add(d+'/')
+        for file in files:
+            entries.add(file)
+            file_path = os.path.join(root, file)[len(root_dir):]
+            entries.add(file_path)
+    print(len(entries))
+    return entries
+
+def generate_nested_dir(root_dir):
+    result = []
+    for root, dirs, files in os.walk(root_dir):
+        for d in dirs:
+            dir = os.path.join(root, d)[len(root_dir):]
+            li = dir.split('/')
+            entries = []
+            for i in range(0, len(li)):
+                entries.append('/'.join(li[i:])+'/')
+            for i in range(0, len(entries)):
+                result.append(random.sample(entries, random.randint(0, min(len(entries), 5)) ))
+    print(result)
+    return result
+
+def change_entry(entries):
+    # entries = random.sample( entries, random.randint(0, min(len(entries), 5)) )
+    options = []
+    for entry in entries:
+        type = random.choice(['--include', '--exclude'])
+        value = entry.replace(random.choice(entry), random.choice(['*', '?']), random.randint(0,2))
+        # print(type+' '+value)
+        options.append( (type, "'%s'"%value) )
+    # print(options)
+    return options
+
+all_entry = generate_all_entries(JFS_SOURCE_DIR)
+st_all_entry = st.lists(st.sampled_from(list(all_entry))).map(lambda x: change_entry(x)).filter(lambda x: len(x) != 0)
+nested_dir = generate_nested_dir(JFS_SOURCE_DIR)
+st_nested_dir = st.sampled_from(nested_dir).map(lambda x: change_entry(x)).filter(lambda x: len(x) != 0)
+valid_name = st.text(st.characters(max_codepoint=1000, blacklist_categories=('Cc', 'Cs')), min_size=2).map(lambda s: s.strip()).filter(lambda s: len(s) > 0)
+st_random_text = st.lists(valid_name).map(lambda x: change_entry(x)).filter(lambda x: len(x) != 0)
+
+@given(sync_options=st_random_text)
+@example([['--include', '[*'] ])
+@settings(max_examples=MAX_EXAMPLE, deadline=None)
+def test_sync_with_random_text(sync_options):
+    print(sync_options)
+    compare_rsync_and_juicesync(sync_options)
+
+@given(sync_options=st_all_entry)
+@settings(max_examples=MAX_EXAMPLE, deadline=None)
+def test_sync_with_path_entry(sync_options):
+    compare_rsync_and_juicesync(sync_options)
+
+@given(sync_options=st_nested_dir)
+@example([ ['--include', 'chu*/'],  ['--exclude', 'pk*/'],  ['--exclude', '*.go'] ])
+@settings(max_examples=MAX_EXAMPLE, deadline=None)
+def test_sync_with_nested_dir(sync_options):
+    compare_rsync_and_juicesync(sync_options)
+
+def compare_rsync_and_juicesync(sync_options):
+    assert sync_options != 0
+    sync_options = [item for sublist in sync_options for item in sublist]
+    do_rsync(MOUNT_POINT+'jfs_source/', 'rsync_dir/', sync_options)
+    do_juicesync(MOUNT_POINT+'jfs_source/', 'juicesync_dir/', sync_options)
+    diff_result = os.system('diff -ur juicesync_dir rsync_dir')
+    assert diff_result==0
+
+def do_juicesync(source_dir, dest_dir, sync_options):
+    if os.path.exists(dest_dir):
+        shutil.rmtree(dest_dir)
+    os.makedirs(dest_dir)
+    juicesync_cmd = [JFS_BIN , 'sync', '--dirs', source_dir, dest_dir]+sync_options
+    print('juicesync_cmd: '+' '.join(juicesync_cmd))
+    try:
+        subprocess.check_call(juicesync_cmd)
+    except Exception as e:
+        assert False
+
+def do_rsync(source_dir, dest_dir, sync_options):
+    if os.path.exists(dest_dir):
+        shutil.rmtree(dest_dir)
+    os.makedirs(dest_dir)
+    rsync_cmd = ['rsync', '-a', '-r' , source_dir,  dest_dir]+sync_options
+    print('rsync_cmd: '+ ' '.join(rsync_cmd))
+    try:
+        subprocess.check_call(rsync_cmd)
+    except Exception as e:
+        assert False
+
+if __name__ == "__main__":
+    setup()
+    test_sync_with_random_text()
+    test_sync_with_nested_dir()
+    test_sync_with_path_entry()
+    
diff --git a/pkg/meta/base_test.go b/pkg/meta/base_test.go
index 43ae398e..0c4055b1 100644
--- a/pkg/meta/base_test.go
+++ b/pkg/meta/base_test.go
@@ -69,6 +69,7 @@ func testMeta(t *testing.T, m Meta) {
 	testCopyFileRange(t, m)
 	testCloseSession(t, m)
 	testConcurrentDir(t, m)
+	testAttrFlags(t, m)
 	base := m.getBase()
 	base.conf.OpenCache = time.Second
 	base.of.expire = time.Second
@@ -1426,3 +1427,160 @@ func testConcurrentDir(t *testing.T, m Meta) {
 	}
 	g.Wait()
 }
+
+func testAttrFlags(t *testing.T, m Meta) {
+	ctx := Background
+	var attr = &Attr{}
+	var inode Ino
+	if st := m.Create(ctx, 1, "f", 0644, 022, 0, &inode, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	attr.Flags = FlagAppend
+	if st := m.SetAttr(ctx, inode, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.Open(ctx, inode, syscall.O_WRONLY, attr); st != syscall.EPERM {
+		t.Fatalf("open f: %s", st)
+	}
+	if st := m.Open(ctx, inode, syscall.O_WRONLY|syscall.O_APPEND, attr); st != 0 {
+		t.Fatalf("open f: %s", st)
+	}
+	attr.Flags = FlagAppend | FlagImmutable
+	if st := m.SetAttr(ctx, inode, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.Open(ctx, inode, syscall.O_WRONLY, attr); st != syscall.EPERM {
+		t.Fatalf("open f: %s", st)
+	}
+	if st := m.Open(ctx, inode, syscall.O_WRONLY|syscall.O_APPEND, attr); st != syscall.EPERM {
+		t.Fatalf("open f: %s", st)
+	}
+
+	var d Ino
+	if st := m.Mkdir(ctx, 1, "d", 0640, 022, 0, &d, attr); st != 0 {
+		t.Fatalf("mkdir d: %s", st)
+	}
+	attr.Flags = FlagAppend
+	if st := m.SetAttr(ctx, d, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Create(ctx, d, "f", 0644, 022, 0, &inode, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	if st := m.Unlink(ctx, d, "f"); st != syscall.EPERM {
+		t.Fatalf("unlink f: %s", st)
+	}
+	attr.Flags = FlagAppend | FlagImmutable
+	if st := m.SetAttr(ctx, d, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Create(ctx, d, "f2", 0644, 022, 0, &inode, nil); st != syscall.EPERM {
+		t.Fatalf("create f2: %s", st)
+	}
+
+	var Immutable Ino
+	if st := m.Mkdir(ctx, 1, "ImmutFile", 0640, 022, 0, &Immutable, attr); st != 0 {
+		t.Fatalf("mkdir d: %s", st)
+	}
+	attr.Flags = FlagImmutable
+	if st := m.SetAttr(ctx, Immutable, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Create(ctx, Immutable, "f2", 0644, 022, 0, &inode, nil); st != syscall.EPERM {
+		t.Fatalf("create f2: %s", st)
+	}
+
+	var src1, dst1, mfile Ino
+	attr.Flags = 0
+	if st := m.Mkdir(ctx, 1, "src1", 0640, 022, 0, &src1, attr); st != 0 {
+		t.Fatalf("mkdir src1: %s", st)
+	}
+	if st := m.Create(ctx, src1, "mfile", 0644, 022, 0, &mfile, nil); st != 0 {
+		t.Fatalf("create mfile: %s", st)
+	}
+	if st := m.Mkdir(ctx, 1, "dst1", 0640, 022, 0, &dst1, attr); st != 0 {
+		t.Fatalf("mkdir dst1: %s", st)
+	}
+
+	attr.Flags = FlagAppend
+	if st := m.SetAttr(ctx, src1, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Rename(ctx, src1, "mfile", dst1, "mfile", 0, &mfile, attr); st != syscall.EPERM {
+		t.Fatalf("rename d: %s", st)
+	}
+
+	attr.Flags = FlagImmutable
+	if st := m.SetAttr(ctx, src1, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Rename(ctx, src1, "mfile", dst1, "mfile", 0, &mfile, attr); st != syscall.EPERM {
+		t.Fatalf("rename d: %s", st)
+	}
+
+	if st := m.SetAttr(ctx, dst1, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Rename(ctx, src1, "mfile", dst1, "mfile", 0, &mfile, attr); st != syscall.EPERM {
+		t.Fatalf("rename d: %s", st)
+	}
+
+	var delFile Ino
+	if st := m.Create(ctx, 1, "delfile", 0644, 022, 0, &delFile, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	attr.Flags = FlagImmutable | FlagAppend
+	if st := m.SetAttr(ctx, delFile, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr d: %s", st)
+	}
+	if st := m.Unlink(ctx, 1, "delfile"); st != syscall.EPERM {
+		t.Fatalf("unlink f: %s", st)
+	}
+
+	var fallocFile Ino
+	if st := m.Create(ctx, 1, "fallocfile", 0644, 022, 0, &fallocFile, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	attr.Flags = FlagAppend
+	if st := m.SetAttr(ctx, fallocFile, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.Fallocate(ctx, fallocFile, fallocKeepSize, 0, 1024); st != 0 {
+		t.Fatalf("fallocate f: %s", st)
+	}
+	if st := m.Fallocate(ctx, fallocFile, fallocKeepSize|fallocZeroRange, 0, 1024); st != syscall.EPERM {
+		t.Fatalf("fallocate f: %s", st)
+	}
+	attr.Flags = FlagImmutable
+	if st := m.SetAttr(ctx, fallocFile, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.Fallocate(ctx, fallocFile, fallocKeepSize, 0, 1024); st != syscall.EPERM {
+		t.Fatalf("fallocate f: %s", st)
+	}
+
+	var copysrcFile, copydstFile Ino
+	if st := m.Create(ctx, 1, "copysrcfile", 0644, 022, 0, &copysrcFile, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	if st := m.Create(ctx, 1, "copydstfile", 0644, 022, 0, &copydstFile, nil); st != 0 {
+		t.Fatalf("create f: %s", st)
+	}
+	if st := m.Fallocate(ctx, copysrcFile, 0, 0, 1024); st != 0 {
+		t.Fatalf("fallocate f: %s", st)
+	}
+	attr.Flags = FlagAppend
+	if st := m.SetAttr(ctx, copydstFile, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.CopyFileRange(ctx, copysrcFile, 0, copydstFile, 0, 1024, 0, nil); st != syscall.EPERM {
+		t.Fatalf("copy_file_range f: %s", st)
+	}
+	attr.Flags = FlagImmutable
+	if st := m.SetAttr(ctx, copydstFile, SetAttrFlag, 0, attr); st != 0 {
+		t.Fatalf("setattr f: %s", st)
+	}
+	if st := m.CopyFileRange(ctx, copysrcFile, 0, copydstFile, 0, 1024, 0, nil); st != syscall.EPERM {
+		t.Fatalf("copy_file_range f: %s", st)
+	}
+}
diff --git a/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java b/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
index 206cccd9..45cf28d8 100644
--- a/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
+++ b/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
@@ -29,6 +29,7 @@ import org.apache.hadoop.security.UserGroupInformation;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.net.URI;
 import java.nio.ByteBuffer;
 import java.security.PrivilegedExceptionAction;
@@ -134,6 +135,18 @@ public class JuiceFileSystemTest extends TestCase {
     }
   }
 
+  public void testWrite() throws Exception {
+    Path f = new Path("/testWriteFile");
+    FSDataOutputStream fou = fs.create(f);
+    byte[] b = "hello world".getBytes();
+    OutputStream ou = ((JuiceFileSystemImpl.BufferedFSOutputStream)fou.getWrappedStream()).getOutputStream();
+    ou.write(b, 6, 5);
+    ou.close();
+    FSDataInputStream in = fs.open(f);
+    String str = IOUtils.toString(in);
+    assertEquals("world", str);
+  }
+
   public void testReadSkip() throws Exception {
     Path p = new Path("/test_readskip");
     fs.create(p).close();