1/*	$NetBSD: ch.c,v 1.2 2011/07/03 19:51:26 tron Exp $	*/
2
3/*
4 * Copyright (C) 1984-2011  Mark Nudelman
5 *
6 * You may distribute under the terms of either the GNU General Public
7 * License or the Less License, as specified in the README file.
8 *
9 * For more information about less, or for information on how to
10 * contact the author, see the README file.
11 */
12
13
14/*
15 * Low level character input from the input file.
16 * We use these special purpose routines which optimize moving
17 * both forward and backward from the current read pointer.
18 */
19
20#include "less.h"
21#if MSDOS_COMPILER==WIN32C
22#include <errno.h>
23#include <windows.h>
24#endif
25
26#if HAVE_STAT_INO
27#include <sys/stat.h>
28extern dev_t curr_dev;
29extern ino_t curr_ino;
30#endif
31
32typedef POSITION BLOCKNUM;
33
34public int ignore_eoi;
35
36/*
37 * Pool of buffers holding the most recently used blocks of the input file.
38 * The buffer pool is kept as a doubly-linked circular list,
39 * in order from most- to least-recently used.
40 * The circular list is anchored by the file state "thisfile".
41 */
42struct bufnode {
43	struct bufnode *next, *prev;
44	struct bufnode *hnext, *hprev;
45};
46
47#define	LBUFSIZE	8192
48struct buf {
49	struct bufnode node;
50	BLOCKNUM block;
51	unsigned int datasize;
52	unsigned char data[LBUFSIZE];
53};
54#define bufnode_buf(bn)  ((struct buf *) bn)
55
56/*
57 * The file state is maintained in a filestate structure.
58 * A pointer to the filestate is kept in the ifile structure.
59 */
60#define	BUFHASH_SIZE	64
61struct filestate {
62	struct bufnode buflist;
63	struct bufnode hashtbl[BUFHASH_SIZE];
64	int file;
65	int flags;
66	POSITION fpos;
67	int nbufs;
68	BLOCKNUM block;
69	unsigned int offset;
70	POSITION fsize;
71};
72
73#define	ch_bufhead	thisfile->buflist.next
74#define	ch_buftail	thisfile->buflist.prev
75#define	ch_nbufs	thisfile->nbufs
76#define	ch_block	thisfile->block
77#define	ch_offset	thisfile->offset
78#define	ch_fpos		thisfile->fpos
79#define	ch_fsize	thisfile->fsize
80#define	ch_flags	thisfile->flags
81#define	ch_file		thisfile->file
82
83#define	END_OF_CHAIN	(&thisfile->buflist)
84#define	END_OF_HCHAIN(h) (&thisfile->hashtbl[h])
85#define BUFHASH(blk)	((blk) & (BUFHASH_SIZE-1))
86
87/*
88 * Macros to manipulate the list of buffers in thisfile->buflist.
89 */
90#define	FOR_BUFS(bn) \
91	for (bn = ch_bufhead;  bn != END_OF_CHAIN;  bn = bn->next)
92
93#define BUF_RM(bn) \
94	(bn)->next->prev = (bn)->prev; \
95	(bn)->prev->next = (bn)->next;
96
97#define BUF_INS_HEAD(bn) \
98	(bn)->next = ch_bufhead; \
99	(bn)->prev = END_OF_CHAIN; \
100	ch_bufhead->prev = (bn); \
101	ch_bufhead = (bn);
102
103#define BUF_INS_TAIL(bn) \
104	(bn)->next = END_OF_CHAIN; \
105	(bn)->prev = ch_buftail; \
106	ch_buftail->next = (bn); \
107	ch_buftail = (bn);
108
109/*
110 * Macros to manipulate the list of buffers in thisfile->hashtbl[n].
111 */
112#define	FOR_BUFS_IN_CHAIN(h,bn) \
113	for (bn = thisfile->hashtbl[h].hnext;  \
114	     bn != END_OF_HCHAIN(h);  bn = bn->hnext)
115
116#define	BUF_HASH_RM(bn) \
117	(bn)->hnext->hprev = (bn)->hprev; \
118	(bn)->hprev->hnext = (bn)->hnext;
119
120#define	BUF_HASH_INS(bn,h) \
121	(bn)->hnext = thisfile->hashtbl[h].hnext; \
122	(bn)->hprev = END_OF_HCHAIN(h); \
123	thisfile->hashtbl[h].hnext->hprev = (bn); \
124	thisfile->hashtbl[h].hnext = (bn);
125
126static struct filestate *thisfile;
127static int ch_ungotchar = -1;
128static int maxbufs = -1;
129
130extern int autobuf;
131extern int sigs;
132extern int secure;
133extern int screen_trashed;
134extern int follow_mode;
135extern constant char helpdata[];
136extern constant int size_helpdata;
137extern IFILE curr_ifile;
138#if LOGFILE
139extern int logfile;
140extern char *namelogfile;
141#endif
142
143static int ch_addbuf __P((void));
144static int buffered __P((BLOCKNUM));
145static void ch_delbufs __P((void));
146
147
148/*
149 * Get the character pointed to by the read pointer.
150 */
151	int
152ch_get()
153{
154	register struct buf *bp;
155	register struct bufnode *bn;
156	register int n;
157	register int slept;
158	register int h;
159	POSITION pos;
160	POSITION len;
161
162	if (thisfile == NULL)
163		return (EOI);
164
165	/*
166	 * Quick check for the common case where
167	 * the desired char is in the head buffer.
168	 */
169	if (ch_bufhead != END_OF_CHAIN)
170	{
171		bp = bufnode_buf(ch_bufhead);
172		if (ch_block == bp->block && ch_offset < bp->datasize)
173			return bp->data[ch_offset];
174	}
175
176	slept = FALSE;
177
178	/*
179	 * Look for a buffer holding the desired block.
180	 */
181	h = BUFHASH(ch_block);
182	FOR_BUFS_IN_CHAIN(h, bn)
183	{
184		bp = bufnode_buf(bn);
185		if (bp->block == ch_block)
186		{
187			if (ch_offset >= bp->datasize)
188				/*
189				 * Need more data in this buffer.
190				 */
191				break;
192			goto found;
193		}
194	}
195	if (bn == END_OF_HCHAIN(h))
196	{
197		/*
198		 * Block is not in a buffer.
199		 * Take the least recently used buffer
200		 * and read the desired block into it.
201		 * If the LRU buffer has data in it,
202		 * then maybe allocate a new buffer.
203		 */
204		if (ch_buftail == END_OF_CHAIN ||
205			bufnode_buf(ch_buftail)->block != -1)
206		{
207			/*
208			 * There is no empty buffer to use.
209			 * Allocate a new buffer if:
210			 * 1. We can't seek on this file and -b is not in effect; or
211			 * 2. We haven't allocated the max buffers for this file yet.
212			 */
213			if ((autobuf && !(ch_flags & CH_CANSEEK)) ||
214				(maxbufs < 0 || ch_nbufs < maxbufs))
215				if (ch_addbuf())
216					/*
217					 * Allocation failed: turn off autobuf.
218					 */
219					autobuf = OPT_OFF;
220		}
221		bn = ch_buftail;
222		bp = bufnode_buf(bn);
223		BUF_HASH_RM(bn); /* Remove from old hash chain. */
224		bp->block = ch_block;
225		bp->datasize = 0;
226		BUF_HASH_INS(bn, h); /* Insert into new hash chain. */
227	}
228
229    read_more:
230	pos = (ch_block * LBUFSIZE) + bp->datasize;
231	if ((len = ch_length()) != NULL_POSITION && pos >= len)
232		/*
233		 * At end of file.
234		 */
235		return (EOI);
236
237	if (pos != ch_fpos)
238	{
239		/*
240		 * Not at the correct position: must seek.
241		 * If input is a pipe, we're in trouble (can't seek on a pipe).
242		 * Some data has been lost: just return "?".
243		 */
244		if (!(ch_flags & CH_CANSEEK))
245			return ('?');
246		if (lseek(ch_file, (off_t)pos, SEEK_SET) == BAD_LSEEK)
247		{
248 			error("seek error", NULL_PARG);
249			clear_eol();
250			return (EOI);
251 		}
252 		ch_fpos = pos;
253 	}
254
255	/*
256	 * Read the block.
257	 * If we read less than a full block, that's ok.
258	 * We use partial block and pick up the rest next time.
259	 */
260	if (ch_ungotchar != -1)
261	{
262		bp->data[bp->datasize] = ch_ungotchar;
263		n = 1;
264		ch_ungotchar = -1;
265	} else if (ch_flags & CH_HELPFILE)
266	{
267		bp->data[bp->datasize] = helpdata[ch_fpos];
268		n = 1;
269	} else
270	{
271		n = iread(ch_file, &bp->data[bp->datasize],
272			(unsigned int)(LBUFSIZE - bp->datasize));
273	}
274
275	if (n == READ_INTR)
276		return (EOI);
277	if (n < 0)
278	{
279#if MSDOS_COMPILER==WIN32C
280		if (errno != EPIPE)
281#endif
282		{
283			error("read error", NULL_PARG);
284			clear_eol();
285		}
286		n = 0;
287	}
288
289#if LOGFILE
290	/*
291	 * If we have a log file, write the new data to it.
292	 */
293	if (!secure && logfile >= 0 && n > 0)
294		write(logfile, (char *) &bp->data[bp->datasize], n);
295#endif
296
297	ch_fpos += n;
298	bp->datasize += n;
299
300	/*
301	 * If we have read to end of file, set ch_fsize to indicate
302	 * the position of the end of file.
303	 */
304	if (n == 0)
305	{
306		ch_fsize = pos;
307		if (ignore_eoi)
308		{
309			/*
310			 * We are ignoring EOF.
311			 * Wait a while, then try again.
312			 */
313			if (!slept)
314			{
315				PARG parg;
316				parg.p_string = wait_message();
317				ierror("%s", &parg);
318			}
319#if !MSDOS_COMPILER
320	 		sleep(1);
321#else
322#if MSDOS_COMPILER==WIN32C
323			Sleep(1000);
324#endif
325#endif
326			slept = TRUE;
327
328#if HAVE_STAT_INO
329			if (follow_mode == FOLLOW_NAME)
330			{
331				/* See whether the file's i-number has changed.
332				 * If so, force the file to be closed and
333				 * reopened. */
334				struct stat st;
335				int r = stat(get_filename(curr_ifile), &st);
336				if (r == 0 && (st.st_ino != curr_ino ||
337					st.st_dev != curr_dev))
338				{
339					/* screen_trashed=2 causes
340					 * make_display to reopen the file. */
341					screen_trashed = 2;
342					return (EOI);
343				}
344			}
345#endif
346		}
347		if (sigs)
348			return (EOI);
349	}
350
351    found:
352	if (ch_bufhead != bn)
353	{
354		/*
355		 * Move the buffer to the head of the buffer chain.
356		 * This orders the buffer chain, most- to least-recently used.
357		 */
358		BUF_RM(bn);
359		BUF_INS_HEAD(bn);
360
361		/*
362		 * Move to head of hash chain too.
363		 */
364		BUF_HASH_RM(bn);
365		BUF_HASH_INS(bn, h);
366	}
367
368	if (ch_offset >= bp->datasize)
369		/*
370		 * After all that, we still don't have enough data.
371		 * Go back and try again.
372		 */
373		goto read_more;
374
375	return (bp->data[ch_offset]);
376}
377
378/*
379 * ch_ungetchar is a rather kludgy and limited way to push
380 * a single char onto an input file descriptor.
381 */
382	public void
383ch_ungetchar(c)
384	int c;
385{
386	if (c != -1 && ch_ungotchar != -1)
387		error("ch_ungetchar overrun", NULL_PARG);
388	ch_ungotchar = c;
389}
390
391#if LOGFILE
392/*
393 * Close the logfile.
394 * If we haven't read all of standard input into it, do that now.
395 */
396	public void
397end_logfile()
398{
399	static int tried = FALSE;
400
401	if (logfile < 0)
402		return;
403	if (!tried && ch_fsize == NULL_POSITION)
404	{
405		tried = TRUE;
406		ierror("Finishing logfile", NULL_PARG);
407		while (ch_forw_get() != EOI)
408			if (ABORT_SIGS())
409				break;
410	}
411	close(logfile);
412	logfile = -1;
413	namelogfile = NULL;
414}
415
416/*
417 * Start a log file AFTER less has already been running.
418 * Invoked from the - command; see toggle_option().
419 * Write all the existing buffered data to the log file.
420 */
421	public void
422sync_logfile()
423{
424	register struct buf *bp;
425	register struct bufnode *bn;
426	int warned = FALSE;
427	BLOCKNUM block;
428	BLOCKNUM nblocks;
429
430	nblocks = (ch_fpos + LBUFSIZE - 1) / LBUFSIZE;
431	for (block = 0;  block < nblocks;  block++)
432	{
433		int wrote = FALSE;
434		FOR_BUFS(bn)
435		{
436			bp = bufnode_buf(bn);
437			if (bp->block == block)
438			{
439				write(logfile, (char *) bp->data, bp->datasize);
440				wrote = TRUE;
441				break;
442			}
443		}
444		if (!wrote && !warned)
445		{
446			error("Warning: log file is incomplete",
447				NULL_PARG);
448			warned = TRUE;
449		}
450	}
451}
452
453#endif
454
455/*
456 * Determine if a specific block is currently in one of the buffers.
457 */
458	static int
459buffered(block)
460	BLOCKNUM block;
461{
462	register struct buf *bp;
463	register struct bufnode *bn;
464	register int h;
465
466	h = BUFHASH(block);
467	FOR_BUFS_IN_CHAIN(h, bn)
468	{
469		bp = bufnode_buf(bn);
470		if (bp->block == block)
471			return (TRUE);
472	}
473	return (FALSE);
474}
475
476/*
477 * Seek to a specified position in the file.
478 * Return 0 if successful, non-zero if can't seek there.
479 */
480	public int
481ch_seek(pos)
482	register POSITION pos;
483{
484	BLOCKNUM new_block;
485	POSITION len;
486
487	if (thisfile == NULL)
488		return (0);
489
490	len = ch_length();
491	if (pos < ch_zero() || (len != NULL_POSITION && pos > len))
492		return (1);
493
494	new_block = pos / LBUFSIZE;
495	if (!(ch_flags & CH_CANSEEK) && pos != ch_fpos && !buffered(new_block))
496	{
497		if (ch_fpos > pos)
498			return (1);
499		while (ch_fpos < pos)
500		{
501			if (ch_forw_get() == EOI)
502				return (1);
503			if (ABORT_SIGS())
504				return (1);
505		}
506		return (0);
507	}
508	/*
509	 * Set read pointer.
510	 */
511	ch_block = new_block;
512	ch_offset = pos % LBUFSIZE;
513	return (0);
514}
515
516/*
517 * Seek to the end of the file.
518 */
519	public int
520ch_end_seek()
521{
522	POSITION len;
523
524	if (thisfile == NULL)
525		return (0);
526
527	if (ch_flags & CH_CANSEEK)
528		ch_fsize = filesize(ch_file);
529
530	len = ch_length();
531	if (len != NULL_POSITION)
532		return (ch_seek(len));
533
534	/*
535	 * Do it the slow way: read till end of data.
536	 */
537	while (ch_forw_get() != EOI)
538		if (ABORT_SIGS())
539			return (1);
540	return (0);
541}
542
543/*
544 * Seek to the beginning of the file, or as close to it as we can get.
545 * We may not be able to seek there if input is a pipe and the
546 * beginning of the pipe is no longer buffered.
547 */
548	public int
549ch_beg_seek()
550{
551	register struct bufnode *bn;
552	register struct bufnode *firstbn;
553
554	/*
555	 * Try a plain ch_seek first.
556	 */
557	if (ch_seek(ch_zero()) == 0)
558		return (0);
559
560	/*
561	 * Can't get to position 0.
562	 * Look thru the buffers for the one closest to position 0.
563	 */
564	firstbn = ch_bufhead;
565	if (firstbn == END_OF_CHAIN)
566		return (1);
567	FOR_BUFS(bn)
568	{
569		if (bufnode_buf(bn)->block < bufnode_buf(firstbn)->block)
570			firstbn = bn;
571	}
572	ch_block = bufnode_buf(firstbn)->block;
573	ch_offset = 0;
574	return (0);
575}
576
577/*
578 * Return the length of the file, if known.
579 */
580	public POSITION
581ch_length()
582{
583	if (thisfile == NULL)
584		return (NULL_POSITION);
585	if (ignore_eoi)
586		return (NULL_POSITION);
587	if (ch_flags & CH_HELPFILE)
588		return (size_helpdata);
589	return (ch_fsize);
590}
591
592/*
593 * Return the current position in the file.
594 */
595	public POSITION
596ch_tell()
597{
598	if (thisfile == NULL)
599		return (NULL_POSITION);
600	return (ch_block * LBUFSIZE) + ch_offset;
601}
602
603/*
604 * Get the current char and post-increment the read pointer.
605 */
606	public int
607ch_forw_get()
608{
609	register int c;
610
611	if (thisfile == NULL)
612		return (EOI);
613	c = ch_get();
614	if (c == EOI)
615		return (EOI);
616	if (ch_offset < LBUFSIZE-1)
617		ch_offset++;
618	else
619	{
620		ch_block ++;
621		ch_offset = 0;
622	}
623	return (c);
624}
625
626/*
627 * Pre-decrement the read pointer and get the new current char.
628 */
629	public int
630ch_back_get()
631{
632	if (thisfile == NULL)
633		return (EOI);
634	if (ch_offset > 0)
635		ch_offset --;
636	else
637	{
638		if (ch_block <= 0)
639			return (EOI);
640		if (!(ch_flags & CH_CANSEEK) && !buffered(ch_block-1))
641			return (EOI);
642		ch_block--;
643		ch_offset = LBUFSIZE-1;
644	}
645	return (ch_get());
646}
647
648/*
649 * Set max amount of buffer space.
650 * bufspace is in units of 1024 bytes.  -1 mean no limit.
651 */
652	public void
653ch_setbufspace(bufspace)
654	int bufspace;
655{
656	if (bufspace < 0)
657		maxbufs = -1;
658	else
659	{
660		maxbufs = ((bufspace * 1024) + LBUFSIZE-1) / LBUFSIZE;
661		if (maxbufs < 1)
662			maxbufs = 1;
663	}
664}
665
666/*
667 * Flush (discard) any saved file state, including buffer contents.
668 */
669	public void
670ch_flush()
671{
672	register struct bufnode *bn;
673
674	if (thisfile == NULL)
675		return;
676
677	if (!(ch_flags & CH_CANSEEK))
678	{
679		/*
680		 * If input is a pipe, we don't flush buffer contents,
681		 * since the contents can't be recovered.
682		 */
683		ch_fsize = NULL_POSITION;
684		return;
685	}
686
687	/*
688	 * Initialize all the buffers.
689	 */
690	FOR_BUFS(bn)
691	{
692		bufnode_buf(bn)->block = -1;
693	}
694
695	/*
696	 * Figure out the size of the file, if we can.
697	 */
698	ch_fsize = filesize(ch_file);
699
700	/*
701	 * Seek to a known position: the beginning of the file.
702	 */
703	ch_fpos = 0;
704	ch_block = 0; /* ch_fpos / LBUFSIZE; */
705	ch_offset = 0; /* ch_fpos % LBUFSIZE; */
706
707#if 1
708	/*
709	 * This is a kludge to workaround a Linux kernel bug: files in
710	 * /proc have a size of 0 according to fstat() but have readable
711	 * data.  They are sometimes, but not always, seekable.
712	 * Force them to be non-seekable here.
713	 */
714	if (ch_fsize == 0)
715	{
716		ch_fsize = NULL_POSITION;
717		ch_flags &= ~CH_CANSEEK;
718	}
719#endif
720
721	if (lseek(ch_file, (off_t)0, SEEK_SET) == BAD_LSEEK)
722	{
723		/*
724		 * Warning only; even if the seek fails for some reason,
725		 * there's a good chance we're at the beginning anyway.
726		 * {{ I think this is bogus reasoning. }}
727		 */
728		error("seek error to 0", NULL_PARG);
729	}
730}
731
732/*
733 * Allocate a new buffer.
734 * The buffer is added to the tail of the buffer chain.
735 */
736	static int
737ch_addbuf()
738{
739	register struct buf *bp;
740	register struct bufnode *bn;
741
742	/*
743	 * Allocate and initialize a new buffer and link it
744	 * onto the tail of the buffer list.
745	 */
746	bp = (struct buf *) calloc(1, sizeof(struct buf));
747	if (bp == NULL)
748		return (1);
749	ch_nbufs++;
750	bp->block = -1;
751	bn = &bp->node;
752
753	BUF_INS_TAIL(bn);
754	BUF_HASH_INS(bn, 0);
755	return (0);
756}
757
758/*
759 *
760 */
761	static void
762init_hashtbl()
763{
764	register int h;
765
766	for (h = 0;  h < BUFHASH_SIZE;  h++)
767	{
768		thisfile->hashtbl[h].hnext = END_OF_HCHAIN(h);
769		thisfile->hashtbl[h].hprev = END_OF_HCHAIN(h);
770	}
771}
772
773/*
774 * Delete all buffers for this file.
775 */
776	static void
777ch_delbufs()
778{
779	register struct bufnode *bn;
780
781	while (ch_bufhead != END_OF_CHAIN)
782	{
783		bn = ch_bufhead;
784		BUF_RM(bn);
785		free(bufnode_buf(bn));
786	}
787	ch_nbufs = 0;
788	init_hashtbl();
789}
790
791/*
792 * Is it possible to seek on a file descriptor?
793 */
794	public int
795seekable(f)
796	int f;
797{
798#if MSDOS_COMPILER
799	extern int fd0;
800	if (f == fd0 && !isatty(fd0))
801	{
802		/*
803		 * In MS-DOS, pipes are seekable.  Check for
804		 * standard input, and pretend it is not seekable.
805		 */
806		return (0);
807	}
808#endif
809	return (lseek(f, (off_t)1, SEEK_SET) != BAD_LSEEK);
810}
811
812/*
813 * Initialize file state for a new file.
814 */
815	public void
816ch_init(f, flags)
817	int f;
818	int flags;
819{
820	/*
821	 * See if we already have a filestate for this file.
822	 */
823	thisfile = (struct filestate *) get_filestate(curr_ifile);
824	if (thisfile == NULL)
825	{
826		/*
827		 * Allocate and initialize a new filestate.
828		 */
829		thisfile = (struct filestate *)
830				calloc(1, sizeof(struct filestate));
831		thisfile->buflist.next = thisfile->buflist.prev = END_OF_CHAIN;
832		thisfile->nbufs = 0;
833		thisfile->flags = 0;
834		thisfile->fpos = 0;
835		thisfile->block = 0;
836		thisfile->offset = 0;
837		thisfile->file = -1;
838		thisfile->fsize = NULL_POSITION;
839		ch_flags = flags;
840		init_hashtbl();
841		/*
842		 * Try to seek; set CH_CANSEEK if it works.
843		 */
844		if ((flags & CH_CANSEEK) && !seekable(f))
845			ch_flags &= ~CH_CANSEEK;
846		set_filestate(curr_ifile, (void *) thisfile);
847	}
848	if (thisfile->file == -1)
849		thisfile->file = f;
850	ch_flush();
851}
852
853/*
854 * Close a filestate.
855 */
856	public void
857ch_close()
858{
859	int keepstate = FALSE;
860
861	if (thisfile == NULL)
862		return;
863
864	if (ch_flags & (CH_CANSEEK|CH_POPENED|CH_HELPFILE))
865	{
866		/*
867		 * We can seek or re-open, so we don't need to keep buffers.
868		 */
869		ch_delbufs();
870	} else
871		keepstate = TRUE;
872	if (!(ch_flags & CH_KEEPOPEN))
873	{
874		/*
875		 * We don't need to keep the file descriptor open
876		 * (because we can re-open it.)
877		 * But don't really close it if it was opened via popen(),
878		 * because pclose() wants to close it.
879		 */
880		if (!(ch_flags & (CH_POPENED|CH_HELPFILE)))
881			close(ch_file);
882		ch_file = -1;
883	} else
884		keepstate = TRUE;
885	if (!keepstate)
886	{
887		/*
888		 * We don't even need to keep the filestate structure.
889		 */
890		free(thisfile);
891		thisfile = NULL;
892		set_filestate(curr_ifile, (void *) NULL);
893	}
894}
895
896/*
897 * Return ch_flags for the current file.
898 */
899	public int
900ch_getflags()
901{
902	if (thisfile == NULL)
903		return (0);
904	return (ch_flags);
905}
906
907#if 0
908	public void
909ch_dump(struct filestate *fs)
910{
911	struct buf *bp;
912	struct bufnode *bn;
913	unsigned char *s;
914
915	if (fs == NULL)
916	{
917		printf(" --no filestate\n");
918		return;
919	}
920	printf(" file %d, flags %x, fpos %x, fsize %x, blk/off %x/%x\n",
921		fs->file, fs->flags, fs->fpos,
922		fs->fsize, fs->block, fs->offset);
923	printf(" %d bufs:\n", fs->nbufs);
924	for (bn = fs->next; bn != &fs->buflist;  bn = bn->next)
925	{
926		bp = bufnode_buf(bn);
927		printf("%x: blk %x, size %x \"",
928			bp, bp->block, bp->datasize);
929		for (s = bp->data;  s < bp->data + 30;  s++)
930			if (*s >= ' ' && *s < 0x7F)
931				printf("%c", *s);
932			else
933				printf(".");
934		printf("\"\n");
935	}
936}
937#endif
938