aboutsummaryrefslogtreecommitdiff
path: root/ext2fs/truncate.c
diff options
context:
space:
mode:
Diffstat (limited to 'ext2fs/truncate.c')
-rw-r--r--ext2fs/truncate.c365
1 files changed, 365 insertions, 0 deletions
diff --git a/ext2fs/truncate.c b/ext2fs/truncate.c
new file mode 100644
index 00000000..336981cc
--- /dev/null
+++ b/ext2fs/truncate.c
@@ -0,0 +1,365 @@
+/* File truncation
+
+ Copyright (C) 1995, 1996, 1997, 1999 Free Software Foundation, Inc.
+
+ Written by Miles Bader <miles@gnu.ai.mit.edu>
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2, or (at
+ your option) any later version.
+
+ 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., 675 Mass Ave, Cambridge, MA 02139, USA. */
+
+#include "ext2fs.h"
+
+#ifdef DONT_CACHE_MEMORY_OBJECTS
+#define MAY_CACHE 0
+#else
+#define MAY_CACHE 1
+#endif
+
+/* ---------------------------------------------------------------- */
+
+/* A sequence of blocks to be freed in NODE. */
+struct free_block_run
+{
+ block_t first_block;
+ unsigned long num_blocks;
+ struct node *node;
+};
+
+/* Initialize FBR, pointing to NODE. */
+static inline void
+free_block_run_init (struct free_block_run *fbr, struct node *node)
+{
+ fbr->num_blocks = 0;
+ fbr->node = node;
+}
+
+static inline void
+_free_block_run_flush (struct free_block_run *fbr, unsigned long count)
+{
+ fbr->node->dn_stat.st_blocks -= count << log2_stat_blocks_per_fs_block;
+ fbr->node->dn_stat_dirty = 1;
+ ext2_free_blocks (fbr->first_block, count);
+}
+
+/* Add BLOCK to the list of blocks to be freed in FBR. */
+static inline void
+free_block_run_add (struct free_block_run *fbr, block_t block)
+{
+ unsigned long count = fbr->num_blocks;
+ if (count == 0)
+ {
+ fbr->first_block = block;
+ fbr->num_blocks++;
+ }
+ else if (count > 0 && fbr->first_block == block - count)
+ fbr->num_blocks++;
+ else
+ {
+ _free_block_run_flush (fbr, count);
+ fbr->first_block = block;
+ fbr->num_blocks = 1;
+ }
+}
+
+/* If *P is non-zero, set it to zero, and add the block it pointed to the
+ list of blocks to be freed in FBR. */
+static inline void
+free_block_run_free_ptr (struct free_block_run *fbr, block_t *p)
+{
+ block_t block = *p;
+ if (block)
+ {
+ *p = 0;
+ free_block_run_add (fbr, block);
+ }
+}
+
+/* Free any blocks left in FBR, and cleanup any resources it's using. */
+static inline void
+free_block_run_finish (struct free_block_run *fbr)
+{
+ unsigned long count = fbr->num_blocks;
+ if (count > 0)
+ _free_block_run_flush (fbr, count);
+}
+
+/* ---------------------------------------------------------------- */
+
+/* Free any direct blocks starting with block END. */
+static void
+trunc_direct (struct node *node, block_t end, struct free_block_run *fbr)
+{
+ block_t *blocks = node->dn->info.i_data;
+
+ ext2_debug ("truncating direct blocks from %ld", end);
+
+ while (end < EXT2_NDIR_BLOCKS)
+ free_block_run_free_ptr (fbr, blocks + end++);
+}
+
+/* Free any blocks in NODE greater than or equal to END that are rooted in
+ the indirect block *P; OFFSET should be the block position that *P
+ corresponds to. For each block pointer in *P that should be freed,
+ FREE_BLOCK is called with a pointer to the entry for that block, and the
+ index of the entry within *P. If every block in *P is freed, then *P is
+ set to 0, otherwise it is left alone. */
+static void
+trunc_indirect (struct node *node, block_t end,
+ block_t *p, block_t offset,
+ void (*free_block)(block_t *p, unsigned index),
+ struct free_block_run *fbr)
+{
+ if (*p)
+ {
+ unsigned index;
+ int modified = 0, all_freed = 1;
+ block_t *ind_bh = (block_t *)bptr (*p);
+ unsigned first = end < offset ? 0 : end - offset;
+
+ for (index = first; index < addr_per_block; index++)
+ if (ind_bh[index])
+ {
+ (*free_block)(ind_bh + index, index);
+ if (ind_bh[index])
+ all_freed = 0; /* Some descendent hasn't been freed. */
+ else
+ modified = 1;
+ }
+
+ if (first == 0 && all_freed)
+ free_block_run_free_ptr (fbr, p);
+ else if (modified)
+ record_indir_poke (node, ind_bh);
+ }
+}
+
+static void
+trunc_single_indirect (struct node *node, block_t end,
+ block_t *p, block_t offset,
+ struct free_block_run *fbr)
+{
+ void free_block (block_t *p, unsigned index)
+ {
+ free_block_run_free_ptr (fbr, p);
+ }
+ trunc_indirect (node, end, p, offset, free_block, fbr);
+}
+
+static void
+trunc_double_indirect (struct node *node, block_t end,
+ block_t *p, block_t offset,
+ struct free_block_run *fbr)
+{
+ void free_block (block_t *p, unsigned index)
+ {
+ block_t entry_offs = offset + (index * addr_per_block);
+ trunc_single_indirect (node, end, p, entry_offs, fbr);
+ }
+ trunc_indirect (node, end, p, offset, free_block, fbr);
+}
+
+static void
+trunc_triple_indirect (struct node *node, block_t end,
+ block_t *p, block_t offset,
+ struct free_block_run *fbr)
+{
+ void free_block (block_t *p, unsigned index)
+ {
+ block_t entry_offs = offset + (index * addr_per_block * addr_per_block);
+ trunc_double_indirect (node, end, p, entry_offs, fbr);
+ }
+ trunc_indirect (node, end, p, offset, free_block, fbr);
+}
+
+/* ---------------------------------------------------------------- */
+
+/* Write something to each page from START to END inclusive of memory
+ object OBJ, but make sure the data doesns't actually change. */
+static void
+poke_pages (memory_object_t obj, vm_offset_t start, vm_offset_t end)
+{
+ while (start < end)
+ {
+ error_t err;
+ vm_size_t len = 8 * vm_page_size;
+ vm_address_t addr = 0;
+
+ if (len > end - start)
+ len = end - start;
+
+ err = vm_map (mach_task_self (), &addr, len, 0, 1, obj, start, 0,
+ VM_PROT_WRITE|VM_PROT_READ, VM_PROT_READ|VM_PROT_WRITE, 0);
+ if (!err)
+ {
+ vm_address_t poke;
+ for (poke = addr; poke < addr + len; poke += vm_page_size)
+ *(volatile int *)poke = *(volatile int *)poke;
+ munmap ((caddr_t) addr, len);
+ }
+
+ start += len;
+ }
+}
+
+/* Flush all the data past the new size from the kernel. Also force any
+ delayed copies of this data to take place immediately. (We are implicitly
+ changing the data to zeros and doing it without the kernel's immediate
+ knowledge; accordingl we must help out the kernel thusly.) */
+static void
+force_delayed_copies (struct node *node, off_t length)
+{
+ struct pager *pager;
+
+ spin_lock (&node_to_page_lock);
+ pager = node->dn->pager;
+ if (pager)
+ ports_port_ref (pager);
+ spin_unlock (&node_to_page_lock);
+
+ if (pager)
+ {
+ mach_port_t obj;
+
+ pager_change_attributes (pager, MAY_CACHE, MEMORY_OBJECT_COPY_NONE, 1);
+ obj = diskfs_get_filemap (node, VM_PROT_READ);
+ if (obj != MACH_PORT_NULL)
+ {
+ /* XXX should cope with errors from diskfs_get_filemap */
+ poke_pages (obj, round_page (length), round_page (node->allocsize));
+ mach_port_deallocate (mach_task_self (), obj);
+ pager_flush_some (pager, round_page(length),
+ node->allocsize - length, 1);
+ }
+
+ ports_port_deref (pager);
+ }
+}
+
+static void
+enable_delayed_copies (struct node *node)
+{
+ struct pager *pager;
+
+ spin_lock (&node_to_page_lock);
+ pager = node->dn->pager;
+ if (pager)
+ ports_port_ref (pager);
+ spin_unlock (&node_to_page_lock);
+
+ if (pager)
+ {
+ pager_change_attributes (pager, MAY_CACHE, MEMORY_OBJECT_COPY_DELAY, 0);
+ ports_port_deref (pager);
+ }
+}
+
+/* ---------------------------------------------------------------- */
+
+/* The user must define this function. Truncate locked node NODE to be SIZE
+ bytes long. (If NODE is already less than or equal to SIZE bytes
+ long, do nothing.) If this is a symlink (and diskfs_shortcut_symlink
+ is set) then this should clear the symlink, even if
+ diskfs_create_symlink_hook stores the link target elsewhere. */
+error_t
+diskfs_truncate (struct node *node, off_t length)
+{
+ error_t err;
+ off_t offset;
+
+ diskfs_check_readonly ();
+ assert (!diskfs_readonly);
+
+ if (length >= node->dn_stat.st_size)
+ return 0;
+
+ if (! node->dn_stat.st_blocks)
+ /* There aren't really any blocks allocated, so just frob the size. This
+ is true for fast symlinks, and also apparently for some device nodes
+ in linux. */
+ {
+ node->dn_stat.st_size = length;
+ node->dn_set_mtime = 1;
+ node->dn_set_ctime = 1;
+ diskfs_node_update (node, 1);
+ return 0;
+ }
+
+ /*
+ * If the file is not being truncated to a block boundary, the
+ * contents of the partial block following the end of the file must be
+ * zeroed in case it ever becomes accessible again because of
+ * subsequent file growth.
+ */
+ offset = length % block_size;
+ if (offset > 0)
+ {
+ diskfs_node_rdwr (node, (void *)zeroblock, length, block_size - offset,
+ 1, 0, 0);
+ diskfs_file_update (node, 1);
+ }
+
+ ext2_discard_prealloc(node);
+
+ force_delayed_copies (node, length);
+
+ rwlock_writer_lock (&node->dn->alloc_lock);
+
+ /* Update the size on disk; fsck will finish freeing blocks if necessary
+ should we crash. */
+ node->dn_stat.st_size = length;
+ node->dn_set_mtime = 1;
+ node->dn_set_ctime = 1;
+ diskfs_node_update (node, 1);
+
+ err = diskfs_catch_exception ();
+ if (!err)
+ {
+ block_t end = boffs_block (round_block (length)), offs;
+ block_t *bptrs = node->dn->info.i_data;
+ struct free_block_run fbr;
+
+ free_block_run_init (&fbr, node);
+
+ trunc_direct (node, end, &fbr);
+
+ offs = EXT2_NDIR_BLOCKS;
+ trunc_single_indirect (node, end, bptrs + EXT2_IND_BLOCK, offs, &fbr);
+ offs += addr_per_block;
+ trunc_double_indirect (node, end, bptrs + EXT2_DIND_BLOCK, offs, &fbr);
+ offs += addr_per_block * addr_per_block;
+ trunc_triple_indirect (node, end, bptrs + EXT2_TIND_BLOCK, offs, &fbr);
+
+ free_block_run_finish (&fbr);
+
+ node->allocsize = round_block (length);
+
+ /* Set our last_page_partially_writable to a pessimistic state -- it
+ won't hurt if is wrong. */
+ node->dn->last_page_partially_writable =
+ trunc_page (node->allocsize) != node->allocsize;
+
+ diskfs_end_catch_exception ();
+ }
+
+ node->dn_set_mtime = 1;
+ node->dn_set_ctime = 1;
+ node->dn_stat_dirty = 1;
+
+ /* Now we can permit delayed copies again. */
+ enable_delayed_copies (node);
+
+ rwlock_writer_unlock (&node->dn->alloc_lock);
+
+ return err;
+}