internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRevId, Status status)
        {
            if (status == null) {
                status = new Status();
            }

            status.Code = StatusCode.Ok;
            var revAttachments = rev.GetAttachments();
            if (revAttachments == null) {
                return true; // no-op: no attachments
            }

            // Deletions can't have attachments:
            if (rev.IsDeleted() || revAttachments.Count == 0) {
                var body = rev.GetProperties();
                body.Remove("_attachments");
                rev.SetProperties(body);
                return true;
            }

            int generation = RevisionInternal.GenerationFromRevID(prevRevId) + 1;
            IDictionary<string, object> parentAttachments = null;
            return rev.MutateAttachments((name, attachInfo) =>
            {
                AttachmentInternal attachment = null;
                try {
                    attachment = new AttachmentInternal(name, attachInfo);
                } catch(CouchbaseLiteException e) {
                    return null;
                }

                if(attachment.EncodedContent != null) {
                    // If there's inline attachment data, decode and store it:
                    BlobKey blobKey = new BlobKey();
                    if(!Attachments.StoreBlob(attachment.EncodedContent.ToArray(), blobKey)) {
                        status.Code = StatusCode.AttachmentError;
                        return null;
                    }

                    attachment.BlobKey = blobKey;
                } else if(attachInfo.GetCast<bool>("follows")) {
                    // "follows" means the uploader provided the attachment in a separate MIME part.
                    // This means it's already been registered in _pendingAttachmentsByDigest;
                    // I just need to look it up by its "digest" property and install it into the store:
                    InstallAttachment(attachment, attachInfo);
                } else if(attachInfo.GetCast<bool>("stub")) {
                    // "stub" on an incoming revision means the attachment is the same as in the parent.
                    if(parentAttachments == null && prevRevId != null) {
                        parentAttachments = GetAttachmentsFromDoc(rev.GetDocId(), prevRevId, status);
                        if(parentAttachments == null) {
                            if(status.Code == StatusCode.Ok || status.Code == StatusCode.NotFound) {
                                status.Code = StatusCode.BadAttachment;
                            }

                            return null;
                        }
                    }

                    var parentAttachment = parentAttachments == null ? null : parentAttachments.Get(name).AsDictionary<string, object>();
                    if(parentAttachment == null) {
                        status.Code = StatusCode.BadAttachment;
                        return null;
                    }

                    return parentAttachment;
                }


                // Set or validate the revpos:
                if(attachment.RevPos == 0) {
                    attachment.RevPos = generation;
                } else if(attachment.RevPos >= generation) {
                    status.Code = StatusCode.BadAttachment;
                    return null;
                }

                Debug.Assert(attachment.IsValid);
                return attachment.AsStubDictionary();
            });
        }
        internal RevisionInternal TransformRevision(RevisionInternal rev)
        {
            if (RevisionBodyTransformationFunction != null) {
                try {
                    var generation = rev.GetGeneration();
                    var xformed = RevisionBodyTransformationFunction(rev);
                    if (xformed == null) {
                        return null;
                    }

                    if (xformed != rev) {
                        Debug.Assert((xformed.GetDocId().Equals(rev.GetDocId())));
                        Debug.Assert((xformed.GetRevId().Equals(rev.GetRevId())));
                        Debug.Assert((xformed.GetProperties().Get("_revisions").Equals(rev.GetProperties().Get("_revisions"))));

                        if (xformed.GetProperties().ContainsKey("_attachments")) {
                            // Insert 'revpos' properties into any attachments added by the callback:
                            var mx = new RevisionInternal(xformed.GetProperties());
                            xformed = mx;
                            mx.MutateAttachments((name, info) =>
                            {
                                if (info.Get("revpos") != null) {
                                    return info;
                                }

                                if (info.Get("data") == null) {
                                    throw new InvalidOperationException("Transformer added attachment without adding data");
                                }

                                var newInfo = new Dictionary<string, object>(info);
                                newInfo["revpos"] = generation;
                                return newInfo;
                            });
                        }
                    }
                } catch (Exception e) {
                    Log.W(TAG, String.Format("Exception transforming a revision of doc '{0}'", rev.GetDocId()), e);
                }
            }

            return rev;
        }
        internal static void StubOutAttachmentsInRevBeforeRevPos(RevisionInternal rev, long minRevPos, bool attachmentsFollow)
        {
            if (minRevPos <= 1 && !attachmentsFollow)
            {
                return;
            }

            rev.MutateAttachments((name, attachment) =>
            {
                var revPos = 0L;
                if (attachment.ContainsKey("revpos"))
                {
                    revPos = Convert.ToInt64(attachment["revpos"]);
                }

                var includeAttachment = (revPos == 0 || revPos >= minRevPos);
                var stubItOut = !includeAttachment && (!attachment.ContainsKey("stub") || (bool)attachment["stub"] == false);
                var addFollows = includeAttachment && attachmentsFollow && (!attachment.ContainsKey("follows") || (bool)attachment["follows"] == false);

                if (!stubItOut && !addFollows)
                {
                    return attachment; // no change
                }

                // Need to modify attachment entry
                var editedAttachment = new Dictionary<string, object>(attachment);
                editedAttachment.Remove("data");

                if (stubItOut)
                {
                    // ...then remove the 'data' and 'follows' key:
                    editedAttachment.Remove("follows");
                    editedAttachment["stub"] = true;
                    Log.V(TAG, String.Format("Stubbed out attachment {0}: revpos {1} < {2}", rev, revPos, minRevPos));
                }
                else if (addFollows)
                {
                    editedAttachment.Remove("stub");
                    editedAttachment["follows"] = true;
                    Log.V(TAG, String.Format("Added 'follows' for attachment {0}: revpos {1} >= {2}", rev, revPos, minRevPos));
                }

                return editedAttachment;
            });
        }
        // Replaces the "follows" key with the real attachment data in all attachments to 'doc'.
        internal bool InlineFollowingAttachmentsIn(RevisionInternal rev)
        {
            return rev.MutateAttachments((s, attachment)=>
            {
                if (!attachment.ContainsKey("follows"))
                {
                    return attachment;
                }

                var fileURL = FileForAttachmentDict(attachment);
                byte[] fileData = null;
                try
                {
                    var inputStream = fileURL.OpenConnection().GetInputStream();
                    var os = new ByteArrayOutputStream();
                    inputStream.CopyTo(os);
                    fileData = os.ToByteArray();
                }
                catch (IOException e)
                {
                    Log.E(TAG, "could not retrieve attachment data: {0}".Fmt(fileURL.ToString()), e);
                    return null;
                }

                var editedAttachment = new Dictionary<string, object>(attachment);
                editedAttachment.Remove("follows");
                editedAttachment.Put("data", Convert.ToBase64String(fileData));

                return editedAttachment;
            });
        }
        // Replaces attachment data whose revpos is < minRevPos with stubs.
        // If attachmentsFollow==YES, replaces data with "follows" key.
        private static void StubOutAttachmentsInRevBeforeRevPos(RevisionInternal rev, int minRevPos, bool attachmentsFollow)
        {
            if (minRevPos <= 1 && !attachmentsFollow)
            {
                return;
            }

            rev.MutateAttachments((s, attachment)=>
            {
                var revPos = 0;
                if (attachment.Get("revpos") != null)
                {
                    revPos = (int)attachment.Get("revpos");
                }

                var includeAttachment = (revPos == 0 || revPos >= minRevPos);
                var stubItOut = !includeAttachment && (attachment.Get("stub") == null || (bool)attachment.Get("stub") == false);
                var addFollows = includeAttachment && attachmentsFollow && (attachment.Get("follows") == null || !(bool)attachment.Get ("follows"));
                if (!stubItOut && !addFollows)
                {
                    return attachment;
                }

                // no change
                // Need to modify attachment entry:
                var editedAttachment = new Dictionary<string, object>(attachment);
                editedAttachment.Remove("data");

                if (stubItOut)
                {
                    // ...then remove the 'data' and 'follows' key:
                    editedAttachment.Remove("follows");
                    editedAttachment.Put("stub", true);
                    Log.V(Tag, "Stubbed out attachment {0}: revpos {1} < {2}".Fmt(rev, revPos, minRevPos));
                }
                else
                {
                    if (addFollows)
                    {
                        editedAttachment.Remove("stub");
                        editedAttachment.Put("follows", true);
                        Log.V(Tag, "Added 'follows' for attachment {0}: revpos {1} >= {2}".Fmt(rev, revPos, minRevPos));
                    }
                }
                return editedAttachment;
            });
        }
        internal bool ExpandAttachments(RevisionInternal rev, int minRevPos, bool allowFollows, 
            bool decodeAttachments, Status outStatus)
        {
            outStatus.Code = StatusCode.Ok;
            rev.MutateAttachments((name, attachment) =>
            {
                var revPos = attachment.GetCast<long>("revpos");
                if(revPos < minRevPos && revPos != 0) {
                    //Stub:
                    return new Dictionary<string, object> { { "stub", true }, { "revpos", revPos } };
                }

                var expanded = new Dictionary<string, object>(attachment);
                expanded.Remove("stub");
                if(decodeAttachments) {
                    expanded.Remove("encoding");
                    expanded.Remove("encoded_length");
                }

                if(allowFollows && SmallestLength(expanded) >= Database.BIG_ATTACHMENT_LENGTH) {
                    //Data will follow (multipart):
                    expanded["follows"] = true;
                    expanded.Remove("data");
                } else {
                    //Put data inline:
                    expanded.Remove("follows");
                    Status status = new Status();
                    var attachObj = AttachmentForDict(attachment, name, status);
                    if(attachObj == null) {
                        Log.W(TAG, "Can't get attachment '{0}' of {1} (status {2})", name, rev, status);
                        outStatus.Code = status.Code;
                        return attachment;
                    }

                    var data = decodeAttachments ? attachObj.Content : attachObj.EncodedContent;
                    if(data == null) {
                        Log.W(TAG, "Can't get binary data of attachment '{0}' of {1}", name, rev);
                        outStatus.Code = StatusCode.NotFound;
                        return attachment;
                    }

                    expanded["data"] = Convert.ToBase64String(data.ToArray());
                }
                    
                return expanded;
            });

            return outStatus.Code == StatusCode.Ok;
        }
        internal bool ProcessAttachmentsForRevision(RevisionInternal rev, IList<string> ancestry)
        {
            var revAttachments = rev.GetAttachments();
            if (revAttachments == null) {
                return true; // no-op: no attachments
            }

            // Deletions can't have attachments:
            if (rev.IsDeleted() || revAttachments.Count == 0) {
                var body = rev.GetProperties();
                body.Remove("_attachments");
                rev.SetProperties(body);
                return true;
            }

            var prevRevId = ancestry != null && ancestry.Count > 0 ? ancestry[0] : null;
            int generation = RevisionInternal.GenerationFromRevID(prevRevId) + 1;
            IDictionary<string, object> parentAttachments = null;
            return rev.MutateAttachments((name, attachInfo) =>
            {
                AttachmentInternal attachment = null;
                try {
                    attachment = new AttachmentInternal(name, attachInfo);
                } catch(CouchbaseLiteException) {
                    return null;
                }

                if(attachment.EncodedContent != null) {
                    // If there's inline attachment data, decode and store it:
                    BlobKey blobKey = new BlobKey();
                    if(!Attachments.StoreBlob(attachment.EncodedContent.ToArray(), blobKey)) {
                        throw new CouchbaseLiteException(
                            String.Format("Failed to write attachment ' {0}'to disk", name), StatusCode.AttachmentError);
                    }

                    attachment.BlobKey = blobKey;
                } else if(attachInfo.GetCast<bool>("follows")) {
                    // "follows" means the uploader provided the attachment in a separate MIME part.
                    // This means it's already been registered in _pendingAttachmentsByDigest;
                    // I just need to look it up by its "digest" property and install it into the store:
                    InstallAttachment(attachment);
                } else if(attachInfo.GetCast<bool>("stub")) {
                    // "stub" on an incoming revision means the attachment is the same as in the parent.
                    if(parentAttachments == null && prevRevId != null) {
                        parentAttachments = GetAttachmentsFromDoc(rev.GetDocId(), prevRevId);
                        if(parentAttachments == null) {
                            if(Attachments.HasBlobForKey(attachment.BlobKey)) {
                                // Parent revision's body isn't known (we are probably pulling a rev along
                                // with its entire history) but it's OK, we have the attachment already
                                return attachInfo;
                            }

                            var ancestorAttachment = FindAttachment(name, attachment.RevPos, rev.GetDocId(), ancestry);
                            if(ancestorAttachment != null) {
                                return ancestorAttachment;
                            }

                            throw new CouchbaseLiteException(
                                String.Format("Unable to find 'stub' attachment {0} in history", name), StatusCode.BadAttachment);
                        }
                    }

                    var parentAttachment = parentAttachments == null ? null : parentAttachments.Get(name).AsDictionary<string, object>();
                    if(parentAttachment == null) {
                        throw new CouchbaseLiteException(
                            String.Format("Unable to find 'stub' attachment {0} in history", name), StatusCode.BadAttachment);
                    }

                    return parentAttachment;
                }


                // Set or validate the revpos:
                if(attachment.RevPos == 0) {
                    attachment.RevPos = generation;
                } else if(attachment.RevPos > generation) {
                    throw new CouchbaseLiteException(
                        String.Format("Attachment specifies revision generation {0} but document is only at revision generation {1}",
                        attachment.RevPos, generation), StatusCode.BadAttachment);
                }

                Debug.Assert(attachment.IsValid);
                return attachment.AsStubDictionary();
            });
        }
        internal void ExpandAttachments(RevisionInternal rev, int minRevPos, bool allowFollows, 
            bool decodeAttachments)
        {
            if (!IsOpen) {
                throw new CouchbaseLiteException("DB is closed", StatusCode.DbError);
            }

            rev.MutateAttachments((name, attachment) =>
            {
                var revPos = attachment.GetCast<long>("revpos");
                if(revPos < minRevPos && revPos != 0) {
                    //Stub:
                    return new Dictionary<string, object> { { "stub", true }, { "revpos", revPos } };
                }

                var expanded = new Dictionary<string, object>(attachment);
                expanded.Remove("stub");
                if(decodeAttachments) {
                    expanded.Remove("encoding");
                    expanded.Remove("encoded_length");
                }

                if(allowFollows && SmallestLength(expanded) >= Database.BIG_ATTACHMENT_LENGTH) {
                    //Data will follow (multipart):
                    expanded["follows"] = true;
                    expanded.Remove("data");
                } else {
                    //Put data inline:
                    expanded.Remove("follows");
                    var attachObj = AttachmentForDict(attachment, name);
                    var data = decodeAttachments ? attachObj.Content : attachObj.EncodedContent;
                    if(data == null) {
                        Log.W(TAG, "Can't get binary data of attachment '{0}' of {1}", name, rev);
                        return attachment;
                    }

                    expanded["data"] = Convert.ToBase64String(data.ToArray());
                }
                    
                return expanded;
            });
        }
        internal RevisionInternal TransformRevision(RevisionInternal rev)
        {
            if (RevisionBodyTransformationFunction != null) {
                try {
                    var generation = rev.Generation;
                    var xformed = RevisionBodyTransformationFunction(rev);
                    if (xformed == null) {
                        return null;
                    }

                    if (xformed != rev) {
                        Debug.Assert((xformed.DocID.Equals(rev.DocID)));
                        Debug.Assert((xformed.RevID.Equals(rev.RevID)));
                        Debug.Assert((xformed.GetProperties().Get("_revisions").Equals(rev.GetProperties().Get("_revisions"))));

                        if (xformed.GetProperties().ContainsKey("_attachments")) {
                            // Insert 'revpos' properties into any attachments added by the callback:
                            var mx = new RevisionInternal(xformed.GetProperties());
                            xformed = mx;
                            mx.MutateAttachments((name, info) =>
                            {
                                if (info.Get("revpos") != null) {
                                    return info;
                                }

                                if (info.Get("data") == null) {
                                    throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.InternalServerError, Tag,
                                        "Transformer added attachment without adding data");
                                }

                                var newInfo = new Dictionary<string, object>(info);
                                newInfo["revpos"] = generation;
                                return newInfo;
                            });
                        }
                    }
                } catch (Exception e) {
                    Log.To.Sync.W(Tag, String.Format("Exception transforming a revision of doc '{0}', aborting...", 
                        new SecureLogString(rev.DocID, LogMessageSensitivity.PotentiallyInsecure)), e);
                }
            }

            return rev;
        }
        internal bool ProcessAttachmentsForRevision(RevisionInternal rev, IList<RevisionID> ancestry)
        {
            var revAttachments = rev.GetAttachments();
            if(revAttachments == null) {
                return true; // no-op: no attachments
            }

            // Deletions can't have attachments:
            if(rev.Deleted || revAttachments.Count == 0) {
                var body = rev.GetProperties();
                body.Remove("_attachments");
                rev.SetProperties(body);
                return true;
            }

            var prevRevId = ancestry != null && ancestry.Count > 0 ? ancestry[0] : null;
            int generation = rev.Generation;
            IDictionary<string, object> parentAttachments = null;
            return rev.MutateAttachments((name, attachInfo) =>
            {
                AttachmentInternal attachment = null;
                try {
                    attachment = new AttachmentInternal(name, attachInfo);
                } catch(CouchbaseLiteException) {
                    Log.To.Database.W(TAG, "Error creating attachment object for '{0}' ('{1}'), " +
                        "returning null", new SecureLogString(name, LogMessageSensitivity.PotentiallyInsecure),
                        new SecureLogJsonString(attachInfo, LogMessageSensitivity.PotentiallyInsecure));
                    return null;
                }

                if(attachment.EncodedContent != null) {
                    // If there's inline attachment data, decode and store it:
                    BlobKey blobKey = new BlobKey();
                    try {
                        Attachments.StoreBlob(attachment.EncodedContent.ToArray(), blobKey);
                    } catch(CouchbaseLiteException) {
                        Log.To.Database.E(TAG, "Failed to write attachment '{0}' to disk, rethrowing...", name);
                        throw;
                    } catch(Exception e) {
                        throw Misc.CreateExceptionAndLog(Log.To.Database, e, TAG,
                            "Exception during attachment writing '{0}'",
                            new SecureLogString(name, LogMessageSensitivity.PotentiallyInsecure));
                    }

                    attachment.BlobKey = blobKey;
                } else if(attachInfo.GetCast<bool>("follows")) {
                    // "follows" means the uploader provided the attachment in a separate MIME part.
                    // This means it's already been registered in _pendingAttachmentsByDigest;
                    // I just need to look it up by its "digest" property and install it into the store:
                    InstallAttachment(attachment);
                } else if(attachInfo.GetCast<bool>("stub")) {
                    // "stub" on an incoming revision means the attachment is the same as in the parent.
                    if(parentAttachments == null && prevRevId != null) {
                        parentAttachments = GetAttachmentsFromDoc(rev.DocID, prevRevId);
                        if(parentAttachments == null) {
                            if(Attachments.HasBlobForKey(attachment.BlobKey)) {
                                // Parent revision's body isn't known (we are probably pulling a rev along
                                // with its entire history) but it's OK, we have the attachment already
                                return attachInfo;
                            }

                            var ancestorAttachment = FindAttachment(name, attachment.RevPos, rev.DocID, ancestry);
                            if(ancestorAttachment != null) {
                                return ancestorAttachment;
                            }

                            throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadAttachment, TAG,
                                "Unable to find 'stub' attachment {0} in history (1)",
                                new SecureLogString(name, LogMessageSensitivity.PotentiallyInsecure));
                        }
                    }

                    var parentAttachment = parentAttachments == null ? null : parentAttachments.Get(name).AsDictionary<string, object>();
                    if(parentAttachment == null) {
                        throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadAttachment, TAG,
                            "Unable to find 'stub' attachment {0} in history (2)",
                            new SecureLogString(name, LogMessageSensitivity.PotentiallyInsecure));
                    }

                    return parentAttachment;
                }


                // Set or validate the revpos:
                if(attachment.RevPos == 0) {
                    attachment.RevPos = generation;
                } else if(attachment.RevPos > generation) {
                    throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadAttachment, TAG,
                        "Attachment specifies revision generation {0} but document is only at revision generation {1}",
                        attachment.RevPos, generation);
                }

                Debug.Assert(attachment.IsValid);
                return attachment.AsStubDictionary();
            });
        }
        internal bool RegisterAttachmentBodies(IDictionary<string, Stream> attachments, RevisionInternal rev)
        {
            var ok = true;
            rev.MutateAttachments((name, meta) =>
            {
                var value = attachments.Get(name);
                if(value != null) {
                    // Register attachment body with database
                    var writer = AttachmentWriter;
                    try {
                        writer.Read(value);
                        writer.Finish();
                    } catch(Exception e) {
                        Log.To.Database.W(TAG, $"Error reading stream for attachment {name}, skipping...",
                            e);
                        ok = false;
                        return null;
                    }

                    // Make attachment mode "follows", indicating the data is registered
                    var nuMeta = new Dictionary<string, object>(meta);
                    nuMeta.Remove("data");
                    nuMeta.Remove("stub");
                    nuMeta["follows"] = true;

                    // Add or verify metadata "digest" property
                    var digest = meta.GetCast<string>("digest");
                    var sha1Digest = writer.SHA1DigestString();
                    if(digest != null) {
                        if(digest != sha1Digest && digest != writer.MD5DigestString()) {
                            Log.To.Database.W(TAG, "Attachment '{0}' body digest ({1}) doesn't match " +
                                "'digest' property {2}", name, sha1Digest, digest);
                            ok = false;
                            return null;
                        }
                    } else {
                        nuMeta["digest"] = digest = sha1Digest;
                    }

                    RememberAttachmentWriter(writer, digest);
                    return nuMeta;
                }

                return meta;
            });

            return ok;
        }