| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 1 | /* -*- linux-c -*- ------------------------------------------------------- * | 
 | 2 |  *    | 
 | 3 |  *   Copyright 2001 H. Peter Anvin - All Rights Reserved | 
 | 4 |  * | 
 | 5 |  *   This program is free software; you can redistribute it and/or modify | 
 | 6 |  *   it under the terms of the GNU General Public License as published by | 
 | 7 |  *   the Free Software Foundation, Inc., 675 Mass Ave, Cambridge MA 02139, | 
 | 8 |  *   USA; either version 2 of the License, or (at your option) any later | 
 | 9 |  *   version; incorporated herein by reference. | 
 | 10 |  * | 
 | 11 |  * ----------------------------------------------------------------------- */ | 
 | 12 |  | 
 | 13 | /* | 
 | 14 |  * linux/fs/isofs/compress.c | 
 | 15 |  * | 
 | 16 |  * Transparent decompression of files on an iso9660 filesystem | 
 | 17 |  */ | 
 | 18 |  | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 19 | #include <linux/module.h> | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 20 | #include <linux/init.h> | 
| Al Viro | 94f2f71 | 2005-04-25 18:32:12 -0700 | [diff] [blame] | 21 |  | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 22 | #include <linux/vmalloc.h> | 
 | 23 | #include <linux/zlib.h> | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 24 |  | 
| Al Viro | 94f2f71 | 2005-04-25 18:32:12 -0700 | [diff] [blame] | 25 | #include "isofs.h" | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 26 | #include "zisofs.h" | 
 | 27 |  | 
 | 28 | /* This should probably be global. */ | 
 | 29 | static char zisofs_sink_page[PAGE_CACHE_SIZE]; | 
 | 30 |  | 
 | 31 | /* | 
 | 32 |  * This contains the zlib memory allocation and the mutex for the | 
 | 33 |  * allocation; this avoids failures at block-decompression time. | 
 | 34 |  */ | 
 | 35 | static void *zisofs_zlib_workspace; | 
