Skip to content

Commit 016a8b6

Browse files
committed
API: Added utilities to operate on blob streams as stdio FILEs
Header is not in the `include/cbl` directory since this isn't official API.
1 parent dba6385 commit 016a8b6

5 files changed

Lines changed: 393 additions & 0 deletions

File tree

CBL_C.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
2736A634242E5A74002B9D65 /* ReplicatorEETest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2736A633242E5A74002B9D65 /* ReplicatorEETest.cc */; };
2121
2736A635242E5A74002B9D65 /* ReplicatorEETest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2736A633242E5A74002B9D65 /* ReplicatorEETest.cc */; };
2222
273CE7E22452123400D01CA2 /* libfleeceBase.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27B61DBA21D6FF2D0027CCDB /* libfleeceBase.a */; };
23+
274E22D42742D49A00B7D1AC /* CBLBlob+FILE.h in Headers */ = {isa = PBXBuildFile; fileRef = 274E22D22742D49A00B7D1AC /* CBLBlob+FILE.h */; };
24+
274E22D52742D49A00B7D1AC /* CBLBlob+FILE.cc in Sources */ = {isa = PBXBuildFile; fileRef = 274E22D32742D49A00B7D1AC /* CBLBlob+FILE.cc */; };
25+
274E22E32742F46900B7D1AC /* CBLBlob+FILE.cc in Sources */ = {isa = PBXBuildFile; fileRef = 274E22D32742D49A00B7D1AC /* CBLBlob+FILE.cc */; };
2326
275B3576234810C400FE9CF0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 275B3567234810C400FE9CF0 /* Foundation.framework */; };
2427
275B358723481AE700FE9CF0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 275B3567234810C400FE9CF0 /* Foundation.framework */; };
2528
275B358923481D0C00FE9CF0 /* CBLTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27B61D6921D6B60D0027CCDB /* CBLTest.cc */; };
@@ -298,6 +301,8 @@
298301
2736A633242E5A74002B9D65 /* ReplicatorEETest.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ReplicatorEETest.cc; sourceTree = "<group>"; };
299302
273CD2D025E81C8F00B93C59 /* cmake */ = {isa = PBXFileReference; lastKnownFileType = folder; path = cmake; sourceTree = "<group>"; };
300303
274BAB9D24DA2DB900F4F810 /* generate_exports.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = generate_exports.sh; sourceTree = "<group>"; };
304+
274E22D22742D49A00B7D1AC /* CBLBlob+FILE.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CBLBlob+FILE.h"; sourceTree = "<group>"; };
305+
274E22D32742D49A00B7D1AC /* CBLBlob+FILE.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "CBLBlob+FILE.cc"; sourceTree = "<group>"; };
301306
275B3567234810C400FE9CF0 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
302307
275B357C234812C800FE9CF0 /* CouchbaseLiteTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CouchbaseLiteTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
303308
275B358823481C5200FE9CF0 /* XCTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = XCTests.xcconfig; sourceTree = "<group>"; };
@@ -490,6 +495,8 @@
490495
isa = PBXGroup;
491496
children = (
492497
275BC4F52209080E00DBE7D2 /* CBLBlob_Internal.hh */,
498+
274E22D22742D49A00B7D1AC /* CBLBlob+FILE.h */,
499+
274E22D32742D49A00B7D1AC /* CBLBlob+FILE.cc */,
493500
271C2A7121CADB170045856E /* CBLDatabase.cc */,
494501
27C9B5E021F655110040BC45 /* CBLDatabase_Internal.hh */,
495502
27DBD097246C9DE7002FD7A7 /* CBLDatabase+Apple.mm */,
@@ -662,6 +669,7 @@
662669
93C70CE026C4D3F80093E927 /* CBLEncryptable.h in Headers */,
663670
271C2A3321CAC98F0045856E /* CBLDocument.h in Headers */,
664671
277B77D3245B44BE00B222D3 /* CBLLog.h in Headers */,
672+
274E22D42742D49A00B7D1AC /* CBLBlob+FILE.h in Headers */,
665673
27B61D6A21D6B60D0027CCDB /* CBLTest.hh in Headers */,
666674
271C2A3521CAC98F0045856E /* CBLQuery.h in Headers */,
667675
271C2A3421CAC98F0045856E /* CBLDatabase.h in Headers */,
@@ -1179,6 +1187,7 @@
11791187
277FEE7521ED3C4900B60E3C /* CBLReplicator_CAPI.cc in Sources */,
11801188
27D11BF02351043B00C58A70 /* ConflictResolver.cc in Sources */,
11811189
27886C8E21F64C1400069BEA /* Listener.cc in Sources */,
1190+
274E22D52742D49A00B7D1AC /* CBLBlob+FILE.cc in Sources */,
11821191
271C2A7621CC4BD60045856E /* Internal.cc in Sources */,
11831192
);
11841193
runOnlyForDeploymentPostprocessing = 0;
@@ -1230,6 +1239,7 @@
12301239
27B61DB921D6ECA70027CCDB /* DatabaseTest.cc in Sources */,
12311240
277FEE5321E6BCA500B60E3C /* DatabaseTest_Cpp.cc in Sources */,
12321241
93C70D1826CB535D0093E927 /* ReplicatorPropEncTest.cc in Sources */,
1242+
274E22E32742F46900B7D1AC /* CBLBlob+FILE.cc in Sources */,
12331243
275BC4F42204FB1400DBE7D2 /* BlobTest_Cpp.cc in Sources */,
12341244
93965A6326A7CD50008728EE /* LogTest.cc in Sources */,
12351245
275B359E234D064600FE9CF0 /* QueryTest.cc in Sources */,

