AtomicFile.cpp   [plain text]


/*
 * Copyright (c) 2000-2001, 2003 Apple Computer, Inc. All Rights Reserved.
 * 
 * The contents of this file constitute Original Code as defined in and are
 * subject to the Apple Public Source License Version 1.2 (the 'License').
 * You may not use this file except in compliance with the License. Please obtain
 * a copy of the License at http://www.apple.com/publicsource and read it before
 * using this file.
 * 
 * This Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS
 * OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT
 * LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the
 * specific language governing rights and limitations under the License.
 */


#include <security_filedb/AtomicFile.h>

#include <security_utilities/devrandom.h>
#include <CommonCrypto/CommonDigest.h>
#include <security_cdsa_utilities/cssmerrors.h>
#include <Security/cssm.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <syslog.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sandbox.h>
#include <set>

#define kAtomicFileMaxBlockSize INT_MAX


//
//  AtomicFile.cpp - Description t.b.d.
//
AtomicFile::AtomicFile(const std::string &inPath) :
	mPath(inPath)
{
	pathSplit(inPath, mDir, mFile);
	
    if (mDir.length() == 0)
    {
        const char* buffer = getwd(NULL);
        mDir = buffer;
        free((void*) buffer);
    }
    
    mDir += '/';

	// determine if the path is on a local or a networked volume
	struct statfs info;
	int result = statfs(mDir.c_str(), &info);
	if (result == -1) // error on opening?
	{
		mIsLocalFileSystem = false; // revert to the old ways if we can't tell what kind of system we have
	}
	else
	{
		mIsLocalFileSystem = (info.f_flags & MNT_LOCAL) != 0;
		if (mIsLocalFileSystem)
		{
			// compute the name of the lock file for this file
			CC_SHA1_CTX ctx;
			CC_SHA1_Init(&ctx);
			CC_SHA1_Update(&ctx, (const void*) mFile.c_str(), mFile.length());
			u_int8_t digest[CC_SHA1_DIGEST_LENGTH];
			CC_SHA1_Final(digest, &ctx);

			u_int32_t hash = (digest[0] << 24) | (digest[1] << 16) | (digest[2] << 8) | digest[3];
			
			char buffer[256];
			sprintf(buffer, "%08X", hash);
			mLockFilePath = mDir + ".fl" + buffer;
		}
	}
}

AtomicFile::~AtomicFile()
{
}

// Aquire the write lock and remove the file.
void
AtomicFile::performDelete()
{
	AtomicLockedFile lock(*this);
	if (::unlink(mPath.c_str()) != 0)
	{
		int error = errno;
		secdebug("atomicfile", "unlink %s: %s", mPath.c_str(), strerror(error));
        if (error == ENOENT)
			CssmError::throwMe(CSSMERR_DL_DATASTORE_DOESNOT_EXIST);
		else
			UnixError::throwMe(error);
	}

	// unlink our lock file
	::unlink(mLockFilePath.c_str());
}

// Aquire the write lock and rename the file (and bump the version and stuff).
void
AtomicFile::rename(const std::string &inNewPath)
{
	const char *path = mPath.c_str();
	const char *newPath = inNewPath.c_str();

	// @@@ lock the destination file too.
	AtomicLockedFile lock(*this);
	if (::rename(path, newPath) != 0)
	{
		int error = errno;
		secdebug("atomicfile", "rename(%s, %s): %s", path, newPath, strerror(error));
		UnixError::throwMe(error);
	}
}

