Merge branch '2019-04-09-master-imports-fs'

- test.py tests for mmc
- ext4 symlink support and other fixes
- ext4 block group descriptor sizing
diff --git a/cmd/fs.c b/cmd/fs.c
index 9446767..aaafbf9 100644
--- a/cmd/fs.c
+++ b/cmd/fs.c
@@ -76,6 +76,20 @@
 	"      device type 'interface' instance 'dev'."
 )
 
+static int do_ln_wrapper(cmd_tbl_t *cmdtp, int flag, int argc,
+			 char * const argv[])
+{
+	return do_ln(cmdtp, flag, argc, argv, FS_TYPE_ANY);
+}
+
+U_BOOT_CMD(
+	ln,	5,	1,	do_ln_wrapper,
+	"Create a symbolic link",
+	"<interface> <dev[:part]> target linkname\n"
+	"    - create a symbolic link to 'target' with the name 'linkname' on\n"
+	"      device type 'interface' instance 'dev'."
+)
+
 static int do_fstype_wrapper(cmd_tbl_t *cmdtp, int flag, int argc,
 				char * const argv[])
 {
diff --git a/env/ext4.c b/env/ext4.c
index 09c5e4a..388474a 100644
--- a/env/ext4.c
+++ b/env/ext4.c
@@ -60,7 +60,7 @@
 	}
 
 	err = ext4fs_write(CONFIG_ENV_EXT4_FILE, (void *)&env_new,
-			   sizeof(env_t));
+			   sizeof(env_t), FILETYPE_REG);
 	ext4fs_close();
 
 	if (err == -1) {
diff --git a/fs/ext4/ext4_common.c b/fs/ext4/ext4_common.c
index 67e2471..464c33d 100644
--- a/fs/ext4/ext4_common.c
+++ b/fs/ext4/ext4_common.c
@@ -190,7 +190,7 @@
 	return res;
 }
 
-void put_ext4(uint64_t off, void *buf, uint32_t size)
+void put_ext4(uint64_t off, const void *buf, uint32_t size)
 {
 	uint64_t startblock;
 	uint64_t remainder;
@@ -510,7 +510,8 @@
 
 restart_read:
 	/* read the block no allocated to a file */
-	first_block_no_of_root = read_allocated_block(g_parent_inode, blk_idx);
+	first_block_no_of_root = read_allocated_block(g_parent_inode, blk_idx,
+						      NULL);
 	if (first_block_no_of_root <= 0)
 		goto fail;
 
@@ -607,7 +608,7 @@
 		dir->direntlen = cpu_to_le16(fs->blksz - totalbytes);
 
 	dir->namelen = strlen(filename);
-	dir->filetype = FILETYPE_REG;	/* regular file */
+	dir->filetype = file_type;
 	temp_dir = (char *)dir;
 	temp_dir = temp_dir + sizeof(struct ext2_dirent);
 	memcpy(temp_dir, filename, strlen(filename));
@@ -646,7 +647,7 @@
 
 	/* get the block no allocated to a file */
 	for (blk_idx = 0; blk_idx < directory_blocks; blk_idx++) {
-		blknr = read_allocated_block(parent_inode, blk_idx);
+		blknr = read_allocated_block(parent_inode, blk_idx, NULL);
 		if (blknr <= 0)
 			goto fail;
 
@@ -943,7 +944,7 @@
 
 	/* read the block no allocated to a file */
 	for (blk_idx = 0; blk_idx < directory_blocks; blk_idx++) {
-		blknr = read_allocated_block(g_parent_inode, blk_idx);
+		blknr = read_allocated_block(g_parent_inode, blk_idx, NULL);
 		if (blknr <= 0)
 			break;
 		inodeno = unlink_filename(filename, blknr);
@@ -1522,7 +1523,7 @@
 #endif
 
 static struct ext4_extent_header *ext4fs_get_extent_block
-	(struct ext2_data *data, char *buf,
+	(struct ext2_data *data, struct ext_block_cache *cache,
 		struct ext4_extent_header *ext_block,
 		uint32_t fileblock, int log2_blksz)
 {
@@ -1546,17 +1547,19 @@
 				break;
 		} while (fileblock >= le32_to_cpu(index[i].ei_block));
 
-		if (--i < 0)
-			return NULL;
+		/*
+		 * If first logical block number is higher than requested fileblock,
+		 * it is a sparse file. This is handled on upper layer.
+		 */
+		if (i > 0)
+			i--;
 
 		block = le16_to_cpu(index[i].ei_leaf_hi);
 		block = (block << 32) + le32_to_cpu(index[i].ei_leaf_lo);
-
-		if (ext4fs_devread((lbaint_t)block << log2_blksz, 0, blksz,
-				   buf))
-			ext_block = (struct ext4_extent_header *)buf;
-		else
+		block <<= log2_blksz;
+		if (!ext_cache_read(cache, (lbaint_t)block, blksz))
 			return NULL;
+		ext_block = (struct ext4_extent_header *)cache->buf;
 	}
 }
 
@@ -1584,7 +1587,7 @@
 
 int ext4fs_read_inode(struct ext2_data *data, int ino, struct ext2_inode *inode)
 {
-	struct ext2_block_group blkgrp;
+	struct ext2_block_group *blkgrp;
 	struct ext2_sblock *sblock = &data->sblock;
 	struct ext_filesystem *fs = get_fs();
 	int log2blksz = get_fs()->dev_desc->log2blksz;
@@ -1592,17 +1595,28 @@
 	long int blkno;
 	unsigned int blkoff;
 
+	/* Allocate blkgrp based on gdsize (for 64-bit support). */
+	blkgrp = zalloc(get_fs()->gdsize);
+	if (!blkgrp)
+		return 0;
+
 	/* It is easier to calculate if the first inode is 0. */
 	ino--;
 	status = ext4fs_blockgroup(data, ino / le32_to_cpu
-				   (sblock->inodes_per_group), &blkgrp);
-	if (status == 0)
+				   (sblock->inodes_per_group), blkgrp);
+	if (status == 0) {
+		free(blkgrp);
 		return 0;
+	}
 
 	inodes_per_block = EXT2_BLOCK_SIZE(data) / fs->inodesz;
-	blkno = ext4fs_bg_get_inode_table_id(&blkgrp, fs) +
+	blkno = ext4fs_bg_get_inode_table_id(blkgrp, fs) +
 	    (ino % le32_to_cpu(sblock->inodes_per_group)) / inodes_per_block;
 	blkoff = (ino % inodes_per_block) * fs->inodesz;
+
+	/* Free blkgrp as it is no longer required. */
+	free(blkgrp);
+
 	/* Read the inode. */
 	status = ext4fs_devread((lbaint_t)blkno << (LOG2_BLOCK_SIZE(data) -
 				log2blksz), blkoff,
@@ -1613,7 +1627,8 @@
 	return 1;
 }
 
-long int read_allocated_block(struct ext2_inode *inode, int fileblock)
+long int read_allocated_block(struct ext2_inode *inode, int fileblock,
+			      struct ext_block_cache *cache)
 {
 	long int blknr;
 	int blksz;
@@ -1630,20 +1645,26 @@
 
 	if (le32_to_cpu(inode->flags) & EXT4_EXTENTS_FL) {
 		long int startblock, endblock;
-		char *buf = zalloc(blksz);
-		if (!buf)
-			return -ENOMEM;
+		struct ext_block_cache *c, cd;
 		struct ext4_extent_header *ext_block;
 		struct ext4_extent *extent;
 		int i;
+
+		if (cache) {
+			c = cache;
+		} else {
+			c = &cd;
+			ext_cache_init(c);
+		}
 		ext_block =
-			ext4fs_get_extent_block(ext4fs_root, buf,
+			ext4fs_get_extent_block(ext4fs_root, c,
 						(struct ext4_extent_header *)
 						inode->b.blocks.dir_blocks,
 						fileblock, log2_blksz);
 		if (!ext_block) {
 			printf("invalid extent block\n");
-			free(buf);
+			if (!cache)
+				ext_cache_fini(c);
 			return -EINVAL;
 		}
 
@@ -1655,19 +1676,22 @@
 
 			if (startblock > fileblock) {
 				/* Sparse file */
-				free(buf);
+				if (!cache)
+					ext_cache_fini(c);
 				return 0;
 
 			} else if (fileblock < endblock) {
 				start = le16_to_cpu(extent[i].ee_start_hi);
 				start = (start << 32) +
 					le32_to_cpu(extent[i].ee_start_lo);
-				free(buf);
+				if (!cache)
+					ext_cache_fini(c);
 				return (fileblock - startblock) + start;
 			}
 		}
 
-		free(buf);
+		if (!cache)
+			ext_cache_fini(c);
 		return 0;
 	}
 
diff --git a/fs/ext4/ext4_common.h b/fs/ext4/ext4_common.h
index 1ee81ab..4dff191 100644
--- a/fs/ext4/ext4_common.h
+++ b/fs/ext4/ext4_common.h
@@ -72,7 +72,7 @@
 void ext4fs_allocate_blocks(struct ext2_inode *file_inode,
 				unsigned int total_remaining_blocks,
 				unsigned int *total_no_of_block);
-void put_ext4(uint64_t off, void *buf, uint32_t size);
+void put_ext4(uint64_t off, const void *buf, uint32_t size);
 struct ext2_block_group *ext4fs_get_group_descriptor
 	(const struct ext_filesystem *fs, uint32_t bg_idx);
 uint64_t ext4fs_bg_get_block_id(const struct ext2_block_group *bg,
diff --git a/fs/ext4/ext4_journal.c b/fs/ext4/ext4_journal.c
index 148593d..6adbab9 100644
--- a/fs/ext4/ext4_journal.c
+++ b/fs/ext4/ext4_journal.c
@@ -347,7 +347,7 @@
 	ext4fs_read_inode(ext4fs_root, EXT2_JOURNAL_INO,
 			  (struct ext2_inode *)&inode_journal);
 	blknr = read_allocated_block((struct ext2_inode *)
-				     &inode_journal, i);
+				     &inode_journal, i, NULL);
 	ext4fs_devread((lbaint_t)blknr * fs->sect_perblk, 0, fs->blksz,
 		       temp_buff);
 	p_jdb = (char *)temp_buff;
@@ -372,7 +372,7 @@
 				be32_to_cpu(jdb->h_sequence)) == 0)
 				continue;
 		}
