Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CBL_C.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
2736A634242E5A74002B9D65 /* ReplicatorEETest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2736A633242E5A74002B9D65 /* ReplicatorEETest.cc */; };
2736A635242E5A74002B9D65 /* ReplicatorEETest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2736A633242E5A74002B9D65 /* ReplicatorEETest.cc */; };
273CE7E22452123400D01CA2 /* libfleeceBase.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27B61DBA21D6FF2D0027CCDB /* libfleeceBase.a */; };
274E22D42742D49A00B7D1AC /* CBLBlob+FILE.h in Headers */ = {isa = PBXBuildFile; fileRef = 274E22D22742D49A00B7D1AC /* CBLBlob+FILE.h */; };
274E22D52742D49A00B7D1AC /* CBLBlob+FILE.cc in Sources */ = {isa = PBXBuildFile; fileRef = 274E22D32742D49A00B7D1AC /* CBLBlob+FILE.cc */; };
274E22E32742F46900B7D1AC /* CBLBlob+FILE.cc in Sources */ = {isa = PBXBuildFile; fileRef = 274E22D32742D49A00B7D1AC /* CBLBlob+FILE.cc */; };
275B3576234810C400FE9CF0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 275B3567234810C400FE9CF0 /* Foundation.framework */; };
275B358723481AE700FE9CF0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 275B3567234810C400FE9CF0 /* Foundation.framework */; };
275B358923481D0C00FE9CF0 /* CBLTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27B61D6921D6B60D0027CCDB /* CBLTest.cc */; };
Expand Down Expand Up @@ -298,6 +301,8 @@
2736A633242E5A74002B9D65 /* ReplicatorEETest.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ReplicatorEETest.cc; sourceTree = "<group>"; };
273CD2D025E81C8F00B93C59 /* cmake */ = {isa = PBXFileReference; lastKnownFileType = folder; path = cmake; sourceTree = "<group>"; };
274BAB9D24DA2DB900F4F810 /* generate_exports.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = generate_exports.sh; sourceTree = "<group>"; };
274E22D22742D49A00B7D1AC /* CBLBlob+FILE.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CBLBlob+FILE.h"; sourceTree = "<group>"; };
274E22D32742D49A00B7D1AC /* CBLBlob+FILE.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "CBLBlob+FILE.cc"; sourceTree = "<group>"; };
275B3567234810C400FE9CF0 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
275B357C234812C800FE9CF0 /* CouchbaseLiteTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CouchbaseLiteTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
275B358823481C5200FE9CF0 /* XCTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = XCTests.xcconfig; sourceTree = "<group>"; };
Expand Down Expand Up @@ -490,6 +495,8 @@
isa = PBXGroup;
children = (
275BC4F52209080E00DBE7D2 /* CBLBlob_Internal.hh */,
274E22D22742D49A00B7D1AC /* CBLBlob+FILE.h */,
274E22D32742D49A00B7D1AC /* CBLBlob+FILE.cc */,
271C2A7121CADB170045856E /* CBLDatabase.cc */,
27C9B5E021F655110040BC45 /* CBLDatabase_Internal.hh */,
27DBD097246C9DE7002FD7A7 /* CBLDatabase+Apple.mm */,
Expand Down Expand Up @@ -662,6 +669,7 @@
93C70CE026C4D3F80093E927 /* CBLEncryptable.h in Headers */,
271C2A3321CAC98F0045856E /* CBLDocument.h in Headers */,
277B77D3245B44BE00B222D3 /* CBLLog.h in Headers */,
274E22D42742D49A00B7D1AC /* CBLBlob+FILE.h in Headers */,
27B61D6A21D6B60D0027CCDB /* CBLTest.hh in Headers */,
271C2A3521CAC98F0045856E /* CBLQuery.h in Headers */,
271C2A3421CAC98F0045856E /* CBLDatabase.h in Headers */,
Expand Down Expand Up @@ -1179,6 +1187,7 @@
277FEE7521ED3C4900B60E3C /* CBLReplicator_CAPI.cc in Sources */,
27D11BF02351043B00C58A70 /* ConflictResolver.cc in Sources */,
27886C8E21F64C1400069BEA /* Listener.cc in Sources */,
274E22D52742D49A00B7D1AC /* CBLBlob+FILE.cc in Sources */,
271C2A7621CC4BD60045856E /* Internal.cc in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -1230,6 +1239,7 @@
27B61DB921D6ECA70027CCDB /* DatabaseTest.cc in Sources */,
277FEE5321E6BCA500B60E3C /* DatabaseTest_Cpp.cc in Sources */,
93C70D1826CB535D0093E927 /* ReplicatorPropEncTest.cc in Sources */,
274E22E32742F46900B7D1AC /* CBLBlob+FILE.cc in Sources */,
275BC4F42204FB1400DBE7D2 /* BlobTest_Cpp.cc in Sources */,
93965A6326A7CD50008728EE /* LogTest.cc in Sources */,
275B359E234D064600FE9CF0 /* QueryTest.cc in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions include/cbl/CBL_Compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@
#define _cbl_nonnull _In_
#define _cbl_warn_unused _Check_return_
#define _cbl_deprecated
#define _cbl_unused
#else
#define CBLINLINE inline
#define _cbl_warn_unused __attribute__((warn_unused_result))
#define _cbl_deprecated __attribute__((deprecated()))
#define _cbl_unused __attribute__((unused()))
#endif

// Macros for defining typed enumerations and option flags.
Expand Down
217 changes: 217 additions & 0 deletions src/CBLBlob+FILE.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
//
// CBLBlob+FILE.cc
//
// Copyright © 2021 Couchbase. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#include "CBLBlob+FILE.h"
#include "CBLLog.h"

using namespace std;
using namespace fleece;


/*
There are two nonstandard APIs in <stdio.h> for opening a `FILE*` with custom read/write/seek
behavior:
- Apple platforms and BSD have `funopen`.
- GNU's libc (Linux) has a similar API called `fopencookie`.
- Sadly, Windows does not support this :(

The two functions, and the callbacks they use, have slightly different parameter types and
semantics. Since `fopencookie`s callbacks have more sensible types, we've implemented those
and then added some `funopen`-compatible wrapper functions.

`funopen` callback error reporting is consistent:
> All user I/O functions can report an error by returning -1. Additionally,
> all of the functions should set the external variable errno appropriately
> if an error occurs.

`fopencookie`s man page doesn't mention setting `errno`, but presumably it's allowed.
The return values are somewhat inconsistent in that the write callback is supposed to return
0, not -1, on error. (Even though the man page itself shows an example that returns -1...)

References:
<https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/funopen.3.html>
<https://www.freebsd.org/cgi/man.cgi?query=funopen>
<https://man7.org/linux/man-pages/man3/fopencookie.3.html>
*/


