// Copyright 1997-2002 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import <OWF/OWDataStream.h>

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>
#import <OmniBase/system.h>

#import <OWF/OWContentCache.h>
#import <OWF/OWContentType.h>
#import <OWF/OWDataStreamCharacterProcessor.h>
#import <OWF/OWDataStreamCursor.h>
#import <OWF/OWParameterizedContentType.h>
#import <OWF/OWUnknownDataStreamProcessor.h>

#include <sys/mman.h>

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Content.subproj/OWDataStream.m,v 1.47 2002/03/09 01:53:49 kc Exp $")

@interface OWDataStream (Private)
- (void)flushContentsToFile;
- (void)flushAndCloseSaveFile;
- (void)_noMoreData;
@end

@implementation OWDataStream

const unsigned int OWDataStreamUnknownLength = NSNotFound;

static unsigned int DataBufferBlockSize;
#define ROUNDED_ALLOCATION_SIZE(x) (DataBufferBlockSize * ( ( (x) + DataBufferBlockSize - 1 ) / DataBufferBlockSize))

static OWContentType *unknownContentType;
static OWContentType *unencodedContentEncoding;

+ (void)initialize;
{
    OBINITIALIZE;

    unknownContentType = [OWUnknownDataStreamProcessor unknownContentType];
    unencodedContentEncoding = [OWContentType contentTypeForString:@"encoding/none"];
    DataBufferBlockSize = 2 * NSPageSize();
}


static inline OWDataStreamBufferDescriptor *descriptorForBlockContainingOffset(OWDataStream *self, unsigned int offset, unsigned int *offsetWithinBlock)
{
    OWDataStreamBufferDescriptor *cursor;
    unsigned int cursorOffset = 0;

    if (self->flags.hasThrownAwayData)
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];

    cursor = self->_first;
    while (cursor != NULL) {
        OWDataStreamBufferDescriptor cursorBlock = *cursor;
        
        if (cursorOffset <= offset && (cursorOffset + cursorBlock.bufferUsed) > offset) {
            *offsetWithinBlock = ( offset - cursorOffset );
            return cursor;
        }
        
        cursor = cursorBlock.next;
        cursorOffset += cursorBlock.bufferUsed;
    }
    
    return NULL;
}

static inline BOOL copyBuffersOut(OWDataStreamBufferDescriptor *dsBuffer, unsigned int offsetIntoBlock, void *outBuffer, unsigned int length)
{
    while (length != 0) {
        OWDataStreamBufferDescriptor dsBufferCopy;
        unsigned bytesCopied;
        
        if (!dsBuffer)
            return NO;
            
        dsBufferCopy = *dsBuffer;
        bytesCopied = MIN(length, dsBufferCopy.bufferUsed - offsetIntoBlock);
        bcopy(dsBufferCopy.buffer + offsetIntoBlock, outBuffer, bytesCopied);
        outBuffer += bytesCopied;
        length -= bytesCopied;
        
        dsBuffer = dsBufferCopy.next;
        offsetIntoBlock = 0;
    }
    
    return YES;
}

static inline void allocateAnotherBuffer(OWDataStream *self, unsigned int bytesToAllocate)
{
    OWDataStreamBufferDescriptor *newBuffer;

    // Create a new buffer descriptor & allocate its data
    newBuffer = NSZoneMalloc([self zone], sizeof(*newBuffer));
    newBuffer->bufferSize = bytesToAllocate;
    OBASSERT((newBuffer->bufferSize % DataBufferBlockSize) == 0);
    newBuffer->buffer = NSAllocateMemoryPages(newBuffer->bufferSize);
    newBuffer->bufferUsed = 0;
    newBuffer->next = NULL;
    
    // Link it into our list of descriptors.
    // This function is only ever called from the writing thread, so we don't have to worry as much about ordering of operations.
    if (self->_last) {
        self->_last->next = newBuffer;
        self->_last = newBuffer;
    } else {
        OBASSERT(!self->_first);
        self->_first = self->_last = newBuffer;
    }
    
    OBPOSTCONDITION(self->_last != NULL);
    OBPOSTCONDITION(self->_last->bufferUsed < self->_last->bufferSize);
}