// Lock the file for writing and return a newly created AtomicTempFile.
RefPointer<AtomicTempFile>
AtomicFile::create(mode_t mode)
{
	const char *path = mPath.c_str();

	// First make sure the directory to this file exists and is writable
	mkpath(mDir);

	RefPointer<AtomicLockedFile> lock(new AtomicLockedFile(*this));
	int fileRef = ropen(path, O_WRONLY|O_CREAT|O_EXCL, mode);
    if (fileRef == -1)
    {
        int error = errno;
		secdebug("atomicfile", "open %s: %s", path, strerror(error));

        // Do the obvious error code translations here.
		// @@@ Consider moving these up a level.
        if (error == EACCES)
			CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED);
        else if (error == EEXIST)
			CssmError::throwMe(CSSMERR_DL_DATASTORE_ALREADY_EXISTS);
		else
			UnixError::throwMe(error);
    }
	rclose(fileRef);

	try
	{
		// Now that we have created the lock and the new db file create a tempfile
		// object.
		RefPointer<AtomicTempFile> temp(new AtomicTempFile(*this, lock, mode));
		secdebug("atomicfile", "%p created %s", this, path);
		return temp;
	}
	catch (...)
	{
		// Creating the temp file failed so remove the db file we just created too.
		if (::unlink(path) == -1)
		{
			secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
		}
		throw;
	}
}

// Lock the database file for writing and return a newly created AtomicTempFile.
// If the parent directory allows the write we're going to allow this.  Previous
// versions checked for writability of the db file and that caused problems when
// setuid programs had made entries.  As long as the db (keychain) file is readable
// this function can make the newer keychain file with the correct owner just by virtue
// of the copy that takes place.

RefPointer<AtomicTempFile>
AtomicFile::write()
{

	RefPointer<AtomicLockedFile> lock(new AtomicLockedFile(*this));
	return new AtomicTempFile(*this, lock);
}

// Return a bufferedFile containing current version of the file for reading.
RefPointer<AtomicBufferedFile>
AtomicFile::read()
{
	return new AtomicBufferedFile(mPath, mIsLocalFileSystem);
}

mode_t
AtomicFile::mode() const
{
	const char *path = mPath.c_str();
	struct stat st;
	if (::stat(path, &st) == -1)
	{
		int error = errno;
		secdebug("atomicfile", "stat %s: %s", path, strerror(error));
		UnixError::throwMe(error);
	}
	return st.st_mode;
}

// Split full into a dir and file component.
void
AtomicFile::pathSplit(const std::string &inFull, std::string &outDir, std::string &outFile)
{
	std::string::size_type slash, len = inFull.size();
	slash = inFull.rfind('/');
	if (slash == std::string::npos)
	{
		outDir = "";
		outFile = inFull;
	}
	else if (slash + 1 == len)
	{
		outDir = inFull;
		outFile = "";
	}
	else
	{
		outDir = inFull.substr(0, slash + 1);
		outFile = inFull.substr(slash + 1, len);
	}
}

static std::string RemoveDoubleSlashes(const std::string &path)
{
	std::string result;
	unsigned i;
	for (i = 0; i < path.length(); ++i)
	{
		result += path[i];
		if ((i < path.length() - 2) && path[i] == '/' && path[i + 1] == '/')
		{
			i += 1; // skip a second '/'
		}
	}
	
	return result;
}



//
// Make sure the directory up to inDir exists inDir *must* end in a slash.
//
void
AtomicFile::mkpath(const std::string &inDir, mode_t mode)
{
	for (std::string::size_type pos = 0; (pos = inDir.find('/', pos + 1)) != std::string::npos;)
	{
		std::string path = inDir.substr(0, pos);
		const char *cpath = path.c_str();
		struct stat sb;
		if (::stat(cpath, &sb))
		{
			// if we are creating a path in the user's home directory, override the user's mode
			std::string homedir = getenv("HOME");
			
			// canonicalize the path (remove double slashes)
			string canonPath = RemoveDoubleSlashes(cpath);
			
			if (canonPath.find(homedir, 0) == 0)
			{
				mode = 0700;
			}
			
			if (errno != ENOENT || ::mkdir(cpath, mode))
				UnixError::throwMe(errno);
		}
		else if (!S_ISDIR(sb.st_mode))
			CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED);  // @@@ Should be is a directory
	}
}