-		blknr = read_allocated_block(&inode_journal, i);
+		blknr = read_allocated_block(&inode_journal, i, NULL);
 		ext4fs_devread((lbaint_t)blknr * fs->sect_perblk, 0,
 			       fs->blksz, metadata_buff);
 		put_ext4((uint64_t)((uint64_t)be32_to_cpu(tag->block) * (uint64_t)fs->blksz),
@@ -419,7 +419,8 @@
 	}
 
 	ext4fs_read_inode(ext4fs_root, EXT2_JOURNAL_INO, &inode_journal);
-	blknr = read_allocated_block(&inode_journal, EXT2_JOURNAL_SUPERBLOCK);
+	blknr = read_allocated_block(&inode_journal, EXT2_JOURNAL_SUPERBLOCK,
+				     NULL);
 	ext4fs_devread((lbaint_t)blknr * fs->sect_perblk, 0, fs->blksz,
 		       temp_buff);
 	jsb = (struct journal_superblock_t *) temp_buff;
@@ -443,7 +444,7 @@
 
 	i = be32_to_cpu(jsb->s_first);
 	while (1) {
-		blknr = read_allocated_block(&inode_journal, i);
+		blknr = read_allocated_block(&inode_journal, i, NULL);
 		memset(temp_buff1, '\0', fs->blksz);
 		ext4fs_devread((lbaint_t)blknr * fs->sect_perblk,
 			       0, fs->blksz, temp_buff1);
@@ -537,7 +538,7 @@
 		ext4_read_superblock((char *)fs->sb);
 
 		blknr = read_allocated_block(&inode_journal,
-					 EXT2_JOURNAL_SUPERBLOCK);
+					 EXT2_JOURNAL_SUPERBLOCK, NULL);
 		put_ext4((uint64_t) ((uint64_t)blknr * (uint64_t)fs->blksz),
 			 (struct journal_superblock_t *)temp_buff,
 			 (uint32_t) fs->blksz);
@@ -566,7 +567,7 @@
 
 	ext4fs_read_inode(ext4fs_root, EXT2_JOURNAL_INO, &inode_journal);
 	jsb_blknr = read_allocated_block(&inode_journal,
-					 EXT2_JOURNAL_SUPERBLOCK);
+					 EXT2_JOURNAL_SUPERBLOCK, NULL);
 	ext4fs_devread((lbaint_t)jsb_blknr * fs->sect_perblk, 0, fs->blksz,
 		       temp_buff);
 	jsb = (struct journal_superblock_t *) temp_buff;
@@ -618,7 +619,7 @@
 	ext4fs_read_inode(ext4fs_root, EXT2_JOURNAL_INO,
 			  &inode_journal);
 	jsb_blknr = read_allocated_block(&inode_journal,
-					 EXT2_JOURNAL_SUPERBLOCK);
+					 EXT2_JOURNAL_SUPERBLOCK, NULL);
 	ext4fs_devread((lbaint_t)jsb_blknr * fs->sect_perblk, 0, fs->blksz,
 		       temp_buff);
 	jsb = (struct journal_superblock_t *) temp_buff;
@@ -645,16 +646,17 @@
 	long int blknr;
 	int i;
 	ext4fs_read_inode(ext4fs_root, EXT2_JOURNAL_INO, &inode_journal);
-	blknr = read_allocated_block(&inode_journal, jrnl_blk_idx++);
+	blknr = read_allocated_block(&inode_journal, jrnl_blk_idx++, NULL);
 	update_descriptor_block(blknr);
 	for (i = 0; i < MAX_JOURNAL_ENTRIES; i++) {
 		if (journal_ptr[i]->blknr == -1)
 			break;
-		blknr = read_allocated_block(&inode_journal, jrnl_blk_idx++);
+		blknr = read_allocated_block(&inode_journal, jrnl_blk_idx++,
+					     NULL);
 		put_ext4((uint64_t) ((uint64_t)blknr * (uint64_t)fs->blksz),
 			 journal_ptr[i]->buf, fs->blksz);
 	}
-	blknr = read_allocated_block(&inode_journal, jrnl_blk_idx++);
+	blknr = read_allocated_block(&inode_journal, jrnl_blk_idx++, NULL);
 	update_commit_block(blknr);
 	printf("update journal finished\n");
 }
diff --git a/fs/ext4/ext4_write.c b/fs/ext4/ext4_write.c
index 4eb77c3..504d23a 100644
--- a/fs/ext4/ext4_write.c
+++ b/fs/ext4/ext4_write.c
@@ -465,6 +465,15 @@
 	if (le32_to_cpu(inode.size) % fs->blksz)
 		no_blocks++;
 