- initWithLength:(unsigned int)newLength;
{
    if (![super init])
	return nil;

    dataLength = newLength;
    
    dataAvailableCondition = [[OFCondition alloc] init];
    endOfDataCondition = [[OFCondition alloc] init];

    _first = _last = NULL;

    readLength = 0;
    
    stringEncoding = kCFStringEncodingInvalidId;

    flags.endOfData = NO;
    flags.hasThrownAwayData = NO;
    flags.hasIssuedCursor = NO;
    flags.hasResetContentTypeAndEncoding = NO;
    
    saveFilename = nil;
    saveFileHandle = nil;
    saveFileHandleLock = [[NSLock alloc] init];
    
    if (dataLength != OWDataStreamUnknownLength && dataLength != 0)
        allocateAnotherBuffer(self, ROUNDED_ALLOCATION_SIZE(dataLength));

    return self;
}

- init;
{
    return [self initWithLength:OWDataStreamUnknownLength];
}

// OWAbstractContent subclass (to force inspectors to guess what we are)

- initWithName:(NSString *)name;
{
    // Normally, abstractContent gets initWithName:@"DataStream", because the init method takes the class name and creates a guess with that.  However, in this case, we don't want the guess, because we'd rather have OWPipeline's -rebuildCompositeTypeString method take a guess what to call us than to show the user the word "DataStream", which really means nothing to her.
    return [super initWithName:nil];
}

- (void)dealloc;
{
    OWDataStreamBufferDescriptor *cursor, *nextCursor;
    NSZone *myZone;
    
    OBASSERT(saveFileHandle == nil);
    [saveFileHandleLock release];
    
    myZone = [self zone];
    for (cursor = _first; cursor != NULL; cursor = nextCursor) {
        nextCursor = cursor->next;
        OBASSERT(nextCursor != nil || cursor == _last);
        NSDeallocateMemoryPages(cursor->buffer, cursor->bufferSize);
        NSZoneFree(myZone, cursor);
    }
    _first = _last = NULL;

    [dataAvailableCondition release];
    [endOfDataCondition release];
    [saveFilename release];
    [super dealloc];
}

- (id)newCursor;
{
    if (flags.hasThrownAwayData) // Short-circuit outside the lock when possible
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];
    [saveFileHandleLock lock];
    if (flags.hasThrownAwayData) {
        [saveFileHandleLock unlock];
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];
    }
    flags.hasIssuedCursor = YES;
    [saveFileHandleLock unlock];
    return [[[OWDataStreamCursor alloc] initForDataStream:self] autorelease];
}

- (NSData *)bufferedData;
{
    OWDataStreamBufferDescriptor *local_first, *local_last, *cursor, *nextCursor;
    NSMutableData *result;
    
    local_first = _first;
    local_last = _last;

    if (flags.hasThrownAwayData)
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];

    // Special cases...
    if (local_first == NULL)
        return [NSData data];
    if (local_first == local_last)
        return [NSData dataWithBytes:local_first->buffer length:local_first->bufferUsed];
        
    // General case.
    result = [[[NSMutableData alloc] initWithCapacity:readLength] autorelease];
    for (cursor = local_first; cursor != NULL; cursor = nextCursor) {
        nextCursor = cursor->next;  // look at the 'next' pointer before we look at the 'bufferUsed' pointer, in case someone adds to this block and appends a new block while we're appending to 'result'; this way we get a consistent view of the data stream
        [result appendBytes:cursor->buffer length:cursor->bufferUsed];
    }
    
    return result;
}

- (unsigned int)bufferedDataLength;
{
    if (flags.hasThrownAwayData)
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];

    return readLength;
}

- (unsigned int)accessUnderlyingBuffer:(void **)returnedBufferPtr startingAtLocation:(unsigned int)dataOffset;
{
    OWDataStreamBufferDescriptor *dsBuffer;
    unsigned int remainingOffset;

    if (flags.hasThrownAwayData)
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];
    if (readLength <= dataOffset)
        return 0;
    
    dsBuffer = descriptorForBlockContainingOffset(self, dataOffset, &remainingOffset);
    if (dsBuffer) {
        *returnedBufferPtr = dsBuffer->buffer + remainingOffset;
        return dsBuffer->bufferUsed - remainingOffset;
    }
    
    return 0;
}