int
AtomicFile::ropen(const char *const name, int flags, mode_t mode)
{
    bool isCreate = (flags & O_CREAT) != 0;
    
    bool checkForRead = false;
    bool checkForWrite = false;
    
    // if we are actually trying to create the file, we
    int fd, tries_left = 4 /* kNoResRetry */;

    if (!isCreate)
    {
        switch (flags & O_ACCMODE) 
        {
            case O_RDONLY:
                checkForRead = true;
                break;
            case O_WRONLY:
                checkForWrite = true;
                break;
            case O_RDWR:
                checkForRead = true;
                checkForWrite = true;
                break;
        }

        if (checkForRead)
        {
            int result = sandbox_check(getpid(), "file-read-data", SANDBOX_FILTER_PATH, name);
            if (result != 0)
            {
                return -1;
            }
        }
        
        if (checkForWrite)
        {
            int result = sandbox_check(getpid(), "file-write-data", SANDBOX_FILTER_PATH, name);
            if (result != 0)
            {
                return -1;
            }
        }
    }

	do
	{
		fd = ::open(name, flags, mode);
	} while (fd < 0 && (errno == EINTR || (errno == ENFILE && --tries_left >= 0)));

	return fd;
}

int
AtomicFile::rclose(int fd)
{
	int result;
	do
	{
		result = ::close(fd);
	} while(result && errno == EINTR);

	return result;
}

//
// AtomicBufferedFile - This represents an instance of a file opened for reading.
// The file is read into memory and closed after this is done.
// The memory is released when this object is destroyed.
//
AtomicBufferedFile::AtomicBufferedFile(const std::string &inPath, bool isLocal) :
	mPath(inPath),
	mFileRef(-1),
	mBuffer(NULL),
	mLength(0),
	mIsMapped(isLocal)
{
}

AtomicBufferedFile::~AtomicBufferedFile()
{
	if (mFileRef >= 0)
	{
		AtomicFile::rclose(mFileRef);
		secdebug("atomicfile", "%p closed %s", this, mPath.c_str());
	}

	if (mBuffer)
	{
		secdebug("atomicfile", "%p free %s buffer %p", this, mPath.c_str(), mBuffer);
		unloadBuffer();
	}
}

//
// Open the file and return the length in bytes.
//
off_t
AtomicBufferedFile::open()
{
	const char *path = mPath.c_str();
	if (mFileRef >= 0)
	{
		secdebug("atomicfile", "open %s: already open, closing and reopening", path);
		close();
	}

	mFileRef = AtomicFile::ropen(path, O_RDONLY, 0);
    if (mFileRef == -1)
    {
        int error = errno;
		secdebug("atomicfile", "open %s: %s", path, strerror(error));

        // Do the obvious error code translations here.
		// @@@ Consider moving these up a level.
        if (error == ENOENT)
			CssmError::throwMe(CSSMERR_DL_DATASTORE_DOESNOT_EXIST);
		else if (error == EACCES)
			CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED);
		else
			UnixError::throwMe(error);
    }

	struct stat st;
	int result = fstat(mFileRef, &st);
	if (result == 0)
	{
		mLength = st.st_size;
	}
	else
	{
		int error = errno;
		secdebug("atomicfile", "lseek(%s, END): %s", path, strerror(error));
		AtomicFile::rclose(mFileRef);
		UnixError::throwMe(error);
	}

	secdebug("atomicfile", "%p opened %s: %qd bytes", this, path, mLength);

	return mLength;
}

//
// Unload the contents of the file.
//
void
AtomicBufferedFile::unloadBuffer()
{
	if (!mIsMapped)
	{
		delete [] mBuffer;
	}
	else
	{
		munmap(mBuffer, mLength);
	}
}

