/* -*- mode: c; c-basic-offset: 8; -*-
 * vim: noexpandtab sw=8 ts=8 sts=0:
 *
 * truncate.c
 *
 * Truncate an OCFS2 inode.  For the OCFS2 userspace library.
 *
 * Copyright (C) 2004 Oracle.  All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License, version 2,  as published by the Free Software Foundation.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 021110-1307, USA.
 */

#define _XOPEN_SOURCE 600 /* Triggers magic in features.h */
#define _LARGEFILE64_SOURCE

#include <string.h>
#if HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <assert.h>
#include <errno.h>
#include "ocfs2/ocfs2.h"

struct truncate_ctxt {
	uint64_t ino;
	uint64_t new_size_in_clusters;
	uint32_t new_i_clusters;
	errcode_t (*free_clusters)(ocfs2_filesys *fs,
				   uint32_t len,
				   uint64_t start_blkno,
				   void *free_data);
	void *free_data;
};

static int ocfs2_truncate_clusters(ocfs2_filesys *fs,
				   struct ocfs2_extent_rec *rec,
				   uint64_t ino,
				   uint32_t len,
				   uint64_t start)
{
	if (!ocfs2_refcount_tree(OCFS2_RAW_SB(fs->fs_super)) ||
	    !(rec->e_flags & OCFS2_EXT_REFCOUNTED))
		return ocfs2_free_clusters(fs, len, start);

	assert(ino);

	return ocfs2_decrease_refcount(fs, ino,
				ocfs2_blocks_to_clusters(fs, start),
				len, 1);
}

/*
 * Delete and free clusters if needed.  This only works with DEPTH_TRAVERSE.
 */
static int truncate_iterate(ocfs2_filesys *fs,
			    struct ocfs2_extent_rec *rec,
			    int tree_depth, uint32_t ccount,
			    uint64_t ref_blkno, int ref_recno,
			    void *priv_data)
{
	struct truncate_ctxt *ctxt = (struct truncate_ctxt *)priv_data;
	uint32_t len = 0, new_size_in_clusters = ctxt->new_size_in_clusters;
	uint64_t start = 0;
	errcode_t ret;
	int func_ret = OCFS2_EXTENT_ERROR;
	char *buf = NULL;
	struct ocfs2_extent_list *el = NULL;

	if ((rec->e_cpos + ocfs2_rec_clusters(tree_depth, rec)) <=
							new_size_in_clusters)
		return 0;

	if (rec->e_cpos >= new_size_in_clusters) {
		/* the rec is entirely outside the new size, free it */
		if (!tree_depth) {
			start = rec->e_blkno;
			len = ocfs2_rec_clusters(tree_depth, rec);
		} else {
			/* here we meet with a full empty extent block, delete
			 * it. The extent list it contains should already be
			 * iterated and all the clusters have been freed.
			 */
			ret = ocfs2_delete_extent_block(fs, rec->e_blkno);
			if (ret)
				goto bail;
		}

		memset(rec, 0, sizeof(struct ocfs2_extent_rec));
	} else {
		/* we're truncating into the middle of the rec */
		len = rec->e_cpos +
			 ocfs2_rec_clusters(tree_depth, rec);
		len -= new_size_in_clusters;
		if (!tree_depth) {
			ocfs2_set_rec_clusters(tree_depth, rec,
				 	new_size_in_clusters - rec->e_cpos);
			start = rec->e_blkno +
				ocfs2_clusters_to_blocks(fs,
						ocfs2_rec_clusters(tree_depth,
								   rec));
		} else {
			ocfs2_set_rec_clusters(tree_depth, rec,
					new_size_in_clusters - rec->e_cpos);
			/*
			 * For a sparse file, we may meet with another
			 * situation here:
			 * The start of the left most extent rec is greater
			 * than the new size we truncate the file to, but the
			 * start of the extent block is less than that size.
			 * In this case, actually all the extent records in
			 * this extent block have been removed. So we have
			 * to remove the extent block also.
			 * In this function, we have to reread the extent list
			 * to see whether the extent block is empty or not.
			 */
			ret = ocfs2_malloc_block(fs->fs_io, &buf);
			if (ret)
				goto bail;

			ret = ocfs2_read_extent_block(fs, rec->e_blkno, buf);
			if (ret)
				goto bail;

			el = &((struct ocfs2_extent_block *)buf)->h_list;
			if (el->l_next_free_rec == 0) {
				ret = ocfs2_delete_extent_block(fs, rec->e_blkno);
				if (ret)
					goto bail;
				memset(rec, 0, sizeof(struct ocfs2_extent_rec));
			}
		}
	}

	if (start) {
		if (ctxt->free_clusters)
			ret = ctxt->free_clusters(fs, len, start,
						  ctxt->free_data);
		else
			ret = ocfs2_truncate_clusters(fs, rec, ctxt->ino,
						      len, start);
		if (ret)
			goto bail;
		ctxt->new_i_clusters -= len;
	}

	func_ret =  OCFS2_EXTENT_CHANGED;
bail:
	if (buf)
		ocfs2_free(&buf);
	return func_ret;
}