- (unsigned int)dataLength;
{
    if (flags.hasThrownAwayData)
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];

    if (![self knowsDataLength])
        [endOfDataCondition waitForCondition];
    return dataLength;
}

- (BOOL)knowsDataLength;
{
    return dataLength != OWDataStreamUnknownLength;
}

- (BOOL)getBytes:(void *)buffer range:(NSRange)range;
{
    OWDataStreamBufferDescriptor *dsBuffer;
    unsigned int offsetIntoBlock;

    if (flags.hasThrownAwayData)
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];

    if (![self waitForBufferedDataLength:NSMaxRange(range)])
        return NO;

    dsBuffer = descriptorForBlockContainingOffset(self, range.location, &offsetIntoBlock);
    
    return copyBuffersOut(dsBuffer, offsetIntoBlock, buffer, range.length);
}

- (NSData *)dataWithRange:(NSRange)range;
{
    OWDataStreamBufferDescriptor *dsBuffer;
    unsigned int offsetIntoBlock;

    if (flags.hasThrownAwayData)
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];

    if (![self waitForBufferedDataLength:NSMaxRange(range)])
        return nil;

    dsBuffer = descriptorForBlockContainingOffset(self, range.location, &offsetIntoBlock);
    if (!dsBuffer)
        return nil;

    if (dsBuffer->bufferUsed - offsetIntoBlock >= range.length) {
        // Special case: the requested range lies entirely within one allocated buffer
        return [NSData dataWithBytes:dsBuffer->buffer + offsetIntoBlock length:range.length];
    } else {
        // General case: create a mutable data object and copy (partial) blocks into it
        NSMutableData *subdata = [[NSMutableData alloc] initWithLength:range.length];
        if (!copyBuffersOut(dsBuffer, offsetIntoBlock, [subdata mutableBytes], range.length)) {
            [subdata release];
            return nil;
        }
        return [subdata autorelease];
    }
}

- (BOOL)waitForMoreData;
{
    if (flags.hasThrownAwayData)
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];

    if (flags.endOfData)
	return NO;
    [dataAvailableCondition waitForCondition];
    return YES;
}

- (BOOL)waitForBufferedDataLength:(unsigned int)length;
{
    if (flags.hasThrownAwayData)
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];

    while (readLength < length)
        if (![self waitForMoreData])
            return NO;
    return YES;
}

- (void)writeData:(NSData *)newData;
{
    NSRange range;
    unsigned int length, lengthLeft;
    
    length = [newData length];
    if (length == 0)
        return;
    lengthLeft = length;
    range.location = 0;
    range.length = length;
    
    while (range.length != 0) {
        OWDataStreamBufferDescriptor *lastBuffer = _last;
        
        // Copy data into a buffer if we've already allocated one
        if (lastBuffer && lastBuffer->bufferSize > lastBuffer->bufferUsed) {
            NSRange fragment;
            
            fragment.location = range.location;
            fragment.length = MIN(lastBuffer->bufferSize - lastBuffer->bufferUsed, range.length);
            [newData getBytes:lastBuffer->buffer + lastBuffer->bufferUsed range:fragment];
            
            lastBuffer->bufferUsed += fragment.length;
            readLength += fragment.length;
            range.location += fragment.length;
            range.length -= fragment.length;
        }
        
        if (!range.length)
            break;

        // Allocate a new buffer, rounding up to an integer number of pages
        allocateAnotherBuffer(self, ROUNDED_ALLOCATION_SIZE(range.length));
    }
    
    [dataAvailableCondition broadcastCondition];
    if (saveFilename)
        [self flushContentsToFile];
}

- (void)writeString:(NSString *)string;
{
    CFStringEncoding writeEncoding;
    CFDataRef bytes;

    if (string == nil)
	return;

    writeEncoding = stringEncoding;
    if (writeEncoding == kCFStringEncodingInvalidId)
        writeEncoding = [OWDataStreamCharacterProcessor defaultStringEncoding];
        
    bytes = CFStringCreateExternalRepresentation(kCFAllocatorDefault, (CFStringRef)string, writeEncoding, 1);
    [self writeData:(NSData *)bytes];
    CFRelease(bytes);
}