include/cbl/CBL_Compat.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@
3636
#define _cbl_nonnull _In_
3737
#define _cbl_warn_unused _Check_return_
3838
#define _cbl_deprecated
39+
#define _cbl_unused
3940
#else
4041
#define CBLINLINE inline
4142
#define _cbl_warn_unused __attribute__((warn_unused_result))
4243
#define _cbl_deprecated __attribute__((deprecated()))
44+
#define _cbl_unused __attribute__((unused()))
4345
#endif
4446

4547
// Macros for defining typed enumerations and option flags.

src/CBLBlob+FILE.cc

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
//
2+
// CBLBlob+FILE.cc
3+
//
4+
// Copyright © 2021 Couchbase. All rights reserved.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
#include "CBLBlob+FILE.h"
20+
#include "CBLLog.h"
21+
22+
using namespace std;
23+
using namespace fleece;
24+
25+
26+
/*
27+
There are two nonstandard APIs in <stdio.h> for opening a `FILE*` with custom read/write/seek
28+
behavior:
29+
- Apple platforms and BSD have `funopen`.
30+
- GNU's libc (Linux) has a similar API called `fopencookie`.
31+
- Sadly, Windows does not support this :(
32+
33+
The two functions, and the callbacks they use, have slightly different parameter types and
34+
semantics. Since `fopencookie`s callbacks have more sensible types, we've implemented those
35+
and then added some `funopen`-compatible wrapper functions.
36+
37+
`funopen` callback error reporting is consistent:
38+
> All user I/O functions can report an error by returning -1. Additionally,
39+
> all of the functions should set the external variable errno appropriately
40+
> if an error occurs.
41+
42+
`fopencookie`s man page doesn't mention setting `errno`, but presumably it's allowed.
43+
The return values are somewhat inconsistent in that the write callback is supposed to return
44+
0, not -1, on error. (Even though the man page itself shows an example that returns -1...)
45+
46+
References:
47+
<https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/funopen.3.html>
48+
<https://www.freebsd.org/cgi/man.cgi?query=funopen>
49+
<https://man7.org/linux/man-pages/man3/fopencookie.3.html>
50+
*/
51+
52+
53+
#ifndef _MSC_VER
54+
55+
#ifdef __APPLE__ // ...or BSD...
56+
#define USE_FUNOPEN
57+
#endif
58+
59+
60+
static inline int withErrno(int err) {
61+
errno = err;
62+
return -1;
63+
}
64+
65+
66+
#pragma mark - STDIO READ CALLBACKS:
67+
68+
69+
static ssize_t readFn_cookie(void *cookie, char *dst, size_t len) noexcept {
70+
int result = CBLBlobReader_Read((CBLBlobReadStream*)cookie, dst, len, nullptr);
71+
if (result < 0)
72+
return withErrno(EIO);
73+
return result;
74+
}
75+
76+
77+
_cbl_unused
78+
static int readFn_fun(void *cookie, char *dst, int len) {
79+
if (len < 0)
80+
return withErrno(EINVAL);
81+
return int(readFn_cookie(cookie, dst, len));
82+
}
83+
84+
85+
static int seekFn_cookie(void *cookie, int64_t *offset, int mode) noexcept {
86+
CBLSeekBase base;
87+
switch (mode) {
88+
case SEEK_SET: base = kCBLSeekModeFromStart; break;
89+
case SEEK_CUR: base = kCBLSeekModeRelative; break;
90+
case SEEK_END: base = kCBLSeekModeFromEnd; break;
91+
default: return withErrno(EINVAL);
92+
}
93+
int64_t newOffset = CBLBlobReader_Seek((CBLBlobReadStream*)cookie, *offset, base, nullptr);
94+
if (newOffset < 0)
95+
return withErrno(EINVAL);
96+
*offset = newOffset;
97+
return 0;
98+
}
99+
100+
101+
_cbl_unused
102+
static fpos_t seekFn_fun(void *cookie, fpos_t pos, int mode) noexcept {
103+
return (seekFn_cookie(cookie, &pos, mode) == 0) ? pos : -1;
104+
}
105+
106+
107+
static int closeReaderFn(void *cookie) noexcept {
108+
CBLBlobReader_Close((CBLBlobReadStream*)cookie);
109+
return 0;
110+
}
111+
112+
113+
#pragma mark - STDIO WRITE CALLBACKS:
114+
115+
116+
static ssize_t writeFn_cookie(void *cookie, const char *src, size_t len) noexcept {
117+
// "the write function should return the number of bytes copied from buf, or 0 on error.
118+
// (The function must not return a negative value.)" --Linux man page
119+
if (len == 0) {
120+
errno = EINVAL;
121+
return 0;
122+
}
123+
if (!CBLBlobWriter_Write((CBLBlobWriteStream*)cookie, src, len, nullptr)) {
124+
errno = EIO;
125+
return 0;
126+
}
127+
return len;
128+
}
129+
130+
131+
_cbl_unused
132+
static int writeFn_fun(void *cookie, const char *src, int len) noexcept {
133+
if (len < 0)
134+
return withErrno(EINVAL);
135+
if (len == 0)
136+
return 0;
137+
auto bytesWritten = writeFn_cookie(cookie, src, len);
138+
return bytesWritten > 0 ? int(bytesWritten) : -1;
139+
}
140+
141+
142+
// Coordinator between `closeWriterFn` and `CBLBlobWriter_CreateFILE`.
143+
// (It's thread-local to avoid race conditions if multiple threads create blobs at once.)
144+
__thread static CBLBlobWriteStream** sPutStreamHereOnClose = nullptr;
145+
146+
147+
static int closeWriterFn(void *cookie) noexcept {
148+
if (sPutStreamHereOnClose) {
149+
// Instead of actually closing, copy the pointer to the blob write stream where the
150+
// `CBLBlob_CreateWithFILE` function can retrieve it.
151+
*sPutStreamHereOnClose = (CBLBlobWriteStream*)cookie;
152+
} else {
153+
// If our secret pointer is NULL, then `CBLBlobWriter_CreateFILE` isn't being called,
154+
// so the app must just be calling `fclose` itself to cancel creating a blob.
155+
CBLBlobWriter_Close((CBLBlobWriteStream*)cookie);
156+
}
157+
return 0;
158+
}
159+
160+
161+
static CBLBlobWriteStream* closeFILEAndRecoverStream(FILE *f) {
162+
// There's no stdio API to recover the "cookie" value from a custom `FILE*`, so how are we
163+
// going to get the `CBLBlobWriteStream*` back?
164+
// Kludgy solution: the "close" callback (`closeWriterFn`) stores the cookie into a variable
165+
// pointed to by the static pointer `sPutStreamHereOnClose`, so after calling `fclose`
166+
// -- which we need to do anyway to flush the buffer -- our variable will be set.
167+
// If it wasn't, it means the caller passed in a `FILE*` we didn't open, which is an error.
168+
CBLBlobWriteStream *stream = nullptr;
169+
sPutStreamHereOnClose = &stream;
170+
fclose(f);
171+
sPutStreamHereOnClose = nullptr;
172+
return stream;
173+
}
174+
175+
176+
#pragma mark - API FUNCTIONS:
177+
178+
179+
FILE* CBLBlob_OpenAsFILE(CBLBlob* blob, CBLError* outError) noexcept {
180+
auto stream = CBLBlob_OpenContentStream(blob, outError);
181+
if (!stream)
182+
return nullptr;
183+
#ifdef USE_FUNOPEN
184+
return funopen(stream, &readFn_fun, nullptr, &seekFn_fun, &closeReaderFn);
185+
#else
186+
return fopencookie(stream, "r", {&readfn_cookie, nullptr, &seekfn_cookie, &closeReaderFn});
187+
#endif
188+
}
189+
190+
191+
FILE* _cbl_nullable CBLBlobWriter_CreateFILE(CBLDatabase* db, CBLError* outError) noexcept {
192+
CBLBlobWriteStream *stream = CBLBlobWriter_Create(db, outError);
193+
if (!stream)
194+
return nullptr;
195+
#ifdef USE_FUNOPEN
196+
FILE *f = funopen(stream, nullptr, writeFn_fun, nullptr, closeWriterFn);
197+
#else
198+
FILE *f = fopencookie(stream, "w", {nullptr, &writefn_cookie, nullptr, closeWriterFn});
199+
#endif
200+
if (!f)
201+
CBLBlobWriter_Close(stream);
202+
return f;
203+
}
204+
205+
206+
CBLBlob* CBLBlob_CreateWithFILE(FLString contentType, FILE* file) noexcept {
207+
CBLBlobWriteStream *stream = closeFILEAndRecoverStream(file);
208+
if (!stream) {
209+
CBL_LogMessage(kCBLLogDomainDatabase, kCBLLogError,
210+
FLSTR("CBLBlob_CreateWithFILE was called with a FILE* not opened by"
211+
" CBLBlobWriter_CreateFILE"));
212+
return nullptr;
213+
}
214+
return CBLBlob_CreateWithStream(contentType, stream);
215+
}
216+
217+
#endif // _MSC_VER