#ifndef _MSC_VER

#ifdef __APPLE__ // ...or BSD...
#define USE_FUNOPEN
#endif


static inline int withErrno(int err) {
errno = err;
return -1;
}


#pragma mark - STDIO READ CALLBACKS:


static ssize_t readFn_cookie(void *cookie, char *dst, size_t len) noexcept {
int result = CBLBlobReader_Read((CBLBlobReadStream*)cookie, dst, len, nullptr);
if (result < 0)
return withErrno(EIO);
return result;
}


_cbl_unused
static int readFn_fun(void *cookie, char *dst, int len) {
if (len < 0)
return withErrno(EINVAL);
return int(readFn_cookie(cookie, dst, len));
}


static int seekFn_cookie(void *cookie, int64_t *offset, int mode) noexcept {
CBLSeekBase base;
switch (mode) {
case SEEK_SET: base = kCBLSeekModeFromStart; break;
case SEEK_CUR: base = kCBLSeekModeRelative; break;
case SEEK_END: base = kCBLSeekModeFromEnd; break;
default: return withErrno(EINVAL);
}
int64_t newOffset = CBLBlobReader_Seek((CBLBlobReadStream*)cookie, *offset, base, nullptr);
if (newOffset < 0)
return withErrno(EINVAL);
*offset = newOffset;
return 0;
}


_cbl_unused
static fpos_t seekFn_fun(void *cookie, fpos_t pos, int mode) noexcept {
return (seekFn_cookie(cookie, &pos, mode) == 0) ? pos : -1;
}


static int closeReaderFn(void *cookie) noexcept {
CBLBlobReader_Close((CBLBlobReadStream*)cookie);
return 0;
}


#pragma mark - STDIO WRITE CALLBACKS:


static ssize_t writeFn_cookie(void *cookie, const char *src, size_t len) noexcept {
// "the write function should return the number of bytes copied from buf, or 0 on error.
// (The function must not return a negative value.)" --Linux man page
if (len == 0) {
errno = EINVAL;
return 0;
}
if (!CBLBlobWriter_Write((CBLBlobWriteStream*)cookie, src, len, nullptr)) {
errno = EIO;
return 0;
}
return len;
}


_cbl_unused
static int writeFn_fun(void *cookie, const char *src, int len) noexcept {
if (len < 0)
return withErrno(EINVAL);
if (len == 0)
return 0;
auto bytesWritten = writeFn_cookie(cookie, src, len);
return bytesWritten > 0 ? int(bytesWritten) : -1;
}


// Coordinator between `closeWriterFn` and `CBLBlobWriter_CreateFILE`.
// (It's thread-local to avoid race conditions if multiple threads create blobs at once.)
__thread static CBLBlobWriteStream** sPutStreamHereOnClose = nullptr;