- (void)writeFormat:(NSString *)formatString, ...;
{
    NSString *string;
    va_list argList;

    va_start(argList, formatString);
    string = [[NSString alloc] initWithFormat:formatString arguments:argList];
    va_end(argList);
    [self writeString:string];
    [string release];
}


- (unsigned int)appendToUnderlyingBuffer:(void **)returnedBufferPtr;
{
    OWDataStreamBufferDescriptor *targetBuffer = _last;
    
    if (!targetBuffer || !(targetBuffer->bufferUsed < targetBuffer->bufferSize)) {
        allocateAnotherBuffer(self, DataBufferBlockSize);
        targetBuffer = _last;
    }
        
    *returnedBufferPtr = _last->buffer + _last->bufferUsed;
    return _last->bufferSize - _last->bufferUsed;
}

- (void)wroteBytesToUnderlyingBuffer:(unsigned int)count;    
{
    _last->bufferUsed += count;
    OBINVARIANT(_last->bufferUsed <= _last->bufferSize);
    readLength += count;
    [dataAvailableCondition broadcastCondition];
    if (saveFilename)
        [self flushContentsToFile];    
}

- (CFStringEncoding)stringEncoding;
{
    return stringEncoding;
}

- (enum OWStringEncodingProvenance)stringEncodingProvenance;
{
    return stringEncodingProvenance;
}

- (void)setCFStringEncoding:(CFStringEncoding)aStringEncoding provenance:(enum OWStringEncodingProvenance)whence;
{
    NSString *encodingName;
    OWParameterizedContentType *parameterizedContentType;

    // Don't let less-reliable provenances override more-reliable ones. Allow a newer value to override an older one of equal reliability.
    // (The one case in which we don't want a newer value of the same type to override an older one is charsets specified in META tags, and that's handled by the META tag parser.)
    if (whence < stringEncodingProvenance)
        return;

    stringEncoding = aStringEncoding;
    stringEncodingProvenance = whence;
    encodingName = [OWDataStreamCharacterProcessor charsetForCFEncoding:stringEncoding];
    parameterizedContentType = [self fullContentType];
    if (![[parameterizedContentType objectForKey:@"charset"] isEqual:encodingName]) {
        [parameterizedContentType setObject:encodingName forKey:@"charset"];
    }
}

- (OWContentType *)contentEncoding;
{
    return contentEncoding;
}

- (OWContentType *)encodedContentType;
{
    return [super contentType];
}

- (void)setContentEncoding:(OWContentType *)aContentEncoding;
{
    if (aContentEncoding == unencodedContentEncoding)
	aContentEncoding = nil;
    contentEncoding = aContentEncoding;
}

- (void)setContentTypeAndEncodingFromFilename:(NSString *)aFilename isLocalFile:(BOOL)isLocalFile;
{
    OWContentType *type;

    // TODO: Read the file's type/creator code using PBGetCatInfoSync() or the like

    type = [OWContentType contentTypeForFilename:aFilename isLocalFile:isLocalFile];
    if ([type isEncoding]) {
	[self setContentEncoding:type];
	type = [OWContentType contentTypeForFilename:[aFilename stringByDeletingPathExtension] isLocalFile:NO];
    }
    [self setContentType:type];
}

- (NSString *)pathExtensionForContentTypeAndEncoding;
{
    NSString *typeExtension;
    NSString *encodingExtension;
    
    typeExtension = [[self encodedContentType] primaryExtension];
    encodingExtension = [contentEncoding primaryExtension];
    if (encodingExtension == nil)
	return typeExtension;
    else if (typeExtension == nil)
	return encodingExtension;
    else
	return [typeExtension stringByAppendingPathExtension:encodingExtension];
}

- (BOOL)resetContentTypeAndEncoding;
{
    if (flags.hasResetContentTypeAndEncoding)
        return NO;
    flags.hasResetContentTypeAndEncoding = YES;
    [self setContentType:unknownContentType];
    [self setContentEncoding:unencodedContentEncoding];
    return YES;
}

//