+	/*
+	 * special case for symlinks whose target are small enough that
+	 *it fits in struct ext2_inode.b.symlink: no block had been allocated
+	 */
+	if ((le16_to_cpu(inode.mode) & S_IFLNK) &&
+	    le32_to_cpu(inode.size) <= sizeof(inode.b.symlink)) {
+		no_blocks = 0;
+	}
+
 	if (le32_to_cpu(inode.flags) & EXT4_EXTENTS_FL) {
 		/* FIXME delete extent index blocks, i.e. eh_depth >= 1 */
 		struct ext4_extent_header *eh =
@@ -479,7 +488,7 @@
 
 	/* release data blocks */
 	for (i = 0; i < no_blocks; i++) {
-		blknr = read_allocated_block(&inode, i);
+		blknr = read_allocated_block(&inode, i, NULL);
 		if (blknr == 0)
 			continue;
 		if (blknr < 0)
@@ -695,7 +704,7 @@
 		ext4fs_read_inode(ext4fs_root, EXT2_JOURNAL_INO,
 				  &inode_journal);
 		blknr = read_allocated_block(&inode_journal,
-					EXT2_JOURNAL_SUPERBLOCK);
+					EXT2_JOURNAL_SUPERBLOCK, NULL);
 		ext4fs_devread((lbaint_t)blknr * fs->sect_perblk, 0, fs->blksz,
 			       temp_buff);
 		jsb = (struct journal_superblock_t *)temp_buff;
@@ -752,7 +761,7 @@
  * contigous sectors as ext4fs_read_file
  */
 static int ext4fs_write_file(struct ext2_inode *file_inode,
-			     int pos, unsigned int len, char *buf)
+			     int pos, unsigned int len, const char *buf)
 {
 	int i;
 	int blockcnt;
@@ -764,7 +773,7 @@
 	int delayed_start = 0;
 	int delayed_extent = 0;
 	int delayed_next = 0;
-	char *delayed_buf = NULL;
+	const char *delayed_buf = NULL;
 
 	/* Adjust len so it we can't read past the end of the file. */
 	if (len > filesize)
@@ -776,7 +785,7 @@
 		long int blknr;
 		int blockend = fs->blksz;
 		int skipfirst = 0;
-		blknr = read_allocated_block(file_inode, i);
+		blknr = read_allocated_block(file_inode, i, NULL);
 		if (blknr <= 0)
 			return -1;
 
@@ -816,7 +825,6 @@
 					 (uint32_t) delayed_extent);
 				previous_block_number = -1;
 			}
-			memset(buf, 0, fs->blksz - skipfirst);
 		}
 		buf += fs->blksz - skipfirst;
 	}
@@ -830,8 +838,8 @@
 	return len;
 }
 
-int ext4fs_write(const char *fname, unsigned char *buffer,
-					unsigned long sizebytes)
+int ext4fs_write(const char *fname, const char *buffer,
+		 unsigned long sizebytes, int type)
 {
 	int ret = 0;
 	struct ext2_inode *file_inode = NULL;
@@ -854,8 +862,12 @@
 	struct ext2_block_group *bgd = NULL;
 	struct ext_filesystem *fs = get_fs();
 	ALLOC_CACHE_ALIGN_BUFFER(char, filename, 256);
+	bool store_link_in_inode = false;
 	memset(filename, 0x00, 256);
 
+	if (type != FILETYPE_REG && type != FILETYPE_SYMLINK)
+		return -1;
+
 	g_parent_inode = zalloc(fs->inodesz);
 	if (!g_parent_inode)
 		goto fail;
@@ -893,8 +905,16 @@
 		if (ret)
 			goto fail;
 	}
+
+	/* calculate how many blocks required */
+	if (type == FILETYPE_SYMLINK &&
+	    sizebytes <= sizeof(file_inode->b.symlink)) {
+		store_link_in_inode = true;
+		bytes_reqd_for_file = 0;
+	} else {
+		bytes_reqd_for_file = sizebytes;
+	}
-	/* calucalate how many blocks required */
-	bytes_reqd_for_file = sizebytes;
+
 	blks_reqd_for_file = lldiv(bytes_reqd_for_file, fs->blksz);
 	if (do_div(bytes_reqd_for_file, fs->blksz) != 0) {
 		blks_reqd_for_file++;
@@ -907,7 +927,7 @@
 		goto fail;
 	}
 
-	inodeno = ext4fs_update_parent_dentry(filename, FILETYPE_REG);
+	inodeno = ext4fs_update_parent_dentry(filename, type);
 	if (inodeno == -1)
 		goto fail;
 	/* prepare file inode */
@@ -915,14 +935,23 @@
 	if (!inode_buffer)
 		goto fail;
 	file_inode = (struct ext2_inode *)inode_buffer;
-	file_inode->mode = cpu_to_le16(S_IFREG | S_IRWXU |
-	    S_IRGRP | S_IROTH | S_IXGRP | S_IXOTH);
+	file_inode->size = cpu_to_le32(sizebytes);
+	if (type == FILETYPE_SYMLINK) {
+		file_inode->mode = cpu_to_le16(S_IFLNK | S_IRWXU | S_IRWXG |
+					       S_IRWXO);
+		if (store_link_in_inode) {
+			strncpy(file_inode->b.symlink, buffer, sizebytes);
+			sizebytes = 0;
+		}
+	} else {
+		file_inode->mode = cpu_to_le16(S_IFREG | S_IRWXU | S_IRGRP |
+					       S_IROTH | S_IXGRP | S_IXOTH);
+	}
 	/* ToDo: Update correct time */
 	file_inode->mtime = cpu_to_le32(timestamp);
 	file_inode->atime = cpu_to_le32(timestamp);
 	file_inode->ctime = cpu_to_le32(timestamp);
 	file_inode->nlinks = cpu_to_le16(1);
-	file_inode->size = cpu_to_le32(sizebytes);
 
 	/* Allocate data blocks */
 	ext4fs_allocate_blocks(file_inode, blocks_remaining,
@@ -949,7 +978,7 @@
 	if (ext4fs_put_metadata(temp_ptr, itable_blkno))
 		goto fail;
 	/* copy the file content into data blocks */
-	if (ext4fs_write_file(file_inode, 0, sizebytes, (char *)buffer) == -1) {
+	if (ext4fs_write_file(file_inode, 0, sizebytes, buffer) == -1) {
 		printf("Error in copying content\n");
 		/* FIXME: Deallocate data blocks */
 		goto fail;
@@ -1014,7 +1043,7 @@
 		return -1;
 	}
 
-	ret = ext4fs_write(filename, buf, len);
+	ret = ext4fs_write(filename, buf, len, FILETYPE_REG);
 	if (ret) {
 		printf("** Error ext4fs_write() **\n");
 		goto fail;
@@ -1029,3 +1058,8 @@
 
 	return -1;
 }
+
+int ext4fs_create_link(const char *target, const char *fname)
+{
+	return ext4fs_write(fname, target, strlen(target), FILETYPE_SYMLINK);
+}
diff --git a/fs/ext4/ext4fs.c b/fs/ext4/ext4fs.c
index 2a28031..26db677 100644
--- a/fs/ext4/ext4fs.c
+++ b/fs/ext4/ext4fs.c
@@ -62,6 +62,9 @@
 	lbaint_t delayed_next = 0;
 	char *delayed_buf = NULL;
 	short status;
+	struct ext_block_cache cache;
+
+	ext_cache_init(&cache);
 
 	if (blocksize <= 0)
 		return -1;
@@ -77,9 +80,11 @@
 		int blockoff = pos - (blocksize * i);
 		int blockend = blocksize;
 		int skipfirst = 0;
-		blknr = read_allocated_block(&(node->inode), i);
-		if (blknr < 0)
+		blknr = read_allocated_block(&node->inode, i, &cache);
+		if (blknr < 0) {
+			ext_cache_fini(&cache);
 			return -1;
+		}
 
 		blknr = blknr << log2_fs_blocksize;
 
@@ -109,8 +114,10 @@
 							delayed_skipfirst,
 							delayed_extent,
 							delayed_buf);
-					if (status == 0)
+					if (status == 0) {
+						ext_cache_fini(&cache);
 						return -1;
+					}
 					previous_block_number = blknr;
 					delayed_start = blknr;
 					delayed_extent = blockend;
@@ -136,8 +143,10 @@
 							delayed_skipfirst,
 							delayed_extent,
 							delayed_buf);