| Dave Young | a36a151 | 2007-10-16 23:26:10 -0700 | [diff] [blame] | 36 | static DEFINE_MUTEX(zisofs_zlib_lock); | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 37 |  | 
 | 38 | /* | 
| Jan Kara | 59bc055 | 2009-09-23 14:44:56 +0200 | [diff] [blame] | 39 |  * Read data of @inode from @block_start to @block_end and uncompress | 
 | 40 |  * to one zisofs block. Store the data in the @pages array with @pcount | 
 | 41 |  * entries. Start storing at offset @poffset of the first page. | 
 | 42 |  */ | 
 | 43 | static loff_t zisofs_uncompress_block(struct inode *inode, loff_t block_start, | 
 | 44 | 				      loff_t block_end, int pcount, | 
 | 45 | 				      struct page **pages, unsigned poffset, | 
 | 46 | 				      int *errp) | 
 | 47 | { | 
 | 48 | 	unsigned int zisofs_block_shift = ISOFS_I(inode)->i_format_parm[1]; | 
 | 49 | 	unsigned int bufsize = ISOFS_BUFFER_SIZE(inode); | 
 | 50 | 	unsigned int bufshift = ISOFS_BUFFER_BITS(inode); | 
 | 51 | 	unsigned int bufmask = bufsize - 1; | 
 | 52 | 	int i, block_size = block_end - block_start; | 
 | 53 | 	z_stream stream = { .total_out = 0, | 
 | 54 | 			    .avail_in = 0, | 
 | 55 | 			    .avail_out = 0, }; | 
 | 56 | 	int zerr; | 
 | 57 | 	int needblocks = (block_size + (block_start & bufmask) + bufmask) | 
 | 58 | 				>> bufshift; | 
 | 59 | 	int haveblocks; | 
 | 60 | 	blkcnt_t blocknum; | 
 | 61 | 	struct buffer_head *bhs[needblocks + 1]; | 
 | 62 | 	int curbh, curpage; | 
 | 63 |  | 
 | 64 | 	if (block_size > deflateBound(1UL << zisofs_block_shift)) { | 
 | 65 | 		*errp = -EIO; | 
 | 66 | 		return 0; | 
 | 67 | 	} | 
 | 68 | 	/* Empty block? */ | 
 | 69 | 	if (block_size == 0) { | 
 | 70 | 		for ( i = 0 ; i < pcount ; i++ ) { | 
 | 71 | 			if (!pages[i]) | 
 | 72 | 				continue; | 
 | 73 | 			memset(page_address(pages[i]), 0, PAGE_CACHE_SIZE); | 
 | 74 | 			flush_dcache_page(pages[i]); | 
 | 75 | 			SetPageUptodate(pages[i]); | 
 | 76 | 		} | 
 | 77 | 		return ((loff_t)pcount) << PAGE_CACHE_SHIFT; | 
 | 78 | 	} | 
 | 79 |  | 
 | 80 | 	/* Because zlib is not thread-safe, do all the I/O at the top. */ | 
 | 81 | 	blocknum = block_start >> bufshift; | 
 | 82 | 	memset(bhs, 0, (needblocks + 1) * sizeof(struct buffer_head *)); | 
 | 83 | 	haveblocks = isofs_get_blocks(inode, blocknum, bhs, needblocks); | 
 | 84 | 	ll_rw_block(READ, haveblocks, bhs); | 
 | 85 |  | 
 | 86 | 	curbh = 0; | 
 | 87 | 	curpage = 0; | 
 | 88 | 	/* | 
 | 89 | 	 * First block is special since it may be fractional.  We also wait for | 
 | 90 | 	 * it before grabbing the zlib mutex; odds are that the subsequent | 
 | 91 | 	 * blocks are going to come in in short order so we don't hold the zlib | 
 | 92 | 	 * mutex longer than necessary. | 
 | 93 | 	 */ | 
 | 94 |  | 
 | 95 | 	if (!bhs[0]) | 
 | 96 | 		goto b_eio; | 
 | 97 |  | 
 | 98 | 	wait_on_buffer(bhs[0]); | 
 | 99 | 	if (!buffer_uptodate(bhs[0])) { | 
 | 100 | 		*errp = -EIO; | 
 | 101 | 		goto b_eio; | 
 | 102 | 	} | 
 | 103 |  | 
 | 104 | 	stream.workspace = zisofs_zlib_workspace; | 
 | 105 | 	mutex_lock(&zisofs_zlib_lock); | 
 | 106 | 		 | 
 | 107 | 	zerr = zlib_inflateInit(&stream); | 
 | 108 | 	if (zerr != Z_OK) { | 
 | 109 | 		if (zerr == Z_MEM_ERROR) | 
 | 110 | 			*errp = -ENOMEM; | 
 | 111 | 		else | 
 | 112 | 			*errp = -EIO; | 
 | 113 | 		printk(KERN_DEBUG "zisofs: zisofs_inflateInit returned %d\n", | 
 | 114 | 			       zerr); | 
 | 115 | 		goto z_eio; | 
 | 116 | 	} | 
 | 117 |  | 
 | 118 | 	while (curpage < pcount && curbh < haveblocks && | 
 | 119 | 	       zerr != Z_STREAM_END) { | 
 | 120 | 		if (!stream.avail_out) { | 
 | 121 | 			if (pages[curpage]) { | 
 | 122 | 				stream.next_out = page_address(pages[curpage]) | 
 | 123 | 						+ poffset; | 
 | 124 | 				stream.avail_out = PAGE_CACHE_SIZE - poffset; | 
 | 125 | 				poffset = 0; | 
 | 126 | 			} else { | 
 | 127 | 				stream.next_out = (void *)&zisofs_sink_page; | 
 | 128 | 				stream.avail_out = PAGE_CACHE_SIZE; | 
 | 129 | 			} | 
 | 130 | 		} | 
 | 131 | 		if (!stream.avail_in) { | 
 | 132 | 			wait_on_buffer(bhs[curbh]); | 
 | 133 | 			if (!buffer_uptodate(bhs[curbh])) { | 
 | 134 | 				*errp = -EIO; | 
 | 135 | 				break; | 
 | 136 | 			} | 
 | 137 | 			stream.next_in  = bhs[curbh]->b_data + | 
 | 138 | 						(block_start & bufmask); | 
 | 139 | 			stream.avail_in = min_t(unsigned, bufsize - | 
 | 140 | 						(block_start & bufmask), | 
 | 141 | 						block_size); | 
 | 142 | 			block_size -= stream.avail_in; | 
 | 143 | 			block_start = 0; | 
 | 144 | 		} | 
 | 145 |  | 
 | 146 | 		while (stream.avail_out && stream.avail_in) { | 
 | 147 | 			zerr = zlib_inflate(&stream, Z_SYNC_FLUSH); | 
 | 148 | 			if (zerr == Z_BUF_ERROR && stream.avail_in == 0) | 
 | 149 | 				break; | 
 | 150 | 			if (zerr == Z_STREAM_END) | 
 | 151 | 				break; | 
 | 152 | 			if (zerr != Z_OK) { | 
 | 153 | 				/* EOF, error, or trying to read beyond end of input */ | 
 | 154 | 				if (zerr == Z_MEM_ERROR) | 
 | 155 | 					*errp = -ENOMEM; | 
 | 156 | 				else { | 
 | 157 | 					printk(KERN_DEBUG | 
 | 158 | 					       "zisofs: zisofs_inflate returned" | 
 | 159 | 					       " %d, inode = %lu," | 
 | 160 | 					       " page idx = %d, bh idx = %d," | 
 | 161 | 					       " avail_in = %d," | 
 | 162 | 					       " avail_out = %d\n", | 
 | 163 | 					       zerr, inode->i_ino, curpage, | 
 | 164 | 					       curbh, stream.avail_in, | 
 | 165 | 					       stream.avail_out); | 
 | 166 | 					*errp = -EIO; | 
 | 167 | 				} | 
 | 168 | 				goto inflate_out; | 
 | 169 | 			} | 
 | 170 | 		} | 
 | 171 |  | 
 | 172 | 		if (!stream.avail_out) { | 
 | 173 | 			/* This page completed */ | 
 | 174 | 			if (pages[curpage]) { | 
 | 175 | 				flush_dcache_page(pages[curpage]); | 
 | 176 | 				SetPageUptodate(pages[curpage]); | 
 | 177 | 			} | 
 | 178 | 			curpage++; | 
 | 179 | 		} | 
 | 180 | 		if (!stream.avail_in) | 
 | 181 | 			curbh++; | 
 | 182 | 	} | 
 | 183 | inflate_out: | 
 | 184 | 	zlib_inflateEnd(&stream); | 
 | 185 |  | 
 | 186 | z_eio: | 
 | 187 | 	mutex_unlock(&zisofs_zlib_lock); | 
 | 188 |  | 
 | 189 | b_eio: | 
 | 190 | 	for (i = 0; i < haveblocks; i++) | 
 | 191 | 		brelse(bhs[i]); | 
 | 192 | 	return stream.total_out; | 
 | 193 | } | 
 | 194 |  | 
 | 195 | /* | 
 | 196 |  * Uncompress data so that pages[full_page] is fully uptodate and possibly | 
 | 197 |  * fills in other pages if we have data for them. | 
 | 198 |  */ | 
 | 199 | static int zisofs_fill_pages(struct inode *inode, int full_page, int pcount, | 
 | 200 | 			     struct page **pages) | 
 | 201 | { | 
 | 202 | 	loff_t start_off, end_off; | 
 | 203 | 	loff_t block_start, block_end; | 
 | 204 | 	unsigned int header_size = ISOFS_I(inode)->i_format_parm[0]; | 
 | 205 | 	unsigned int zisofs_block_shift = ISOFS_I(inode)->i_format_parm[1]; | 
 | 206 | 	unsigned int blockptr; | 
 | 207 | 	loff_t poffset = 0; | 
 | 208 | 	blkcnt_t cstart_block, cend_block; | 
 | 209 | 	struct buffer_head *bh; | 
 | 210 | 	unsigned int blkbits = ISOFS_BUFFER_BITS(inode); | 
 | 211 | 	unsigned int blksize = 1 << blkbits; | 
 | 212 | 	int err; | 
 | 213 | 	loff_t ret; | 
 | 214 |  | 
 | 215 | 	BUG_ON(!pages[full_page]); | 
 | 216 |  | 
 | 217 | 	/* | 
 | 218 | 	 * We want to read at least 'full_page' page. Because we have to | 
 | 219 | 	 * uncompress the whole compression block anyway, fill the surrounding | 
 | 220 | 	 * pages with the data we have anyway... | 
 | 221 | 	 */ | 
 | 222 | 	start_off = page_offset(pages[full_page]); | 
 | 223 | 	end_off = min_t(loff_t, start_off + PAGE_CACHE_SIZE, inode->i_size); | 
 | 224 |  | 
 | 225 | 	cstart_block = start_off >> zisofs_block_shift; | 
 | 226 | 	cend_block = (end_off + (1 << zisofs_block_shift) - 1) | 
 | 227 | 			>> zisofs_block_shift; | 
 | 228 |  | 
 | 229 | 	WARN_ON(start_off - (full_page << PAGE_CACHE_SHIFT) != | 
 | 230 | 		((cstart_block << zisofs_block_shift) & PAGE_CACHE_MASK)); | 
 | 231 |  | 
 | 232 | 	/* Find the pointer to this specific chunk */ | 
 | 233 | 	/* Note: we're not using isonum_731() here because the data is known aligned */ | 
 | 234 | 	/* Note: header_size is in 32-bit words (4 bytes) */ | 
 | 235 | 	blockptr = (header_size + cstart_block) << 2; | 
 | 236 | 	bh = isofs_bread(inode, blockptr >> blkbits); | 
 | 237 | 	if (!bh) | 
 | 238 | 		return -EIO; | 
 | 239 | 	block_start = le32_to_cpu(*(__le32 *) | 
 | 240 | 				(bh->b_data + (blockptr & (blksize - 1)))); | 
 | 241 |  | 
 | 242 | 	while (cstart_block < cend_block && pcount > 0) { | 
 | 243 | 		/* Load end of the compressed block in the file */ | 
 | 244 | 		blockptr += 4; | 
 | 245 | 		/* Traversed to next block? */ | 
 | 246 | 		if (!(blockptr & (blksize - 1))) { | 
 | 247 | 			brelse(bh); | 
 | 248 |  | 
 | 249 | 			bh = isofs_bread(inode, blockptr >> blkbits); | 
 | 250 | 			if (!bh) | 
 | 251 | 				return -EIO; | 
 | 252 | 		} | 
 | 253 | 		block_end = le32_to_cpu(*(__le32 *) | 
 | 254 | 				(bh->b_data + (blockptr & (blksize - 1)))); | 
 | 255 | 		if (block_start > block_end) { | 
 | 256 | 			brelse(bh); | 
 | 257 | 			return -EIO; | 
 | 258 | 		} | 
 | 259 | 		err = 0; | 
 | 260 | 		ret = zisofs_uncompress_block(inode, block_start, block_end, | 
 | 261 | 					      pcount, pages, poffset, &err); | 
 | 262 | 		poffset += ret; | 
 | 263 | 		pages += poffset >> PAGE_CACHE_SHIFT; | 
 | 264 | 		pcount -= poffset >> PAGE_CACHE_SHIFT; | 
 | 265 | 		full_page -= poffset >> PAGE_CACHE_SHIFT; | 
 | 266 | 		poffset &= ~PAGE_CACHE_MASK; | 
 | 267 |  | 
 | 268 | 		if (err) { | 
 | 269 | 			brelse(bh); | 
 | 270 | 			/* | 
 | 271 | 			 * Did we finish reading the page we really wanted | 
 | 272 | 			 * to read? | 
 | 273 | 			 */ | 
 | 274 | 			if (full_page < 0) | 
 | 275 | 				return 0; | 
 | 276 | 			return err; | 
 | 277 | 		} | 
 | 278 |  | 
 | 279 | 		block_start = block_end; | 
 | 280 | 		cstart_block++; | 
 | 281 | 	} | 
 | 282 |  | 
 | 283 | 	if (poffset && *pages) { | 
 | 284 | 		memset(page_address(*pages) + poffset, 0, | 
 | 285 | 		       PAGE_CACHE_SIZE - poffset); | 
 | 286 | 		flush_dcache_page(*pages); | 
 | 287 | 		SetPageUptodate(*pages); | 
 | 288 | 	} | 
 | 289 | 	return 0; | 
 | 290 | } | 
 | 291 |  | 
 | 292 | /* | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 293 |  * When decompressing, we typically obtain more than one page | 
 | 294 |  * per reference.  We inject the additional pages into the page | 
 | 295 |  * cache as a form of readahead. | 
 | 296 |  */ | 
 | 297 | static int zisofs_readpage(struct file *file, struct page *page) | 
 | 298 | { | 
| Josef "Jeff" Sipek | 2485822 | 2006-12-08 02:36:40 -0800 | [diff] [blame] | 299 | 	struct inode *inode = file->f_path.dentry->d_inode; | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 300 | 	struct address_space *mapping = inode->i_mapping; | 
| Jan Kara | 59bc055 | 2009-09-23 14:44:56 +0200 | [diff] [blame] | 301 | 	int err; | 
 | 302 | 	int i, pcount, full_page; | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 303 | 	unsigned int zisofs_block_shift = ISOFS_I(inode)->i_format_parm[1]; | 
| Jan Kara | 59bc055 | 2009-09-23 14:44:56 +0200 | [diff] [blame] | 304 | 	unsigned int zisofs_pages_per_cblock = | 
 | 305 | 		PAGE_CACHE_SHIFT <= zisofs_block_shift ? | 
 | 306 | 		(1 << (zisofs_block_shift - PAGE_CACHE_SHIFT)) : 0; | 
 | 307 | 	struct page *pages[max_t(unsigned, zisofs_pages_per_cblock, 1)]; | 
 | 308 | 	pgoff_t index = page->index, end_index; | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 309 |  | 
| Jan Kara | 59bc055 | 2009-09-23 14:44:56 +0200 | [diff] [blame] | 310 | 	end_index = (inode->i_size + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT; | 
| Dave Young | 08ca0db | 2008-03-19 17:01:01 -0700 | [diff] [blame] | 311 | 	/* | 
 | 312 | 	 * If this page is wholly outside i_size we just return zero; | 
 | 313 | 	 * do_generic_file_read() will handle this for us | 
 | 314 | 	 */ | 
| Jan Kara | 59bc055 | 2009-09-23 14:44:56 +0200 | [diff] [blame] | 315 | 	if (index >= end_index) { | 
| Dave Young | 08ca0db | 2008-03-19 17:01:01 -0700 | [diff] [blame] | 316 | 		SetPageUptodate(page); | 
 | 317 | 		unlock_page(page); | 
 | 318 | 		return 0; | 
 | 319 | 	} | 
 | 320 |  | 
| Jan Kara | 59bc055 | 2009-09-23 14:44:56 +0200 | [diff] [blame] | 321 | 	if (PAGE_CACHE_SHIFT <= zisofs_block_shift) { | 
 | 322 | 		/* We have already been given one page, this is the one | 
 | 323 | 		   we must do. */ | 
 | 324 | 		full_page = index & (zisofs_pages_per_cblock - 1); | 
 | 325 | 		pcount = min_t(int, zisofs_pages_per_cblock, | 
 | 326 | 			end_index - (index & ~(zisofs_pages_per_cblock - 1))); | 
 | 327 | 		index -= full_page; | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 328 | 	} else { | 
| Jan Kara | 59bc055 | 2009-09-23 14:44:56 +0200 | [diff] [blame] | 329 | 		full_page = 0; | 
 | 330 | 		pcount = 1; | 
 | 331 | 	} | 
 | 332 | 	pages[full_page] = page; | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 333 |  | 
| Jan Kara | 59bc055 | 2009-09-23 14:44:56 +0200 | [diff] [blame] | 334 | 	for (i = 0; i < pcount; i++, index++) { | 
 | 335 | 		if (i != full_page) | 
 | 336 | 			pages[i] = grab_cache_page_nowait(mapping, index); | 
 | 337 | 		if (pages[i]) { | 
 | 338 | 			ClearPageError(pages[i]); | 
 | 339 | 			kmap(pages[i]); | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 340 | 		} | 
 | 341 | 	} | 
 | 342 |  | 
| Jan Kara | 59bc055 | 2009-09-23 14:44:56 +0200 | [diff] [blame] | 343 | 	err = zisofs_fill_pages(inode, full_page, pcount, pages); | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 344 |  | 
 | 345 | 	/* Release any residual pages, do not SetPageUptodate */ | 
| Jan Kara | 59bc055 | 2009-09-23 14:44:56 +0200 | [diff] [blame] | 346 | 	for (i = 0; i < pcount; i++) { | 
 | 347 | 		if (pages[i]) { | 
 | 348 | 			flush_dcache_page(pages[i]); | 
 | 349 | 			if (i == full_page && err) | 
 | 350 | 				SetPageError(pages[i]); | 
 | 351 | 			kunmap(pages[i]); | 
 | 352 | 			unlock_page(pages[i]); | 
 | 353 | 			if (i != full_page) | 
 | 354 | 				page_cache_release(pages[i]); | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 355 | 		} | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 356 | 	}			 | 
 | 357 |  | 
 | 358 | 	/* At this point, err contains 0 or -EIO depending on the "critical" page */ | 
 | 359 | 	return err; | 
 | 360 | } | 
 | 361 |  | 
| Christoph Hellwig | f5e54d6 | 2006-06-28 04:26:44 -0700 | [diff] [blame] | 362 | const struct address_space_operations zisofs_aops = { | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 363 | 	.readpage = zisofs_readpage, | 
 | 364 | 	/* No sync_page operation supported? */ | 
 | 365 | 	/* No bmap operation supported */ | 
 | 366 | }; | 
 | 367 |  | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 368 | int __init zisofs_init(void) | 
 | 369 | { | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 370 | 	zisofs_zlib_workspace = vmalloc(zlib_inflate_workspacesize()); | 
 | 371 | 	if ( !zisofs_zlib_workspace ) | 
 | 372 | 		return -ENOMEM; | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 373 |  | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 374 | 	return 0; | 
 | 375 | } | 
 | 376 |  | 
 | 377 | void zisofs_cleanup(void) | 
 | 378 | { | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 379 | 	vfree(zisofs_zlib_workspace); | 
| Linus Torvalds | 1da177e | 2005-04-16 15:20:36 -0700 | [diff] [blame] | 380 | } |