- (BOOL)pipeToFilename:(NSString *)aFilename;
{
    BOOL fileCreated;
    NSFileHandle *newFileHandle;
    
    if (saveFilename)
	return NO;

    if (flags.hasThrownAwayData)
	[NSException raise:OWDataStreamNoLongerValidException format:@"Data stream no longer contains valid data"];

    fileCreated = [[NSFileManager defaultManager] createFileAtPath:aFilename contents:[NSData data] attributes:nil];
    if (!fileCreated)
        [NSException raise:@"Can't save" format:NSLocalizedStringFromTableInBundle(@"Can't create file at path %@: %s", @"OWF", [OWDataStream bundle], datastream error: format items are path and errno string), aFilename, strerror(OMNI_ERRNO())];
    if ([[self contentType] hfsType] != 0)
        [[NSFileManager defaultManager] setType:[[self contentType] hfsType] andCreator:[[self contentType] hfsCreator] forPath:aFilename];

    newFileHandle = [NSFileHandle fileHandleForWritingAtPath:aFilename];
    if (!newFileHandle)
	[NSException raise:@"Can't save" format:NSLocalizedStringFromTableInBundle(@"Can't open file %@ for writing: %s", @"OWF", [OWDataStream bundle], datastream error: format items are path and errno string), aFilename, strerror(OMNI_ERRNO())];
    [saveFileHandleLock lock];
    saveFileHandle = [newFileHandle retain];
#ifdef DEBUG
    NSLog(@"new fd: %d", [saveFileHandle fileDescriptor]);
#endif
    [saveFileHandleLock unlock];
    savedBuffer = _first;
    savedInBuffer = 0;

    saveFilename = [aFilename retain];

    // If end of data happened before we set saveFilename, we need to flush out everything ourselves
    if (flags.endOfData)
        [self flushAndCloseSaveFile];

    return YES;
}

- (BOOL)pipeToFilename:(NSString *)aFilename contentCache:(id)unused;
    // Deprecated method here only for backwards compatibility with OmniWeb 4.0
{
    return [self pipeToFilename:aFilename];
}

- (NSString *)filename;
{
    return saveFilename;
}

- (BOOL)hasThrownAwayData;
{
    return flags.hasThrownAwayData;
}

- (unsigned int)bytesWrittenToFile;
{
    return readLength;
}

// OWStream subclass

- (void)setFullContentType:(OWParameterizedContentType *)aType
{
    CFStringEncoding ctEncoding;
    
    [super setFullContentType:aType];

    ctEncoding = [OWDataStreamCharacterProcessor stringEncodingForContentType:aType];
    if (ctEncoding != kCFStringEncodingInvalidId) {
        [self setCFStringEncoding:ctEncoding provenance:OWStringEncodingProvenance_ProtocolHeader];
    }
}

- (void)dataEnd;
{
    dataLength = readLength;
    [self _noMoreData];
}

- (void)dataAbort;
{
    flags.hasThrownAwayData = YES;

    [self _noMoreData];

    if (saveFilename) {
        NSString *oldFilename;

        oldFilename = saveFilename;
        saveFilename = nil;
        [[NSFileManager defaultManager] removeFileAtPath:oldFilename handler:nil];
        [oldFilename release];
    }
}

- (void)waitForDataEnd;
{
    if (!flags.endOfData)
        [endOfDataCondition waitForCondition];
}

- (BOOL)endOfData;
{
    return flags.endOfData;
}

// OWContent protocol

- (OWContentType *)contentType;
{
    return contentEncoding != nil ? contentEncoding : [self encodedContentType];
}

- (OWParameterizedContentType *)fullContentType;
{
    if (contentEncoding != nil) {
        OWParameterizedContentType *parameterizedContentEncoding;
        
        parameterizedContentEncoding = [[OWParameterizedContentType alloc] initWithContentType:contentEncoding];
        return [parameterizedContentEncoding autorelease];
    }
    
    return [super fullContentType];
}

- (unsigned long int)cacheSize;
{
    return readLength;
}

- (BOOL)contentIsValid;
{
    // Note:  Someone could throw away data in another thread after we perform this check but before we return, so while you can always trust -contentIsValid when it returns NO, you can't always trust it when it returns YES
    return ![self hasThrownAwayData];
}