/*
 * Zero the area past i_size but still within an allocated
 * cluster. This avoids exposing nonzero data on subsequent file
 * extends.
 */
static errcode_t ocfs2_zero_tail_for_truncate(ocfs2_cached_inode *ci,
					      uint64_t new_size)
{
	errcode_t ret;
	char *buf = NULL;
	ocfs2_filesys *fs = ci->ci_fs;
	uint64_t start_blk, p_blkno, contig_blocks, start_off;
	int count, byte_counts, bpc = fs->fs_clustersize /fs->fs_blocksize;
	uint16_t ext_flags;

	if (new_size == 0)
		return 0;

	start_blk = new_size / fs->fs_blocksize;

	ret = ocfs2_extent_map_get_blocks(ci, start_blk, 1,
					  &p_blkno, &contig_blocks, &ext_flags);
	if (ret)
		goto out;

	/* Tail is a hole. */
	if (!p_blkno)
		goto out;

	if (ext_flags & OCFS2_EXT_REFCOUNTED) {
		uint32_t cpos = ocfs2_blocks_to_clusters(fs, start_blk);
		ret = ocfs2_refcount_cow(ci, cpos, 1, cpos + 1);
		if (ret)
			goto out;

		ret = ocfs2_extent_map_get_blocks(ci, start_blk, 1,
						  &p_blkno, &contig_blocks,
						  &ext_flags);
		if (ret)
			goto out;

		assert(!(ext_flags & OCFS2_EXT_REFCOUNTED) && p_blkno);

	}

	/* calculate the total blocks we need to empty. */
	count = bpc - (p_blkno & (bpc - 1));
	ret = ocfs2_malloc_blocks(fs->fs_io, count, &buf);
	if (ret)
		goto out;

	ret = ocfs2_read_blocks(fs, p_blkno, count, buf);
	if (ret)
		goto out;

	/* empty the content after the new_size and within the same cluster. */
	start_off = new_size % fs->fs_blocksize;
	byte_counts = count * fs->fs_blocksize - start_off;
	memset(buf + start_off, 0, byte_counts);

	ret = io_write_block(fs->fs_io, p_blkno, count, buf);

out:
	if (buf)
		ocfs2_free(&buf);
	return ret;
}

/*
 * This function will truncate the file's cluster which exceeds
 * the cluster where new_size resides in and empty all the
 * bytes in the same cluster which exceeds new_size.
 */
static errcode_t ocfs2_zero_tail_and_truncate_full(ocfs2_filesys *fs,
						   ocfs2_cached_inode *ci,
						   uint64_t new_i_size,
						   uint32_t *new_clusters,
			      errcode_t (*free_clusters)(ocfs2_filesys *fs,
							 uint32_t len,
							 uint64_t start,
							 void *free_data),
						   void *free_data)
{
	errcode_t ret;
	uint64_t new_size_in_blocks;
	struct truncate_ctxt ctxt;

	new_size_in_blocks = ocfs2_blocks_in_bytes(fs, new_i_size);
	ctxt.ino = ci->ci_blkno;
	ctxt.new_i_clusters = ci->ci_inode->i_clusters;
	ctxt.new_size_in_clusters =
			ocfs2_clusters_in_blocks(fs, new_size_in_blocks);
	ctxt.free_clusters = free_clusters;
	ctxt.free_data = free_data;

	ret = ocfs2_extent_iterate_inode(fs, ci->ci_inode,
					 OCFS2_EXTENT_FLAG_DEPTH_TRAVERSE,
					 NULL, truncate_iterate,
					 &ctxt);
	if (ret)
		goto out;

	ret = ocfs2_zero_tail_for_truncate(ci, new_i_size);
	if (ret)
		goto out;

	if (new_clusters)
		*new_clusters = ctxt.new_i_clusters;
out:
	return ret;
}