//
// Load the contents of the file into memory.
// If we are on a local file system, we mmap the file.  Otherwise, we
// read it all into memory
void
AtomicBufferedFile::loadBuffer()
{
	if (!mIsMapped)
	{
		// make a buffer big enough to hold the entire file
		mBuffer = new uint8[mLength];
		lseek(mFileRef, 0, SEEK_SET);
		ssize_t pos = 0;
		
		ssize_t bytesToRead = mLength;
		while (bytesToRead > 0)
		{
			ssize_t bytesRead = ::read(mFileRef, mBuffer + pos, bytesToRead);
			if (bytesRead == -1)
			{
				if (errno != EINTR)
				{
					int error = errno;
					secdebug("atomicfile", "lseek(%s, END): %s", mPath.c_str(), strerror(error));
					AtomicFile::rclose(mFileRef);
					UnixError::throwMe(error);
				}
			}
			else
			{
				bytesToRead -= bytesRead;
				pos += bytesRead;
			}
		}
	}
	else
	{
		// mmap the buffer into place
		mBuffer = (uint8*) mmap(NULL, mLength, PROT_READ, MAP_PRIVATE, mFileRef, 0);
		if (mBuffer == (uint8*) -1)
		{
			int error = errno;
			secdebug("atomicfile", "lseek(%s, END): %s", mPath.c_str(), strerror(error));
			AtomicFile::rclose(mFileRef);
			UnixError::throwMe(error);
		}
	}
}



//
// Read the file starting at inOffset for inLength bytes into the buffer and return
// a pointer to it.  On return outLength contain the actual number of bytes read, it
// will only ever be less than inLength if EOF was reached, and it will never be more
// than inLength.
//
const uint8 *
AtomicBufferedFile::read(off_t inOffset, off_t inLength, off_t &outLength)
{
	if (mFileRef < 0)
	{
		secdebug("atomicfile", "read %s: file yet not opened, opening", mPath.c_str());
		open();
	}

	off_t bytesLeft = inLength;
	if (mBuffer)
	{
		secdebug("atomicfile", "%p free %s buffer %p", this, mPath.c_str(), mBuffer);
		unloadBuffer();
	}

	loadBuffer();
	
	secdebug("atomicfile", "%p allocated %s buffer %p size %qd", this, mPath.c_str(), mBuffer, bytesLeft);
	
	ssize_t maxEnd = inOffset + inLength;
	if (maxEnd > mLength)
	{
		maxEnd = mLength;
	}
	
	outLength = maxEnd - inOffset;
	
	return mBuffer + inOffset;
}

void
AtomicBufferedFile::close()
{
	if (mFileRef < 0)
	{
		secdebug("atomicfile", "close %s: already closed", mPath.c_str());
	}
	else
	{
		int result = AtomicFile::rclose(mFileRef);
		mFileRef = -1;
		if (result == -1)
		{
			int error = errno;
			secdebug("atomicfile", "close %s: %s", mPath.c_str(), strerror(errno));
			UnixError::throwMe(error);
		}

		secdebug("atomicfile", "%p closed %s", this, mPath.c_str());
	}
}


//
// AtomicTempFile - A temporary file to write changes to.
//
AtomicTempFile::AtomicTempFile(AtomicFile &inFile, const RefPointer<AtomicLockedFile> &inLockedFile, mode_t mode) :
	mFile(inFile),
	mLockedFile(inLockedFile),
	mCreating(true)
{
	create(mode);
}

AtomicTempFile::AtomicTempFile(AtomicFile &inFile, const RefPointer<AtomicLockedFile> &inLockedFile) :
	mFile(inFile),
	mLockedFile(inLockedFile),
	mCreating(false)
{
	create(mFile.mode());
}

AtomicTempFile::~AtomicTempFile()
{
	// rollback if we didn't commit yet.
	if (mFileRef >= 0)
		rollback();
}

//
// Open the file and return the length in bytes.
//
void
AtomicTempFile::create(mode_t mode)
{
	// we now generate our temporary file name through sandbox API's.
    
    // put the dir into a canonical form
    string dir = mFile.dir();
    int i = dir.length() - 1;
    
    // walk backwards until we get to a non / character
    while (i >= 0 && dir[i] == '/')
    {
        i -= 1;
    }
    
    // point one beyond the string
    i += 1;
    
    const char* temp = _amkrtemp((dir.substr(0, i) + "/" + mFile.file()).c_str());
    if (temp == NULL)
    {
        UnixError::throwMe(errno);
    }
    
	mPath = temp;
    free((void*) temp);
    
	const char *path = mPath.c_str();

	mFileRef = AtomicFile::ropen(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
    if (mFileRef == -1)
    {
        int error = errno;
		secdebug("atomicfile", "open %s: %s", path, strerror(error));

        // Do the obvious error code translations here.
		// @@@ Consider moving these up a level.
        if (error == EACCES)
			CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED);
		else
			UnixError::throwMe(error);
    }

	// If we aren't creating the inital file, make sure we preserve
	// the mode of the old file regardless of the current umask.
	// If we are creating the inital file we respect the users
	// current umask.
	if (!mCreating)
	{
		if (::fchmod(mFileRef, mode))
		{
			int error = errno;
			secdebug("atomicfile", "fchmod %s: %s", path, strerror(error));
			UnixError::throwMe(error);
		}
	}

	secdebug("atomicfile", "%p created %s", this, path);
}