-				if (status == 0)
+				if (status == 0) {
+					ext_cache_fini(&cache);
 					return -1;
+				}
 				previous_block_number = -1;
 			}
 			/* Zero no more than `len' bytes. */
@@ -153,12 +162,15 @@
 		status = ext4fs_devread(delayed_start,
 					delayed_skipfirst, delayed_extent,
 					delayed_buf);
-		if (status == 0)
+		if (status == 0) {
+			ext_cache_fini(&cache);
 			return -1;
+		}
 		previous_block_number = -1;
 	}
 
 	*actread  = len;
+	ext_cache_fini(&cache);
 	return 0;
 }
 
@@ -252,3 +264,32 @@
 	return -ENOSYS;
 #endif
 }
+
+void ext_cache_init(struct ext_block_cache *cache)
+{
+	memset(cache, 0, sizeof(*cache));
+}
+
+void ext_cache_fini(struct ext_block_cache *cache)
+{
+	free(cache->buf);
+	ext_cache_init(cache);
+}
+
+int ext_cache_read(struct ext_block_cache *cache, lbaint_t block, int size)
+{
+	/* This could be more lenient, but this is simple and enough for now */
+	if (cache->buf && cache->block == block && cache->size == size)
+		return 1;
+	ext_cache_fini(cache);
+	cache->buf = malloc(size);
+	if (!cache->buf)
+		return 0;
+	if (!ext4fs_devread(block, 0, size, cache->buf)) {
+		free(cache->buf);
+		return 0;
+	}
+	cache->block = block;
+	cache->size = size;
+	return 1;
+}
diff --git a/fs/fat/fat.c b/fs/fat/fat.c
index 6ade4ea..c5997c2 100644
--- a/fs/fat/fat.c
+++ b/fs/fat/fat.c
@@ -602,8 +602,13 @@
 		mydata->data_begin = mydata->rootdir_sect +
 					mydata->rootdir_size -
 					(mydata->clust_size * 2);
-		mydata->root_cluster =
-			sect_to_clust(mydata, mydata->rootdir_sect);
+
+		/*
+		 * The root directory is not cluster-aligned and may be on a
+		 * "negative" cluster, this will be handled specially in
+		 * next_cluster().
+		 */
+		mydata->root_cluster = 0;
 	}
 
 	mydata->fatbufnum = -1;
@@ -733,20 +738,38 @@
 	itr->last_cluster = 0;
 }
 
-static void *next_cluster(fat_itr *itr)
+static void *next_cluster(fat_itr *itr, unsigned *nbytes)
 {
 	fsdata *mydata = itr->fsdata;  /* for silly macros */
 	int ret;
 	u32 sect;
+	u32 read_size;
 
 	/* have we reached the end? */
 	if (itr->last_cluster)
 		return NULL;
 
-	sect = clust_to_sect(itr->fsdata, itr->next_clust);
+	if (itr->is_root && itr->fsdata->fatsize != 32) {
+		/*
+		 * The root directory is located before the data area and
+		 * cannot be indexed using the regular unsigned cluster
+		 * numbers (it may start at a "negative" cluster or not at a
+		 * cluster boundary at all), so consider itr->next_clust to be
+		 * a offset in cluster-sized units from the start of rootdir.
+		 */
+		unsigned sect_offset = itr->next_clust * itr->fsdata->clust_size;
+		unsigned remaining_sects = itr->fsdata->rootdir_size - sect_offset;
+		sect = itr->fsdata->rootdir_sect + sect_offset;
+		/* do not read past the end of rootdir */
+		read_size = min_t(u32, itr->fsdata->clust_size,
+				  remaining_sects);
+	} else {
+		sect = clust_to_sect(itr->fsdata, itr->next_clust);
+		read_size = itr->fsdata->clust_size;
+	}
 
-	debug("FAT read(sect=%d), clust_size=%d, DIRENTSPERBLOCK=%zd\n",
-	      sect, itr->fsdata->clust_size, DIRENTSPERBLOCK);
+	debug("FAT read(sect=%d), clust_size=%d, read_size=%u, DIRENTSPERBLOCK=%zd\n",
+	      sect, itr->fsdata->clust_size, read_size, DIRENTSPERBLOCK);
 
 	/*
 	 * NOTE: do_fat_read_at() had complicated logic to deal w/
@@ -757,18 +780,17 @@
 	 * dent at a time and iteratively constructing the vfat long
 	 * name.
 	 */
-	ret = disk_read(sect, itr->fsdata->clust_size,
-			itr->block);
+	ret = disk_read(sect, read_size, itr->block);
 	if (ret < 0) {
 		debug("Error: reading block\n");
 		return NULL;
 	}
 
+	*nbytes = read_size * itr->fsdata->sect_size;
 	itr->clust = itr->next_clust;
 	if (itr->is_root && itr->fsdata->fatsize != 32) {
 		itr->next_clust++;
-		sect = clust_to_sect(itr->fsdata, itr->next_clust);
-		if (sect - itr->fsdata->rootdir_sect >=
+		if (itr->next_clust * itr->fsdata->clust_size >=
 		    itr->fsdata->rootdir_size) {
 			debug("nextclust: 0x%x\n", itr->next_clust);
 			itr->last_cluster = 1;
@@ -787,9 +809,8 @@
 static dir_entry *next_dent(fat_itr *itr)
 {
 	if (itr->remaining == 0) {
-		struct dir_entry *dent = next_cluster(itr);
-		unsigned nbytes = itr->fsdata->sect_size *
-			itr->fsdata->clust_size;
+		unsigned nbytes;
+		struct dir_entry *dent = next_cluster(itr, &nbytes);
 
 		/* have we reached the last cluster? */
 		if (!dent) {
diff --git a/fs/fs.c b/fs/fs.c
index c5c35eb..736ebef 100644
--- a/fs/fs.c
+++ b/fs/fs.c
@@ -90,6 +90,11 @@
 	return -1;
 }
 
+static inline int fs_ln_unsupported(const char *filename, const char *target)
+{
+	return -1;
+}
+
 static inline void fs_close_unsupported(void)
 {
 }
@@ -154,6 +159,7 @@
 	void (*closedir)(struct fs_dir_stream *dirs);
 	int (*unlink)(const char *filename);
 	int (*mkdir)(const char *dirname);
+	int (*ln)(const char *filename, const char *target);
 };
 
 static struct fstype_info fstypes[] = {
@@ -181,6 +187,7 @@
 		.opendir = fat_opendir,
 		.readdir = fat_readdir,
 		.closedir = fat_closedir,
+		.ln = fs_ln_unsupported,
 	},
 #endif
 
@@ -197,8 +204,10 @@
 		.read = ext4_read_file,
 #ifdef CONFIG_CMD_EXT4_WRITE
 		.write = ext4_write_file,
+		.ln = ext4fs_create_link,
 #else
 		.write = fs_write_unsupported,
+		.ln = fs_ln_unsupported,
 #endif
 		.uuid = ext4fs_uuid,
 		.opendir = fs_opendir_unsupported,
@@ -222,6 +231,7 @@
 		.opendir = fs_opendir_unsupported,
 		.unlink = fs_unlink_unsupported,
 		.mkdir = fs_mkdir_unsupported,
+		.ln = fs_ln_unsupported,
 	},
 #endif
 #ifdef CONFIG_CMD_UBIFS
@@ -240,6 +250,7 @@
 		.opendir = fs_opendir_unsupported,
 		.unlink = fs_unlink_unsupported,
 		.mkdir = fs_mkdir_unsupported,
+		.ln = fs_ln_unsupported,
 	},
 #endif
 #ifdef CONFIG_FS_BTRFS
@@ -258,6 +269,7 @@
 		.opendir = fs_opendir_unsupported,
 		.unlink = fs_unlink_unsupported,
 		.mkdir = fs_mkdir_unsupported,
+		.ln = fs_ln_unsupported,
 	},
 #endif
 	{
@@ -275,6 +287,7 @@
 		.opendir = fs_opendir_unsupported,
 		.unlink = fs_unlink_unsupported,
 		.mkdir = fs_mkdir_unsupported,
+		.ln = fs_ln_unsupported,
 	},
 };
 
@@ -602,6 +615,22 @@
 	return ret;
 }
 