errcode_t ocfs2_zero_tail_and_truncate(ocfs2_filesys *fs,
				       ocfs2_cached_inode *ci,
				       uint64_t new_i_size,
				       uint32_t *new_clusters)
{
	return ocfs2_zero_tail_and_truncate_full(fs, ci, new_i_size,
						 new_clusters, NULL, NULL);
}

/*
 * NOTE: ocfs2_truncate_inline() also handles fast symlink,
 * since truncating for inline file and fasy symlink are
 * almost the same thing per se.
 */
errcode_t ocfs2_truncate_inline(ocfs2_filesys *fs, uint64_t ino,
				uint64_t new_i_size)
{
	errcode_t ret = 0;
	char *buf = NULL;
	struct ocfs2_dinode *di = NULL;
	struct ocfs2_inline_data *idata = NULL;

	if (!(fs->fs_flags & OCFS2_FLAG_RW))
		return OCFS2_ET_RO_FILESYS;

	ret = ocfs2_malloc_block(fs->fs_io, &buf);
	if (ret)
		return ret;

	ret = ocfs2_read_inode(fs, ino, buf);
	if (ret)
		goto out_free_buf;

	di = (struct ocfs2_dinode *)buf;
	if (di->i_size < new_i_size) {
		ret = EINVAL;
		goto out_free_buf;
	}

	idata = &di->id2.i_data;

	if (!(di->i_dyn_features & OCFS2_INLINE_DATA_FL) &&
	    !(S_ISLNK(di->i_mode) && !di->i_clusters)) {
		ret = EINVAL;
		goto out_free_buf;
	}

	if (di->i_dyn_features & OCFS2_INLINE_DATA_FL)
		memset(idata->id_data + new_i_size, 0, di->i_size - new_i_size);
	else
		memset(di->id2.i_symlink + new_i_size, 0,
		       di->i_size - new_i_size);

	di->i_size = new_i_size;

	ret = ocfs2_write_inode(fs, ino, buf);

out_free_buf:
	if (buf)
		ocfs2_free(&buf);
	return ret;
}

/* XXX care about zeroing new clusters and final partially truncated 
 * clusters */
errcode_t ocfs2_truncate_full(ocfs2_filesys *fs, uint64_t ino,
			      uint64_t new_i_size,
			      errcode_t (*free_clusters)(ocfs2_filesys *fs,
							 uint32_t len,
							 uint64_t start,
							 void *free_data),
			      void *free_data)
{
	errcode_t ret;
	uint32_t new_clusters;
	ocfs2_cached_inode *ci = NULL;

	ret = ocfs2_read_cached_inode(fs, ino, &ci);
	if (ret)
		goto out;

	if (ci->ci_inode->i_size == new_i_size)
		goto out;

	if (ci->ci_inode->i_size < new_i_size) {
		ret = ocfs2_extend_file(fs, ino, new_i_size);
		goto out;
	}

	if ((S_ISLNK(ci->ci_inode->i_mode) && !ci->ci_inode->i_clusters) ||
	    (ci->ci_inode->i_dyn_features & OCFS2_INLINE_DATA_FL))
		ret = ocfs2_truncate_inline(fs, ino, new_i_size);
	else {
		ret = ocfs2_zero_tail_and_truncate_full(fs, ci, new_i_size,
							&new_clusters,
							free_clusters,
							free_data);
		if (ret)
			goto out;

		ci->ci_inode->i_clusters = new_clusters;

		/* now all the clusters and extent blocks are freed.
		 * only when the file's content is empty, should the tree depth
		 * change.
		 */
		if (new_clusters == 0)
			ci->ci_inode->id2.i_list.l_tree_depth = 0;

		ci->ci_inode->i_size = new_i_size;
		ret = ocfs2_write_cached_inode(fs, ci);
	}
out:
	if (ci)
		ocfs2_free_cached_inode(fs, ci);
	return ret;
}