void
AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset, const uint32 inData)
{
    uint32 aData = htonl(inData);
    write(inOffsetType, inOffset, reinterpret_cast<uint8 *>(&aData), sizeof(aData));
}

void
AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset,
				  const uint32 *inData, uint32 inCount)
{
#ifdef HOST_LONG_IS_NETWORK_LONG
    // Optimize this for the case where hl == nl
    const uint32 *aBuffer = inData;
#else
    auto_array<uint32> aBuffer(inCount);
    for (uint32 i = 0; i < inCount; i++)
        aBuffer.get()[i] = htonl(inData[i]);
#endif

    write(inOffsetType, inOffset, reinterpret_cast<const uint8 *>(aBuffer.get()),
    	  inCount * sizeof(*inData));
}

void
AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset, const uint8 *inData, size_t inLength)
{
	off_t pos;
	if (inOffsetType == AtomicFile::FromEnd)
	{
		pos = ::lseek(mFileRef, 0, SEEK_END);
		if (pos == -1)
		{
			int error = errno;
			secdebug("atomicfile", "lseek(%s, %qd): %s", mPath.c_str(), inOffset, strerror(error));
			UnixError::throwMe(error);
		}
	}
	else if (inOffsetType == AtomicFile::FromStart)
		pos = inOffset;
	else
		CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR);

	off_t bytesLeft = inLength;
	const uint8 *ptr = inData;
	while (bytesLeft)
	{
		size_t toWrite = bytesLeft > kAtomicFileMaxBlockSize ? kAtomicFileMaxBlockSize : size_t(bytesLeft);
		ssize_t bytesWritten = ::pwrite(mFileRef, ptr, toWrite, pos);
		if (bytesWritten == -1)
		{
			int error = errno;
			if (error == EINTR)
			{
				// We got interrupted by a signal, so try again.
				secdebug("atomicfile", "write %s: interrupted, retrying", mPath.c_str());
				continue;
			}

			secdebug("atomicfile", "write %s: %s", mPath.c_str(), strerror(error));
			UnixError::throwMe(error);
		}

		// Write returning 0 is bad mmkay.
		if (bytesWritten == 0)
		{
			secdebug("atomicfile", "write %s: 0 bytes written", mPath.c_str());
			CssmError::throwMe(CSSMERR_DL_INTERNAL_ERROR);
		}

		secdebug("atomicfile", "%p wrote %s %ld bytes from %p", this, mPath.c_str(), bytesWritten, ptr);

		bytesLeft -= bytesWritten;
		ptr += bytesWritten;
		pos += bytesWritten;
	}
}

void
AtomicTempFile::fsync()
{
	if (mFileRef < 0)
	{
		secdebug("atomicfile", "fsync %s: already closed", mPath.c_str());
	}
	else
	{
		int result;
		do
		{
			result = ::fsync(mFileRef);
		} while (result && errno == EINTR);

		if (result == -1)
		{
			int error = errno;
			secdebug("atomicfile", "fsync %s: %s", mPath.c_str(), strerror(errno));
			UnixError::throwMe(error);
		}

		secdebug("atomicfile", "%p fsynced %s", this, mPath.c_str());
	}
}

void
AtomicTempFile::close()
{
	if (mFileRef < 0)
	{
		secdebug("atomicfile", "close %s: already closed", mPath.c_str());
	}
	else
	{
		int result = AtomicFile::rclose(mFileRef);
		mFileRef = -1;
		if (result == -1)
		{
			int error = errno;
			secdebug("atomicfile", "close %s: %s", mPath.c_str(), strerror(errno));
			UnixError::throwMe(error);
		}

		secdebug("atomicfile", "%p closed %s", this, mPath.c_str());
	}
}