src/CBLBlob+FILE.h

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// CBLBlob+FILE.h
3+
//
4+
// Copyright (c) 2021 Couchbase, Inc All rights reserved.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
#pragma once
20+
#include "CBLBlob.h"
21+
22+
CBL_CAPI_BEGIN
23+
24+
#ifndef _MSC_VER // sorry, not available on Windows
25+
26+
/** \defgroup blobs Blobs
27+
@{
28+
\name Blob I/O with stdio `FILE`
29+
@{ */
30+
31+
/** Opens a stdio `FILE` on a blob's content. You can use this with any read-only stdio
32+
function that takes a `FILE*`, such as `fread` or `fscanf`.
33+
@note You are responsible for calling `fclose` when done with the "file". */
34+
_cbl_warn_unused
35+
FILE* _cbl_nullable CBLBlob_OpenAsFILE(CBLBlob* blob,
36+
CBLError* _cbl_nullable) CBLAPI;
37+
38+
/** Opens a stdio `FILE*` stream for creating a new Blob. You can pass this stream to any
39+
C library function that writes to a `FILE*`, such as `fwrite` or `fprintf`;
40+
but you cannot read from nor seek this stream, so `fread` and `fseek` will fail.
41+
42+
After writing the data, call \ref CBLBlob_CreateWithFILE to create the blob,
43+
instead of `fclose`.
44+
45+
If you need to cancel without creating a blob, simply call `fclose` instead. */
46+
_cbl_warn_unused
47+
FILE* _cbl_nullable CBLBlobWriter_CreateFILE(CBLDatabase* db,
48+
CBLError* _cbl_nullable) CBLAPI;
49+
50+
/** Creates a new blob object from the data written to a `FILE*` stream that was created with
51+
\ref CBLBlobWriter_CreateFILE.
52+
You should then add the blob to a mutable document as a property -- see
53+
\ref FLSlot_SetBlob.
54+
@note You are responsible for releasing the CBLBlob reference.
55+
@note Do not call `fclose` on the stream; the blob will do that.
56+
@param contentType The MIME type (optional).
57+
@param blobWriter The stream the data was written to, which must have been created with
58+
\ref CBLBlobWriter_CreateFILE.
59+
@return A new CBLBlob instance. */
60+
_cbl_warn_unused
61+
CBLBlob* CBLBlob_CreateWithFILE(FLString contentType,
62+
FILE* blobWriter) CBLAPI;
63+
64+
/** @} */
65+
/** @} */
66+
67+
CBL_CAPI_END
68+
69+
#endif // _MSC_VER

0 commit comments

Comments
 (0)