errcode_t ocfs2_truncate(ocfs2_filesys *fs, uint64_t ino, uint64_t new_i_size)
{
	return ocfs2_truncate_full(fs, ino, new_i_size, NULL, NULL);
}

errcode_t ocfs2_xattr_value_truncate(ocfs2_filesys *fs, uint64_t ino,
				     struct ocfs2_xattr_value_root *xv)
{
	struct truncate_ctxt ctxt;
	int changed;
	struct ocfs2_extent_list *el = &xv->xr_list;

	ctxt.ino = ino;
	ctxt.new_i_clusters = xv->xr_clusters;
	ctxt.new_size_in_clusters = 0;

	return ocfs2_extent_iterate_xattr(fs, el, xv->xr_last_eb_blk,
					OCFS2_EXTENT_FLAG_DEPTH_TRAVERSE,
					truncate_iterate,
					&ctxt, &changed);
}

errcode_t ocfs2_xattr_tree_truncate(ocfs2_filesys *fs,
				    struct ocfs2_xattr_tree_root *xt)
{
	struct truncate_ctxt ctxt;
	int changed;
	struct ocfs2_extent_list *el = &xt->xt_list;

	/*
	 * ino is used to find refcount tree, as we never use refcount
	 * in xattr tree, so set it to 0.
	 */
	ctxt.ino = 0;
	ctxt.new_i_clusters = xt->xt_clusters;
	ctxt.new_size_in_clusters = 0;

	return ocfs2_extent_iterate_xattr(fs, el, xt->xt_last_eb_blk,
					OCFS2_EXTENT_FLAG_DEPTH_TRAVERSE,
					truncate_iterate,
					&ctxt, &changed);
}


errcode_t ocfs2_dir_indexed_tree_truncate(ocfs2_filesys *fs,
					struct ocfs2_dx_root_block *dx_root)
{
	struct truncate_ctxt ctxt;

	memset(&ctxt, 0, sizeof (struct truncate_ctxt));
	ctxt.new_i_clusters = dx_root->dr_clusters;
	ctxt.new_size_in_clusters = 0;

	return ocfs2_extent_iterate_dx_root(fs, dx_root,
					OCFS2_EXTENT_FLAG_DEPTH_TRAVERSE,
					NULL, truncate_iterate,	&ctxt);
}


#ifdef DEBUG_EXE
#include <stdlib.h>
#include <getopt.h>
#include <inttypes.h>

static uint64_t read_number(const char *num)
{
	uint64_t val;
	char *ptr;

	val = strtoull(num, &ptr, 0);
	if (!ptr || *ptr)
		return 0;

	return val;
}

static void print_usage(void)
{
	fprintf(stderr,
		"Usage: debug_truncate -i <ino_blkno> -s <new_size> device\n");
}

extern int opterr, optind;
extern char *optarg;

int main(int argc, char *argv[])
{
	errcode_t ret;
	int c;
	uint64_t blkno = 0, new_size = 0;
	ocfs2_filesys *fs;
	char *device;

	initialize_ocfs_error_table();

	while ((c = getopt(argc, argv, "i:s:")) != EOF) {
		switch (c) {
			case 'i':
				blkno = read_number(optarg);
				if (blkno <= OCFS2_SUPER_BLOCK_BLKNO) {
					fprintf(stderr,
						"Invalid inode block: %s\n",
						optarg);
					print_usage();
					return 1;
				}
				break;

			case 's':
				new_size = read_number(optarg);
				break;

			default:
				print_usage();
				return 1;
				break;
		}
	}

	if (optind >= argc) {
		fprintf(stderr, "Missing device name\n");
		print_usage();
		return 1;
	}
	device = argv[optind];

	if (!blkno || !new_size) {
		print_usage();
		return 1;
	}

	ret = ocfs2_open(device, OCFS2_FLAG_RW, 0, 0, &fs);
	if (ret) {
		com_err(argv[0], ret,
			"while opening file \"%s\"", device);
		return ret;
	}

	ret = ocfs2_truncate(fs, blkno, new_size);
	if (ret)
		com_err(argv[0], ret, "while truncating inode %"PRIu64, blkno);

	ocfs2_close(fs);

	return ret;
}
#endif  /* DEBUG_EXE */