static int closeWriterFn(void *cookie) noexcept {
if (sPutStreamHereOnClose) {
// Instead of actually closing, copy the pointer to the blob write stream where the
// `CBLBlob_CreateWithFILE` function can retrieve it.
*sPutStreamHereOnClose = (CBLBlobWriteStream*)cookie;
} else {
// If our secret pointer is NULL, then `CBLBlobWriter_CreateFILE` isn't being called,
// so the app must just be calling `fclose` itself to cancel creating a blob.
CBLBlobWriter_Close((CBLBlobWriteStream*)cookie);
}
return 0;
}


static CBLBlobWriteStream* closeFILEAndRecoverStream(FILE *f) {
// There's no stdio API to recover the "cookie" value from a custom `FILE*`, so how are we
// going to get the `CBLBlobWriteStream*` back?
// Kludgy solution: the "close" callback (`closeWriterFn`) stores the cookie into a variable
// pointed to by the static pointer `sPutStreamHereOnClose`, so after calling `fclose`
// -- which we need to do anyway to flush the buffer -- our variable will be set.
// If it wasn't, it means the caller passed in a `FILE*` we didn't open, which is an error.
CBLBlobWriteStream *stream = nullptr;
sPutStreamHereOnClose = &stream;
fclose(f);
sPutStreamHereOnClose = nullptr;
return stream;
}


#pragma mark - API FUNCTIONS:


FILE* CBLBlob_OpenAsFILE(CBLBlob* blob, CBLError* outError) noexcept {
auto stream = CBLBlob_OpenContentStream(blob, outError);
if (!stream)
return nullptr;
#ifdef USE_FUNOPEN
return funopen(stream, &readFn_fun, nullptr, &seekFn_fun, &closeReaderFn);
#else
return fopencookie(stream, "r", {&readfn_cookie, nullptr, &seekfn_cookie, &closeReaderFn});
#endif
}


FILE* _cbl_nullable CBLBlobWriter_CreateFILE(CBLDatabase* db, CBLError* outError) noexcept {
CBLBlobWriteStream *stream = CBLBlobWriter_Create(db, outError);
if (!stream)
return nullptr;
#ifdef USE_FUNOPEN
FILE *f = funopen(stream, nullptr, writeFn_fun, nullptr, closeWriterFn);
#else
FILE *f = fopencookie(stream, "w", {nullptr, &writefn_cookie, nullptr, closeWriterFn});
#endif
if (!f)
CBLBlobWriter_Close(stream);
return f;
}


CBLBlob* CBLBlob_CreateWithFILE(FLString contentType, FILE* file) noexcept {
CBLBlobWriteStream *stream = closeFILEAndRecoverStream(file);
if (!stream) {
CBL_LogMessage(kCBLLogDomainDatabase, kCBLLogError,
FLSTR("CBLBlob_CreateWithFILE was called with a FILE* not opened by"
" CBLBlobWriter_CreateFILE"));
return nullptr;
}
return CBLBlob_CreateWithStream(contentType, stream);
}

#endif // _MSC_VER
69 changes: 69 additions & 0 deletions src/CBLBlob+FILE.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// CBLBlob+FILE.h
//
// Copyright (c) 2021 Couchbase, Inc All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#pragma once
#include "cbl/CBLBlob.h"

CBL_CAPI_BEGIN

#ifndef _MSC_VER // sorry, not available on Windows

/** \defgroup blobs Blobs
@{
\name Blob I/O with stdio `FILE`
@{ */

/** Opens a stdio `FILE` on a blob's content. You can use this with any read-only stdio
function that takes a `FILE*`, such as `fread` or `fscanf`.
@note You are responsible for calling `fclose` when done with the "file". */
_cbl_warn_unused
FILE* _cbl_nullable CBLBlob_OpenAsFILE(CBLBlob* blob,
CBLError* _cbl_nullable) CBLAPI;

/** Opens a stdio `FILE*` stream for creating a new Blob. You can pass this stream to any
C library function that writes to a `FILE*`, such as `fwrite` or `fprintf`;
but you cannot read from nor seek this stream, so `fread` and `fseek` will fail.

After writing the data, call \ref CBLBlob_CreateWithFILE to create the blob,
instead of `fclose`.

If you need to cancel without creating a blob, simply call `fclose` instead. */
_cbl_warn_unused
FILE* _cbl_nullable CBLBlobWriter_CreateFILE(CBLDatabase* db,
CBLError* _cbl_nullable) CBLAPI;

/** Creates a new blob object from the data written to a `FILE*` stream that was created with
\ref CBLBlobWriter_CreateFILE.
You should then add the blob to a mutable document as a property -- see
\ref FLSlot_SetBlob.
@note You are responsible for releasing the CBLBlob reference.
@note Do not call `fclose` on the stream; the blob will do that.
@param contentType The MIME type (optional).
@param blobWriter The stream the data was written to, which must have been created with
\ref CBLBlobWriter_CreateFILE.
@return A new CBLBlob instance. */
_cbl_warn_unused
CBLBlob* CBLBlob_CreateWithFILE(FLString contentType,
FILE* blobWriter) CBLAPI;

/** @} */
/** @} */

CBL_CAPI_END

#endif // _MSC_VER
Loading