/*
	seekdir -- reposition a directory stream

	last edit:	24-May-1987	D A Gwyn

	An unsuccessful seekdir() will in general alter the current
	directory position; beware.

	NOTE:	4.nBSD directory compaction makes seekdir() & telldir()
		practically impossible to do right.  Avoid using them!
*/

#include	<errno.h>
#include	<sys/errno.h>
#include	<sys/types.h>
#include	<dirent.h>

extern off_t _lseek(int d, int offset, int whence);

#ifndef NULL
#define	NULL	0
#endif

#ifndef SEEK_SET
#define	SEEK_SET	0
#endif

typedef int	bool;			/* Boolean data type */
#define	false	0
#define	true	1

void
seekdir(register DIR *dirp, register off_t loc)
/* loc == position from telldir() */
{
	register bool	rewind;		/* "start over when stymied" flag */

	if ( dirp == NULL || dirp->dd_buf == NULL )
		{
		errno = EFAULT;
		return;			/* invalid pointer */
		}

	/* A (struct dirent)'s d_off is an invented quantity on 4.nBSD
	   NFS-supporting systems, so it is not safe to lseek() to it. */

	/* Monotonicity of d_off is heavily exploited in the following. */

	/* This algorithm is tuned for modest directory sizes.  For
	   huge directories, it might be more efficient to read blocks
	   until the first d_off is too large, then back up one block,
	   or even to use binary search on the directory blocks.  I
	   doubt that the extra code for that would be worthwhile. */

	if ( dirp->dd_loc >= dirp->dd_size	/* invalid index */
	  || ((struct dirent *)&dirp->dd_buf[dirp->dd_loc])->d_off > loc
					/* too far along in buffer */
	   )
		dirp->dd_loc = 0;	/* reset to beginning of buffer */
	/* else save time by starting at current dirp->dd_loc */

	for ( rewind = true; ; )
		{
		register struct dirent	*dp;

		/* See whether the matching entry is in the current buffer. */

		if ( (dirp->dd_loc < dirp->dd_size	/* valid index */
		   || readdir( dirp ) != NULL	/* next buffer read */
		   && (dirp->dd_loc = 0, true)	/* beginning of buffer set */
		     )
		  && (dp = (struct dirent *)&dirp->dd_buf[dirp->dd_loc])->d_off
			<= loc		/* match possible in this buffer */
		   )	{
			for ( /* dp initialized above */ ;
			      (char *)dp < &dirp->dd_buf[dirp->dd_size];
			      dp = (struct dirent *)((char *)dp + dp->d_reclen)
			    )
				if ( dp->d_off == loc )
					{	/* found it! */
					dirp->dd_loc =
						(char *)dp - dirp->dd_buf;
					return;
					}

			rewind = false;	/* no point in backing up later */
			dirp->dd_loc = dirp->dd_size;	/* set end of buffer */
			}
		else			/* whole buffer past matching entry */
			if ( !rewind )
				{	/* no point in searching further */
				errno = EINVAL;
				return;	/* no entry at specified loc */
				}
			else	{	/* rewind directory and start over */
				rewind = false;	/* but only once! */

				dirp->dd_loc = dirp->dd_size = 0;

				if ( _lseek( dirp->dd_fd, (off_t)0, SEEK_SET )
					!= 0
				   )
					return;	/* errno already set (EBADF) */

				if ( loc == 0 )
					return; /* save time */
				}
		}
}