+int fs_ln(const char *fname, const char *target)
+{
+	struct fstype_info *info = fs_get_info(fs_type);
+	int ret;
+
+	ret = info->ln(fname, target);
+
+	if (ret < 0) {
+		printf("** Unable to create link %s -> %s **\n", fname, target);
+		ret = -1;
+	}
+	fs_close();
+
+	return ret;
+}
+
 int do_size(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
 		int fstype)
 {
@@ -840,3 +869,18 @@
 
 	return 0;
 }
+
+int do_ln(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
+	  int fstype)
+{
+	if (argc != 5)
+		return CMD_RET_USAGE;
+
+	if (fs_set_blk_dev(argv[1], argv[2], fstype))
+		return 1;
+
+	if (fs_ln(argv[3], argv[4]))
+		return 1;
+
+	return 0;
+}
diff --git a/include/ext4fs.h b/include/ext4fs.h
index 2421011..34585d4 100644
--- a/include/ext4fs.h
+++ b/include/ext4fs.h
@@ -117,6 +117,12 @@
 	struct blk_desc *dev_desc;
 };
 
+struct ext_block_cache {
+	char *buf;
+	lbaint_t block;
+	int size;
+};
+
 extern struct ext2_data *ext4fs_root;
 extern struct ext2fs_node *ext4fs_file;
 
@@ -128,10 +134,11 @@
 int ext4fs_init(void);
 void ext4fs_deinit(void);
 int ext4fs_filename_unlink(char *filename);
-int ext4fs_write(const char *fname, unsigned char *buffer,
-		 unsigned long sizebytes);
+int ext4fs_write(const char *fname, const char *buffer,
+				 unsigned long sizebytes, int type);
 int ext4_write_file(const char *filename, void *buf, loff_t offset, loff_t len,
 		    loff_t *actwrite);
+int ext4fs_create_link(const char *target, const char *fname);
 #endif
 
 struct ext_filesystem *get_fs(void);
@@ -146,11 +153,15 @@
 void ext4fs_free_node(struct ext2fs_node *node, struct ext2fs_node *currroot);
 int ext4fs_devread(lbaint_t sector, int byte_offset, int byte_len, char *buf);
 void ext4fs_set_blk_dev(struct blk_desc *rbdd, disk_partition_t *info);
-long int read_allocated_block(struct ext2_inode *inode, int fileblock);
+long int read_allocated_block(struct ext2_inode *inode, int fileblock,
+			      struct ext_block_cache *cache);
 int ext4fs_probe(struct blk_desc *fs_dev_desc,
 		 disk_partition_t *fs_partition);
 int ext4_read_file(const char *filename, void *buf, loff_t offset, loff_t len,
 		   loff_t *actread);
 int ext4_read_superblock(char *buffer);
 int ext4fs_uuid(char *uuid_str);
+void ext_cache_init(struct ext_block_cache *cache);
+void ext_cache_fini(struct ext_block_cache *cache);
+int ext_cache_read(struct ext_block_cache *cache, lbaint_t block, int size);
 #endif
diff --git a/include/fs.h b/include/fs.h
index aa3604d..6854597 100644
--- a/include/fs.h
+++ b/include/fs.h
@@ -191,6 +191,8 @@
 		int fstype);
 int do_mkdir(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
 		int fstype);