// Commit the current create or write and close the write file.  Note that a throw during the commit does an automatic rollback.
void
AtomicTempFile::commit()
{
	try
	{
		fsync();
		close();
		const char *oldPath = mPath.c_str();
		const char *newPath = mFile.path().c_str();
		if (::rename(oldPath, newPath) == -1)
		{
			int error = errno;
			secdebug("atomicfile", "rename (%s, %s): %s", oldPath, newPath, strerror(errno));
			UnixError::throwMe(error);
		}

		// Unlock the lockfile
		mLockedFile = NULL;

		secdebug("atomicfile", "%p commited %s", this, oldPath);
	}
	catch (...)
	{
		rollback();
		throw;
	}
}

// Rollback the current create or write (happens automatically if commit() isn't called before the destructor is.
void
AtomicTempFile::rollback() throw()
{
	if (mFileRef >= 0)
	{
		AtomicFile::rclose(mFileRef);
		mFileRef = -1;
	}

	// @@@ Log errors if this fails.
	const char *path = mPath.c_str();
	if (::unlink(path) == -1)
	{
		secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
		// rollback can't throw
	}

	// @@@ Think about this.  Depending on how we do locking we might not need this.
	if (mCreating)
	{
		const char *path = mFile.path().c_str();
		if (::unlink(path) == -1)
		{
			secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
			// rollback can't throw
		}
	}
}


//
// An advisory write lock for inFile.
//
FileLocker::~FileLocker()
{
}



LocalFileLocker::LocalFileLocker(AtomicFile &inFile) :
	mPath(inFile.lockFileName())
{
}


LocalFileLocker::~LocalFileLocker()
{
}



#ifndef NDEBUG
static double GetTime()
{
	struct timeval t;
	gettimeofday(&t, NULL);
	return ((double) t.tv_sec) + ((double) t.tv_usec) / 1000000.0;
}
#endif



void
LocalFileLocker::lock(mode_t mode)
{
	struct stat st;

	do
	{
		// if the lock file doesn't exist, create it
		mLockFile = open(mPath.c_str(), O_RDONLY | O_CREAT, mode);
		
		// if we can't open or create the file, something is wrong
		if (mLockFile == -1)
		{
			UnixError::throwMe(errno);
		}
		
		// try to get exclusive access to the file
		IFDEBUG(double startTime = GetTime());
		int result = flock(mLockFile, LOCK_EX);
		IFDEBUG(double endTime = GetTime());
		
		IFDEBUG(secdebug("atomictime", "Waited %.4f milliseconds for file lock", (endTime - startTime) * 1000.0));
		
		// errors at this point are bad
		if (result == -1)
		{
			UnixError::throwMe(errno);
		}
		
		// check and see if the file we have access to still exists.  If not, another file shared our file lock
		// due to a hash collision and has thrown our lock away -- that, or a user blew the lock file away himself.
		
		result = fstat(mLockFile, &st);
		
		// errors at this point are bad
		if (result == -1)
		{
			UnixError::throwMe(errno);
		}
		
		if (st.st_nlink == 0) // we've been unlinked!
		{
			close(mLockFile);
		}
	} while (st.st_nlink == 0);
}


void
LocalFileLocker::unlock()
{
	flock(mLockFile, LOCK_UN);
	close(mLockFile);
}


	
NetworkFileLocker::NetworkFileLocker(AtomicFile &inFile) :
	mDir(inFile.dir()),
	mPath(inFile.dir() + "lck~" + inFile.file())
{
}

NetworkFileLocker::~NetworkFileLocker()
{
}