// OBObject subclass (Debugging)

- (NSMutableDictionary *)debugDictionary;
{
    NSMutableDictionary *debugDictionary;

    debugDictionary = [super debugDictionary];
    [debugDictionary setObject:flags.endOfData ? @"YES" : @"NO" forKey:@"flags.endOfData"];
    [debugDictionary setObject:flags.hasThrownAwayData ? @"YES" : @"NO" forKey:@"flags.hasThrownAwayData"];
    [debugDictionary setObject:flags.hasIssuedCursor ? @"YES" : @"NO" forKey:@"flags.hasIssuedCursor"];
    [debugDictionary setObject:flags.hasResetContentTypeAndEncoding ? @"YES" : @"NO" forKey:@"flags.hasResetContentTypeAndEncoding"];
    [debugDictionary setObject:[NSNumber numberWithInt:readLength] forKey:@"readLength"];
    return debugDictionary;
}

@end


@implementation OWDataStream (Private)

- (void)flushContentsToFile;
{
    // This always happens in writer's thread, or after writer is done.

    if (savedBuffer == NULL) {
        OBASSERT(!flags.hasThrownAwayData);
        if (_first == NULL) {
            // There's nothing to flush!  (Perhaps we downloaded a 0-byte file.)
            return;
        }
        savedBuffer = _first;
    }

    do {
        unsigned int bytesCount;
        void *bytesPointer;
        
        bytesPointer = savedBuffer->buffer + savedInBuffer;
        bytesCount = savedBuffer->bufferUsed - savedInBuffer;
        
        if (bytesCount > 0) {
            NSData *data;
            
            data = [[NSData alloc] initWithBytes:bytesPointer length:bytesCount];
            [saveFileHandleLock lock];
            [saveFileHandle writeData:data];
            [saveFileHandleLock unlock];
            [data release];
            savedInBuffer += bytesCount;
        }
        
        OBASSERT(savedInBuffer == savedBuffer->bufferUsed);
        
        if (savedBuffer->next != NULL) {
            savedBuffer = savedBuffer->next;
            savedInBuffer = 0;
        } else {
            // We've written everything in this buffer, and there isn't a next buffer. Since we don't know whether more data will be appended to this buffer before a new buffer is allocated, leave the cursor at the end of this buffer.
            break;
        }
    } while (1);

    // throw away anything no longer needed
    if (flags.hasIssuedCursor)
        return; // Thread-safe shortcut to avoid grabbing the lock unnecessarily

    [saveFileHandleLock lock];
    // Now that we have the lock, check again
    if (!flags.hasIssuedCursor) {
        flags.hasThrownAwayData = YES;
        [saveFileHandleLock unlock];
        while (_first != savedBuffer) {
            OWDataStreamBufferDescriptor *this, *nextFirst;

            this = _first;
            nextFirst = this->next;
            NSDeallocateMemoryPages(this->buffer, this->bufferSize);
            NSZoneFree([self zone], this);
            _first = nextFirst;
        }
    }
    [saveFileHandleLock unlock];
}

- (void)flushAndCloseSaveFile;
{
    [self flushContentsToFile];
    [saveFileHandleLock lock];
#ifdef DEBUG
    NSLog(@"release fd: %d thread: %d", [saveFileHandle fileDescriptor], [NSThread currentThread]);
#endif
    [saveFileHandle release];
    saveFileHandle = nil;
    [saveFileHandleLock unlock];
}

- (void)_noMoreData;
{
    if (saveFilename)
        [self flushAndCloseSaveFile];
    
    flags.endOfData = YES;
    [dataAvailableCondition clearCondition];
    [endOfDataCondition clearCondition];
}

// This function seems like a good idea, except that madvise(2) doesn't actually do what its documentation says it does (10.1, apple bug ID #2789078  ---wim)
- (void)_adviseDataPages:(int)madviseFlags
{
    OWDataStreamBufferDescriptor *cursor = _first;

    for (cursor = _first; cursor != NULL; cursor = cursor->next)
        madvise(cursor->buffer, cursor->bufferSize, madviseFlags);
}

@end

NSString *OWDataStreamNoLongerValidException = @"Stream invalid";