+int do_ln(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
+	  int fstype);
 
 /*
  * Determine the UUID of the specified filesystem and print it. Optionally it is
diff --git a/test/py/tests/test_fs/conftest.py b/test/py/tests/test_fs/conftest.py
index 43eeb4b..9324657 100644
--- a/test/py/tests/test_fs/conftest.py
+++ b/test/py/tests/test_fs/conftest.py
@@ -13,6 +13,7 @@
 supported_fs_ext = ['fat16', 'fat32']
 supported_fs_mkdir = ['fat16', 'fat32']
 supported_fs_unlink = ['fat16', 'fat32']
+supported_fs_symlink = ['ext4']
 
 #
 # Filesystem test specific setup
@@ -48,6 +49,7 @@
     global supported_fs_ext
     global supported_fs_mkdir
     global supported_fs_unlink
+    global supported_fs_symlink
 
     def intersect(listA, listB):
         return  [x for x in listA if x in listB]
@@ -59,6 +61,7 @@
         supported_fs_ext =  intersect(supported_fs, supported_fs_ext)
         supported_fs_mkdir =  intersect(supported_fs, supported_fs_mkdir)
         supported_fs_unlink =  intersect(supported_fs, supported_fs_unlink)
+        supported_fs_symlink =  intersect(supported_fs, supported_fs_symlink)
 
 def pytest_generate_tests(metafunc):
     """Parametrize fixtures, fs_obj_xxx
@@ -84,6 +87,9 @@
     if 'fs_obj_unlink' in metafunc.fixturenames:
         metafunc.parametrize('fs_obj_unlink', supported_fs_unlink,
             indirect=True, scope='module')
+    if 'fs_obj_symlink' in metafunc.fixturenames:
+        metafunc.parametrize('fs_obj_symlink', supported_fs_symlink,
+            indirect=True, scope='module')
 
 #
 # Helper functions
@@ -143,6 +149,8 @@
         mkfs_opt = '-F 16'
     elif fs_type == 'fat32':
         mkfs_opt = '-F 32'
+    elif fs_type == 'ext4':
+        mkfs_opt = '-O ^metadata_csum'
     else:
         mkfs_opt = ''
 
@@ -523,3 +531,72 @@
         call('rmdir %s' % mount_dir, shell=True)
         if fs_img:
             call('rm -f %s' % fs_img, shell=True)
+
+#
+# Fixture for symlink fs test
+#
+# NOTE: yield_fixture was deprecated since pytest-3.0
+@pytest.yield_fixture()
+def fs_obj_symlink(request, u_boot_config):
+    """Set up a file system to be used in symlink fs test.
+
+    Args:
+        request: Pytest request object.
+        u_boot_config: U-boot configuration.
+
+    Return:
+        A fixture for basic fs test, i.e. a triplet of file system type,
+        volume file name and  a list of MD5 hashes.
+    """
+    fs_type = request.param
+    fs_img = ''
+
+    fs_ubtype = fstype_to_ubname(fs_type)
+    check_ubconfig(u_boot_config, fs_ubtype)
+
+    mount_dir = u_boot_config.persistent_data_dir + '/mnt'
+
+    small_file = mount_dir + '/' + SMALL_FILE
+    medium_file = mount_dir + '/' + MEDIUM_FILE
+
+    try:
+
+        # 3GiB volume
+        fs_img = mk_fs(u_boot_config, fs_type, 0x40000000, '1GB')
+
+        # Mount the image so we can populate it.
+        check_call('mkdir -p %s' % mount_dir, shell=True)
+        mount_fs(fs_type, fs_img, mount_dir)
+
+        # Create a subdirectory.
+        check_call('mkdir %s/SUBDIR' % mount_dir, shell=True)
+
+        # Create a small file in this image.
+        check_call('dd if=/dev/urandom of=%s bs=1M count=1'
+                   % small_file, shell=True)
+
+        # Create a medium file in this image.
+        check_call('dd if=/dev/urandom of=%s bs=10M count=1'
+                   % medium_file, shell=True)
+
+        # Generate the md5sums of reads that we will test against small file
+        out = check_output(
+            'dd if=%s bs=1M skip=0 count=1 2> /dev/null | md5sum'
+            % small_file, shell=True)
+        md5val = [out.split()[0]]
+        out = check_output(
+            'dd if=%s bs=10M skip=0 count=1 2> /dev/null | md5sum'
+            % medium_file, shell=True)
+        md5val.extend([out.split()[0]])
+
+        umount_fs(mount_dir)
+    except CalledProcessError:
+        pytest.skip('Setup failed for filesystem: ' + fs_type)
+        return
+    else:
+        yield [fs_ubtype, fs_img, md5val]
+    finally:
+        umount_fs(mount_dir)
+        call('rmdir %s' % mount_dir, shell=True)
+        if fs_img:
+            call('rm -f %s' % fs_img, shell=True)
diff --git a/test/py/tests/test_fs/fstest_defs.py b/test/py/tests/test_fs/fstest_defs.py
index 5f10756..35b2bb6 100644
--- a/test/py/tests/test_fs/fstest_defs.py
+++ b/test/py/tests/test_fs/fstest_defs.py
@@ -6,6 +6,9 @@
 # $SMALL_FILE is the name of the 1MB file in the file system image
 SMALL_FILE='1MB.file'
 
+# $MEDIUM_FILE is the name of the 10MB file in the file system image
+MEDIUM_FILE='10MB.file'
+
 # $BIG_FILE is the name of the 2.5GB file in the file system image
 BIG_FILE='2.5GB.file'
 
diff --git a/test/py/tests/test_fs/fstest_helpers.py b/test/py/tests/test_fs/fstest_helpers.py
new file mode 100644
index 0000000..faec298
--- /dev/null
+++ b/test/py/tests/test_fs/fstest_helpers.py
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier:      GPL-2.0+
+# Copyright (c) 2019, Texas Instrument
+# Author: JJ Hiblot <jjhiblot@ti.com>
+#
+
+from subprocess import check_call, CalledProcessError
+
+def assert_fs_integrity(fs_type, fs_img):
+    try:
+        if fs_type == 'ext4':
+            check_call('fsck.ext4 -n -f %s' % fs_img, shell=True)
+    except CalledProcessError:
+        raise
diff --git a/test/py/tests/test_fs/test_basic.py b/test/py/tests/test_fs/test_basic.py
index 140ca29..71f3e86 100644
--- a/test/py/tests/test_fs/test_basic.py
+++ b/test/py/tests/test_fs/test_basic.py
@@ -11,6 +11,7 @@
 import pytest
 import re
 from fstest_defs import *
+from fstest_helpers import assert_fs_integrity
 
 @pytest.mark.boardspec('sandbox')
 @pytest.mark.slow
@@ -237,6 +238,7 @@
                 'md5sum %x $filesize' % ADDR,
                 'setenv filesize'])
             assert(md5val[0] in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_fs12(self, u_boot_console, fs_obj_basic):
         """
@@ -252,6 +254,7 @@
                 'host bind 0 %s' % fs_img,
                 '%swrite host 0:0 %x /. 0x10' % (fs_type, ADDR)])
             assert('Unable to write' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_fs13(self, u_boot_console, fs_obj_basic):
         """
@@ -286,3 +289,4 @@
                 'md5sum %x $filesize' % ADDR,
                 'setenv filesize'])
             assert(md5val[0] in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
diff --git a/test/py/tests/test_fs/test_ext.py b/test/py/tests/test_fs/test_ext.py
index 06cad55..2c47738 100644
--- a/test/py/tests/test_fs/test_ext.py
+++ b/test/py/tests/test_fs/test_ext.py
@@ -11,6 +11,7 @@
 import pytest
 import re
 from fstest_defs import *
+from fstest_helpers import assert_fs_integrity
 
 @pytest.mark.boardspec('sandbox')
 @pytest.mark.slow
@@ -36,6 +37,7 @@
                 'md5sum %x $filesize' % ADDR,
                 'setenv filesize'])
             assert(md5val[0] in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_fs_ext2(self, u_boot_console, fs_obj_ext):
         """
@@ -58,6 +60,7 @@
                 'md5sum %x $filesize' % ADDR,
                 'setenv filesize'])
             assert(md5val[0] in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_fs_ext3(self, u_boot_console, fs_obj_ext):
         """
@@ -72,6 +75,7 @@
                 '%swrite host 0:0 %x /dir1/none/%s.w3 $filesize'
                     % (fs_type, ADDR, MIN_FILE)])
             assert('Unable to write "/dir1/none/' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_fs_ext4(self, u_boot_console, fs_obj_ext):
         """
@@ -104,6 +108,7 @@
                 'md5sum %x $filesize' % ADDR,
                 'setenv filesize'])
             assert(md5val[1] in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_fs_ext5(self, u_boot_console, fs_obj_ext):
         """
@@ -136,6 +141,7 @@
                 'md5sum %x $filesize' % ADDR,
                 'setenv filesize'])
             assert(md5val[2] in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_fs_ext6(self, u_boot_console, fs_obj_ext):
         """
@@ -160,6 +166,7 @@
                 'printenv filesize',
                 'setenv filesize'])
             assert('filesize=0' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_fs_ext7(self, u_boot_console, fs_obj_ext):
         """
@@ -192,6 +199,7 @@
                 'md5sum %x $filesize' % ADDR,
                 'setenv filesize'])
             assert(md5val[3] in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_fs_ext8(self, u_boot_console, fs_obj_ext):
         """
@@ -209,6 +217,7 @@
                 '%swrite host 0:0 %x /dir1/%s.w8 0x1400 %x'
                     % (fs_type, ADDR, MIN_FILE, 0x100000 + 0x1400))
             assert('Unable to write "/dir1' in output)
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_fs_ext9(self, u_boot_console, fs_obj_ext):
         """
@@ -223,3 +232,4 @@
                 '%swrite host 0:0 %x /dir1/%s.w9 0x1400 0x1400'
                     % (fs_type, ADDR, MIN_FILE)])
             assert('Unable to write "/dir1' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
diff --git a/test/py/tests/test_fs/test_mkdir.py b/test/py/tests/test_fs/test_mkdir.py
index b3fe11c..fa9561e 100644
--- a/test/py/tests/test_fs/test_mkdir.py
+++ b/test/py/tests/test_fs/test_mkdir.py
@@ -9,6 +9,7 @@
 """
 
 import pytest
+from fstest_helpers import assert_fs_integrity
 
 @pytest.mark.boardspec('sandbox')
 @pytest.mark.slow
@@ -29,6 +30,8 @@
                 '%sls host 0:0 dir1' % fs_type)
             assert('./'   in output)
             assert('../'  in output)
+            assert_fs_integrity(fs_type, fs_img)
+
 
     def test_mkdir2(self, u_boot_console, fs_obj_mkdir):
         """
@@ -46,6 +49,7 @@
                 '%sls host 0:0 dir1/dir2' % fs_type)
             assert('./'   in output)
             assert('../'  in output)
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_mkdir3(self, u_boot_console, fs_obj_mkdir):
         """
@@ -58,6 +62,7 @@
                 'host bind 0 %s' % fs_img,
                 '%smkdir host 0:0 none/dir3' % fs_type])
             assert('Unable to create a directory' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_mkdir4(self, u_boot_console, fs_obj_mkdir):
         """
@@ -69,6 +74,7 @@
                 'host bind 0 %s' % fs_img,
                 '%smkdir host 0:0 .' % fs_type])
             assert('Unable to create a directory' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_mkdir5(self, u_boot_console, fs_obj_mkdir):
         """
@@ -80,6 +86,7 @@
                 'host bind 0 %s' % fs_img,
                 '%smkdir host 0:0 ..' % fs_type])
             assert('Unable to create a directory' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_mkdir6(self, u_boot_console, fs_obj_mkdir):
         """
@@ -111,3 +118,4 @@
                 '%sls host 0:0 dir6/0123456789abcdef13/..' % fs_type)
             assert('0123456789abcdef00/'  in output)
             assert('0123456789abcdef13/'  in output)
+            assert_fs_integrity(fs_type, fs_img)
diff --git a/test/py/tests/test_fs/test_symlink.py b/test/py/tests/test_fs/test_symlink.py
new file mode 100644
index 0000000..9ced101
--- /dev/null
+++ b/test/py/tests/test_fs/test_symlink.py
@@ -0,0 +1,130 @@
+# SPDX-License-Identifier:      GPL-2.0+
+# Copyright (c) 2019, Texas Instrument
+# Author: Jean-Jacques Hiblot <jjhiblot@ti.com>
+#
+# U-Boot File System:symlink Test
+
+"""
+This test verifies unlink operation (deleting a file or a directory)
+on file system.
+"""
+
+import pytest
+import re
+from fstest_defs import *
+from fstest_helpers import assert_fs_integrity
+
+
+@pytest.mark.boardspec('sandbox')
+@pytest.mark.slow
+class TestSymlink(object):
+    def test_symlink1(self, u_boot_console, fs_obj_symlink):
+        """
+        Test Case 1 - create a link. and follow it when reading
+        """
+        fs_type, fs_img, md5val = fs_obj_symlink
+        with u_boot_console.log.section('Test Case 1 - create link and read'):
+            output = u_boot_console.run_command_list([
+                'host bind 0 %s' % fs_img,
+                'setenv filesize',
+                'ln host 0:0 %s /%s.link ' % (SMALL_FILE, SMALL_FILE),
+            ])
+            assert('' in ''.join(output))
+
+            output = u_boot_console.run_command_list([
+                '%sload host 0:0 %x /%s.link' % (fs_type, ADDR, SMALL_FILE),
+                'printenv filesize'])
+            assert('filesize=100000' in ''.join(output))
+
+            # Test Case 4b - Read full 1MB of small file
+            output = u_boot_console.run_command_list([
+                'md5sum %x $filesize' % ADDR,
+                'setenv filesize'])
+            assert(md5val[0] in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
+
+    def test_symlink2(self, u_boot_console, fs_obj_symlink):
+        """
+        Test Case 2 - create chained links
+        """
+        fs_type, fs_img, md5val = fs_obj_symlink
+        with u_boot_console.log.section('Test Case 2 - create chained links'):
+            output = u_boot_console.run_command_list([
+                'host bind 0 %s' % fs_img,
+                'setenv filesize',
+                'ln host 0:0 %s /%s.link1 ' % (SMALL_FILE, SMALL_FILE),
+                'ln host 0:0 /%s.link1 /SUBDIR/%s.link2' % (
+                    SMALL_FILE, SMALL_FILE),
+                'ln host 0:0 SUBDIR/%s.link2 /%s.link3' % (
+                    SMALL_FILE, SMALL_FILE),
+            ])
+            assert('' in ''.join(output))
+
+            output = u_boot_console.run_command_list([
+                '%sload host 0:0 %x /%s.link3' % (fs_type, ADDR, SMALL_FILE),
+                'printenv filesize'])
+            assert('filesize=100000' in ''.join(output))
+
+            # Test Case 4b - Read full 1MB of small file
+            output = u_boot_console.run_command_list([
+                'md5sum %x $filesize' % ADDR,
+                'setenv filesize'])
+            assert(md5val[0] in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
+
+    def test_symlink3(self, u_boot_console, fs_obj_symlink):
+        """
+        Test Case 3 - replace file/link with link
+        """
+        fs_type, fs_img, md5val = fs_obj_symlink
+        with u_boot_console.log.section('Test Case 1 - create link and read'):
+            output = u_boot_console.run_command_list([
+                'host bind 0 %s' % fs_img,
+                'setenv filesize',
+                'ln host 0:0 %s /%s ' % (MEDIUM_FILE, SMALL_FILE),
+                'ln host 0:0 %s /%s.link ' % (MEDIUM_FILE, MEDIUM_FILE),
+            ])
+            assert('' in ''.join(output))
+
+            output = u_boot_console.run_command_list([
+                '%sload host 0:0 %x /%s' % (fs_type, ADDR, SMALL_FILE),
+                'printenv filesize'])
+            assert('filesize=a00000' in ''.join(output))
+
+            output = u_boot_console.run_command_list([
+                'md5sum %x $filesize' % ADDR,
+                'setenv filesize'])
+            assert(md5val[1] in ''.join(output))
+
+            output = u_boot_console.run_command_list([
+                'ln host 0:0 %s.link /%s ' % (MEDIUM_FILE, SMALL_FILE),
+                '%sload host 0:0 %x /%s' % (fs_type, ADDR, SMALL_FILE),
+                'printenv filesize'])
+            assert('filesize=a00000' in ''.join(output))
+
+            output = u_boot_console.run_command_list([
+                'md5sum %x $filesize' % ADDR,
+                'setenv filesize'])
+            assert(md5val[1] in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
+
+    def test_symlink4(self, u_boot_console, fs_obj_symlink):
+        """
+        Test Case 4 - create a broken link
+        """
+        fs_type, fs_img, md5val = fs_obj_symlink
+        with u_boot_console.log.section('Test Case 1 - create link and read'):
+
+            output = u_boot_console.run_command_list([
+                'setenv filesize',
+                'ln host 0:0 nowhere /link ',
+            ])
+            assert('' in ''.join(output))
+
+            output = u_boot_console.run_command(
+                '%sload host 0:0 %x /link' %
+                (fs_type, ADDR))
+            with u_boot_console.disable_check('error_notification'):
+                output = u_boot_console.run_command('printenv filesize')
+            assert('"filesize" not defined' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
diff --git a/test/py/tests/test_fs/test_unlink.py b/test/py/tests/test_fs/test_unlink.py
index 2b81746..97aafc6 100644
--- a/test/py/tests/test_fs/test_unlink.py
+++ b/test/py/tests/test_fs/test_unlink.py
@@ -10,6 +10,7 @@
 """
 
 import pytest
+from fstest_helpers import assert_fs_integrity
 
 @pytest.mark.boardspec('sandbox')
 @pytest.mark.slow
@@ -30,6 +31,7 @@
                 '%sls host 0:0 dir1/' % fs_type)
             assert(not 'file1' in output)
             assert('file2' in output)
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_unlink2(self, u_boot_console, fs_obj_unlink):
         """
@@ -48,6 +50,7 @@
             output = u_boot_console.run_command(
                 '%sls host 0:0 dir2' % fs_type)
             assert('0 file(s), 2 dir(s)' in output)
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_unlink3(self, u_boot_console, fs_obj_unlink):
         """
@@ -59,6 +62,7 @@
                 'host bind 0 %s' % fs_img,
                 '%srm host 0:0 dir1/nofile' % fs_type])
             assert('nofile: doesn\'t exist' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_unlink4(self, u_boot_console, fs_obj_unlink):
         """
@@ -71,9 +75,10 @@
                 '%srm host 0:0 dir4' % fs_type])
             assert('' == ''.join(output))
 
-        output = u_boot_console.run_command(
-            '%sls host 0:0 /' % fs_type)
-        assert(not 'dir4' in output)
+            output = u_boot_console.run_command(
+                '%sls host 0:0 /' % fs_type)
+            assert(not 'dir4' in output)
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_unlink5(self, u_boot_console, fs_obj_unlink):
         """
@@ -86,6 +91,7 @@
                 'host bind 0 %s' % fs_img,
                 '%srm host 0:0 dir5' % fs_type])
             assert('directory is not empty' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_unlink6(self, u_boot_console, fs_obj_unlink):
         """
@@ -97,6 +103,7 @@
                 'host bind 0 %s' % fs_img,
                 '%srm host 0:0 dir5/.' % fs_type])
             assert('directory is not empty' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
 
     def test_unlink7(self, u_boot_console, fs_obj_unlink):
         """
@@ -108,3 +115,4 @@
                 'host bind 0 %s' % fs_img,
                 '%srm host 0:0 dir5/..' % fs_type])
             assert('directory is not empty' in ''.join(output))
+            assert_fs_integrity(fs_type, fs_img)
diff --git a/test/py/tests/test_mmc_rd.py b/test/py/tests/test_mmc_rd.py
index a13bc0a..2dc715b 100644
--- a/test/py/tests/test_mmc_rd.py
+++ b/test/py/tests/test_mmc_rd.py
@@ -6,6 +6,7 @@
 # read if the test configuration contains a CRC of the expected data.
 
 import pytest
+import time
 import u_boot_utils
 
 """
@@ -57,6 +58,116 @@
 )
 """
 
+def mmc_dev(u_boot_console, is_emmc, devid, partid):
+    """Run the "mmc dev" command.
+
+    Args:
+        u_boot_console: A U-Boot console connection.
+        is_emmc: Whether the device is eMMC
+        devid: Device ID
+        partid: Partition ID
+
+    Returns:
+        Nothing.
+    """
+
+    # Select MMC device
+    cmd = 'mmc dev %d' % devid
+    if is_emmc:
+        cmd += ' %d' % partid
+    response = u_boot_console.run_command(cmd)
+    assert 'no card present' not in response
+    if is_emmc:
+        partid_response = '(part %d)' % partid
+    else:
+        partid_response = ''
+    good_response = 'mmc%d%s is current device' % (devid, partid_response)
+    assert good_response in response
+
+@pytest.mark.buildconfigspec('cmd_mmc')
+def test_mmc_dev(u_boot_console, env__mmc_rd_config):
+    """Test the "mmc dev" command.
+
+    Args:
+        u_boot_console: A U-Boot console connection.
+        env__mmc_rd_config: The single MMC configuration on which
+            to run the test. See the file-level comment above for details
+            of the format.
+
+    Returns:
+        Nothing.
+    """
+
+    is_emmc = env__mmc_rd_config['is_emmc']
+    devid = env__mmc_rd_config['devid']
+    partid = env__mmc_rd_config.get('partid', 0)
+
+    # Select MMC device
+    mmc_dev(u_boot_console, is_emmc, devid, partid)
+
+@pytest.mark.buildconfigspec('cmd_mmc')
+def test_mmc_rescan(u_boot_console, env__mmc_rd_config):
+    """Test the "mmc rescan" command.
+
+    Args:
+        u_boot_console: A U-Boot console connection.
+        env__mmc_rd_config: The single MMC configuration on which
+            to run the test. See the file-level comment above for details
+            of the format.
+
+    Returns:
+        Nothing.
+    """
+
+    is_emmc = env__mmc_rd_config['is_emmc']
+    devid = env__mmc_rd_config['devid']
+    partid = env__mmc_rd_config.get('partid', 0)
+
+    # Select MMC device
+    mmc_dev(u_boot_console, is_emmc, devid, partid)
+
+    # Rescan MMC device
+    cmd = 'mmc rescan'
+    response = u_boot_console.run_command(cmd)
+    assert 'no card present' not in response
+
+@pytest.mark.buildconfigspec('cmd_mmc')
+def test_mmc_info(u_boot_console, env__mmc_rd_config):
+    """Test the "mmc info" command.
+
+    Args:
+        u_boot_console: A U-Boot console connection.
+        env__mmc_rd_config: The single MMC configuration on which
+            to run the test. See the file-level comment above for details
+            of the format.
+
+    Returns:
+        Nothing.
+    """
+
+    is_emmc = env__mmc_rd_config['is_emmc']
+    devid = env__mmc_rd_config['devid']
+    partid = env__mmc_rd_config.get('partid', 0)
+    info_device = env__mmc_rd_config['info_device']
+    info_speed = env__mmc_rd_config['info_speed']
+    info_mode = env__mmc_rd_config['info_mode']
+    info_buswidth = env__mmc_rd_config['info_buswidth']
+
+    # Select MMC device
+    mmc_dev(u_boot_console, is_emmc, devid, partid)
+
+    # Read MMC device information
+    cmd = 'mmc info'
+    response = u_boot_console.run_command(cmd)
+    good_response = "Device: %s" % info_device
+    assert good_response in response
+    good_response = "Bus Speed: %s" % info_speed
+    assert good_response in response
+    good_response = "Mode : %s" % info_mode
+    assert good_response in response
+    good_response = "Bus Width: %s" % info_buswidth
+    assert good_response in response
+
 @pytest.mark.buildconfigspec('cmd_mmc')
 def test_mmc_rd(u_boot_console, env__mmc_rd_config):
     """Test the "mmc read" command.
@@ -77,6 +188,7 @@
     sector = env__mmc_rd_config.get('sector', 0)
     count_sectors = env__mmc_rd_config.get('count', 1)
     expected_crc32 = env__mmc_rd_config.get('crc32', None)
+    read_duration_max = env__mmc_rd_config.get('read_duration_max', 0)
 
     count_bytes = count_sectors * 512
     bcfg = u_boot_console.config.buildconfig
@@ -86,17 +198,7 @@
     addr = '0x%08x' % ram_base
 
     # Select MMC device
-    cmd = 'mmc dev %d' % devid
-    if is_emmc:
-        cmd += ' %d' % partid
-    response = u_boot_console.run_command(cmd)
-    assert 'no card present' not in response
-    if is_emmc:
-        partid_response = '(part %d)' % partid
-    else:
-        partid_response = ''
-    good_response = 'mmc%d%s is current device' % (devid, partid_response)
-    assert good_response in response
+    mmc_dev(u_boot_console, is_emmc, devid, partid)
 
     # Clear target RAM
     if expected_crc32:
@@ -113,7 +215,9 @@
 
     # Read data
     cmd = 'mmc read %s %x %x' % (addr, sector, count_sectors)
+    tstart = time.time()
     response = u_boot_console.run_command(cmd)
+    tend = time.time()
     good_response = 'MMC read: dev # %d, block # %d, count %d ... %d blocks read: OK' % (
         devid, sector, count_sectors, count_sectors)
     assert good_response in response
@@ -126,3 +230,10 @@
             assert expected_crc32 in response
         else:
             u_boot_console.log.warning('CONFIG_CMD_CRC32 != y: Skipping check')
+
+    # Check if the command did not take too long
+    if read_duration_max:
+        elapsed = tend - tstart
+        u_boot_console.log.info('Reading %d bytes took %f seconds' %
+                                (count_bytes, elapsed))
+        assert elapsed <= (read_duration_max - 0.01)