std::string
NetworkFileLocker::unique(mode_t mode)
{
	static const int randomPart = 16;
	DevRandomGenerator randomGen;
	std::string::size_type dirSize = mDir.size();
	std::string fullname(dirSize + randomPart + 2, '\0');
	fullname.replace(0, dirSize, mDir);
	fullname[dirSize] = '~'; /* UNIQ_PREFIX */
	char buf[randomPart];
	struct stat filebuf;
	int result, fd = -1;

	for (int retries = 0; retries < 10; ++retries)
	{
		/* Make a random filename. */
		randomGen.random(buf, randomPart);
		for (int ix = 0; ix < randomPart; ++ix)
		{
			char ch = buf[ix] & 0x3f;
			fullname[ix + dirSize + 1] = ch +
				( ch < 26            ? 'A'
				: ch < 26 + 26       ? 'a' - 26
				: ch < 26 + 26 + 10  ? '0' - 26 - 26
				: ch == 26 + 26 + 10 ? '-' - 26 - 26 - 10
				:                      '_' - 26 - 26 - 11);
		}

		result = lstat(fullname.c_str(), &filebuf);
		if (result && errno == ENAMETOOLONG)
		{
			do
				fullname.erase(fullname.end() - 1);
			while((result = lstat(fullname.c_str(), &filebuf)) && errno == ENAMETOOLONG && fullname.size() > dirSize + 8);
		}       /* either it stopped being a problem or we ran out of filename */

		if (result && errno == ENOENT)
		{
			fd = AtomicFile::ropen(fullname.c_str(), O_WRONLY|O_CREAT|O_EXCL, mode);
			if (fd >= 0 || errno != EEXIST)
				break;
		}
	}

	if (fd < 0)
	{
		int error = errno;
		::syslog(LOG_ERR, "Couldn't create temp file %s: %s", fullname.c_str(), strerror(error));
		secdebug("atomicfile", "Couldn't create temp file %s: %s", fullname.c_str(), strerror(error));
		UnixError::throwMe(error);
	}

	/* @@@ Check for EINTR. */
	write(fd, "0", 1); /* pid 0, `works' across networks */

	AtomicFile::rclose(fd);

	return fullname;
}

/* Return 0 on success and 1 on failure if st is set to the result of stat(old) and -1 on failure if the stat(old) failed. */
int
NetworkFileLocker::rlink(const char *const old, const char *const newn, struct stat &sto)
{
	int result = ::link(old,newn);
	if (result)
	{
		int serrno = errno;
		if (::lstat(old, &sto) == 0)
		{
			struct stat stn;
			if (::lstat(newn, &stn) == 0
				&& sto.st_dev == stn.st_dev
				&& sto.st_ino == stn.st_ino
				&& sto.st_uid == stn.st_uid
				&& sto.st_gid == stn.st_gid
				&& !S_ISLNK(sto.st_mode))
			{
				/* Link failed but files are the same so the link really went ok. */
				return 0;
			}
			else
				result = 1;
		}
		errno = serrno; /* Restore errno from link() */
	}

	return result;
}

/* NFS-resistant rename()
 * rename with fallback for systems that don't support it
 * Note that this does not preserve the contents of the file. */
int
NetworkFileLocker::myrename(const char *const old, const char *const newn)
{
	struct stat stbuf;
	int fd = -1;
	int ret;

	/* Try a real hardlink */
	ret = rlink(old, newn, stbuf);
	if (ret > 0)
	{
		if (stbuf.st_nlink < 2 && (errno == EXDEV || errno == ENOTSUP))
		{
			/* Hard link failed so just create a new file with O_EXCL instead.  */
			fd = AtomicFile::ropen(newn, O_WRONLY|O_CREAT|O_EXCL, stbuf.st_mode);
			if (fd >= 0)
				ret = 0;
		}
	}

	/* We want the errno from the link or the ropen, not that of the unlink. */
	int serrno = errno;

	/* Unlink the temp file. */
	::unlink(old);
	if (fd > 0)
		AtomicFile::rclose(fd);

	errno = serrno;
	return ret;
}

int
NetworkFileLocker::xcreat(const char *const name, mode_t mode, time_t &tim)
{
	std::string uniqueName = unique(mode);
	const char *uniquePath = uniqueName.c_str();
	struct stat stbuf;       /* return the filesystem time to the caller */
	stat(uniquePath, &stbuf);
	tim = stbuf.st_mtime;
	return myrename(uniquePath, name);
}

void
NetworkFileLocker::lock(mode_t mode)
{
	const char *path = mPath.c_str();
	bool triedforce = false;
	struct stat stbuf;
	time_t t, locktimeout = 1024; /* DEFlocktimeout, 17 minutes. */
	bool doSyslog = false;
	bool failed = false;
	int retries = 0;

	while (!failed)
	{
		/* Don't syslog first time through. */
		if (doSyslog)
			::syslog(LOG_NOTICE, "Locking %s", path);
		else
			doSyslog = true;

		secdebug("atomicfile", "Locking %s", path);          /* in order to cater for clock skew: get */
		if (!xcreat(path, mode, t))    /* time t from the filesystem */
		{
			/* lock acquired, hurray! */
			break;
		}
		switch(errno)
		{
		case EEXIST:               /* check if it's time for a lock override */
			if (!lstat(path, &stbuf) && stbuf.st_size <= 16 /* MAX_locksize */ && locktimeout
				&& !lstat(path, &stbuf) && locktimeout < t - stbuf.st_mtime)
				/* stat() till unlink() should be atomic, but can't guarantee that. */
			{
				if (triedforce)
				{
					/* Already tried, force lock override, not trying again */
					failed = true;
					break;
				}
				else if (S_ISDIR(stbuf.st_mode) || ::unlink(path))
				{
					triedforce=true;
					::syslog(LOG_ERR, "Forced unlock denied on %s", path);
					secdebug("atomicfile", "Forced unlock denied on %s", path);
				}
				else
				{
					::syslog(LOG_ERR, "Forcing lock on %s", path);
					secdebug("atomicfile", "Forcing lock on %s", path);
					sleep(16 /* DEFsuspend */);
					break;
				}
			}
			else
				triedforce = false;              /* legitimate iteration, clear flag */

			/* Reset retry counter. */
			retries = 0;
			usleep(250000);
			break;

		case ENOSPC:               /* no space left, treat it as a transient */
#ifdef EDQUOT                                                 /* NFS failure */
		case EDQUOT:                  /* maybe it was a short term shortage? */
#endif
		case ENOENT:
		case ENOTDIR:
		case EIO:
		/*case EACCES:*/
			if(++retries < (256 + 1))  /* nfsTRY number of times+1 to ignore spurious NFS errors */
				usleep(250000);
			else
				failed = true;
			break;

#ifdef ENAMETOOLONG
		case ENAMETOOLONG:     /* Filename is too long, shorten and retry */
			if (mPath.size() > mDir.size() + 8)
			{
				secdebug("atomicfile", "Truncating %s and retrying lock", path);
				mPath.erase(mPath.end() - 1);
				path = mPath.c_str();
				/* Reset retry counter. */
				retries = 0;
				break;
			}
		/* DROPTHROUGH */
#endif
		default:
			failed = true;
			break;
		}
	}

	if (failed)
	{
		int error = errno;
		::syslog(LOG_ERR, "Lock failure on %s: %s", path, strerror(error));
		secdebug("atomicfile", "Lock failure on %s: %s", path, strerror(error));
		UnixError::throwMe(error);
	}
}

void
NetworkFileLocker::unlock()
{
	const char *path = mPath.c_str();
	if (::unlink(path) == -1)
	{
		secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
		// unlock can't throw
	}
}



AtomicLockedFile::AtomicLockedFile(AtomicFile &inFile)
{
	if (inFile.isOnLocalFileSystem())
	{
		mFileLocker = new LocalFileLocker(inFile);
	}
	else
	{
		mFileLocker = new NetworkFileLocker(inFile);
	}
	
	lock();
}



AtomicLockedFile::~AtomicLockedFile()
{
	unlock();
	delete mFileLocker;
}



void
AtomicLockedFile::lock(mode_t mode)
{
	mFileLocker->lock(mode);
}



void AtomicLockedFile::unlock() throw()
{
	mFileLocker->unlock();
}



#undef kAtomicFileMaxBlockSize