/*             ----> DO NOT REMOVE THE FOLLOWING NOTICE <----

                   Copyright (c) 2014-2015 Datalight, Inc.
                       All Rights Reserved Worldwide.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; use version 2 of the License.

    This program is distributed in the hope that it will be useful,
    but "AS-IS," WITHOUT ANY WARRANTY; without even the implied warranty
    of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
/*  Businesses and individuals that for commercial or other reasons cannot
    comply with the terms of the GPLv2 license may obtain a commercial license
    before incorporating Reliance Edge into proprietary software for
    distribution in any form.  Visit http://www.datalight.com/reliance-edge for
    more information.
*/
/** @file
    @brief Implements inode I/O functions.
*/
#include <redfs.h>
#include <redcore.h>


/*  This value is used to initialize the uIndirEntry and uDindirEntry members of
    the CINODE structure.  After seeking, a value of COORD_ENTRY_INVALID in
    uIndirEntry indicates that there is no indirect node in the path through the
    file metadata structure, and a value of COORD_ENTRY_INVALID in uDindirEntry
    indicates that there is no double indirect node.
*/
#define COORD_ENTRY_INVALID (UINT16_MAX)

/*  This enumeration is used by the BranchBlock() and BranchBlockCost()
    functions to determine which blocks of the file metadata structure need to
    be branched, and which to ignore.  DINDIR requires requires branching the
    double indirect only, INDIR requires branching the double indirect
    (if present) and the indirect, and FILE_DATA requires branching the indirect
    and double indirect (if present) and the file data block.
*/
typedef enum
{
    BRANCHDEPTH_DINDIR      = 0U,
    BRANCHDEPTH_INDIR       = 1U,
    BRANCHDEPTH_FILE_DATA   = 2U,
    BRANCHDEPTH_MAX         = BRANCHDEPTH_FILE_DATA
} BRANCHDEPTH;


#if REDCONF_READ_ONLY == 0
#if DELETE_SUPPORTED || TRUNCATE_SUPPORTED
static REDSTATUS Shrink(CINODE *pInode, uint64_t ullSize);
#if DINDIR_POINTERS > 0U
static REDSTATUS TruncDindir(CINODE *pInode, bool *pfFreed);
#endif
#if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
static REDSTATUS TruncIndir(CINODE *pInode, bool *pfFreed);
#endif
static REDSTATUS TruncDataBlock(const CINODE *pInode, uint32_t *pulBlock, bool fPropagate);
#endif
static REDSTATUS ExpandPrepare(CINODE *pInode);
#endif
static void SeekCoord(CINODE *pInode, uint32_t ulBlock);
static REDSTATUS ReadUnaligned(CINODE *pInode, uint64_t ullStart, uint32_t ulLen, uint8_t *pbBuffer);
static REDSTATUS ReadAligned(CINODE *pInode, uint32_t ulBlockStart, uint32_t ulBlockCount, uint8_t *pbBuffer);
#if REDCONF_READ_ONLY == 0
static REDSTATUS WriteUnaligned(CINODE *pInode, uint64_t ullStart, uint32_t ulLen, const uint8_t *pbBuffer);
static REDSTATUS WriteAligned(CINODE *pInode, uint32_t ulBlockStart, uint32_t *pulBlockCount, const uint8_t *pbBuffer);
#endif
static REDSTATUS GetExtent(CINODE *pInode, uint32_t ulBlockStart, uint32_t *pulExtentStart, uint32_t *pulExtentLen);
#if REDCONF_READ_ONLY == 0
static REDSTATUS BranchBlock(CINODE *pInode, BRANCHDEPTH depth, bool fBuffer);
static REDSTATUS BranchOneBlock(uint32_t *pulBlock, void **ppBuffer, uint16_t uBFlag);
static REDSTATUS BranchBlockCost(const CINODE *pInode, BRANCHDEPTH depth, uint32_t *pulCost);
static uint32_t FreeBlockCount(void);
#endif


/** @brief Read data from an inode.

    @param pInode   A pointer to the cached inode structure of the inode from
                    which to read.
    @param ullStart The file offset at which to read.
    @param pulLen   On input, the number of bytes to attempt to read.  On
                    successful return, populated with the number of bytes
                    actually read.
    @param pBuffer  The buffer to read into.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_EINVAL @p pInode is not a mounted cached inode pointer; or
                        @p pulLen is `NULL`; or @p pBuffer is `NULL`.
*/
REDSTATUS RedInodeDataRead(
    CINODE     *pInode,
    uint64_t    ullStart,
    uint32_t   *pulLen,
    void       *pBuffer)
{
    REDSTATUS   ret = 0;

    if(!CINODE_IS_MOUNTED(pInode) || (pulLen == NULL) || (pBuffer == NULL))
    {
        ret = -RED_EINVAL;
    }
    else if(ullStart >= pInode->pInodeBuf->ullSize)
    {
        *pulLen = 0U;
    }
    else if(*pulLen == 0U)
    {
        /*  Do nothing, just return success.
        */
    }
    else
    {
        uint8_t    *pbBuffer = CAST_VOID_PTR_TO_UINT8_PTR(pBuffer);
        uint32_t    ulReadIndex = 0U;
        uint32_t    ulLen = *pulLen;
        uint32_t    ulRemaining;

        /*  Reading beyond the end of the file is not allowed.  If the requested
            read extends beyond the end of the file, truncate the read length so
            that the read stops at the end of the file.
        */
        if((pInode->pInodeBuf->ullSize - ullStart) < ulLen)
        {
            ulLen = (uint32_t)(pInode->pInodeBuf->ullSize - ullStart);
        }

        ulRemaining = ulLen;

        /*  Unaligned partial block at start.
        */
        if((ullStart & (REDCONF_BLOCK_SIZE - 1U)) != 0U)
        {
            uint32_t ulBytesInFirstBlock = REDCONF_BLOCK_SIZE - (uint32_t)(ullStart & (REDCONF_BLOCK_SIZE - 1U));
            uint32_t ulThisRead = REDMIN(ulRemaining, ulBytesInFirstBlock);

            ret = ReadUnaligned(pInode, ullStart, ulThisRead, pbBuffer);

            if(ret == 0)
            {
                ulReadIndex += ulThisRead;
                ulRemaining -= ulThisRead;
            }
        }

        /*  Whole blocks.
        */
        if((ret == 0) && (ulRemaining >= REDCONF_BLOCK_SIZE))
        {
            uint32_t ulBlockOffset = (uint32_t)((ullStart + ulReadIndex) >> BLOCK_SIZE_P2);
            uint32_t ulBlockCount = ulRemaining >> BLOCK_SIZE_P2;

            REDASSERT(((ullStart + ulReadIndex) & (REDCONF_BLOCK_SIZE - 1U)) == 0U);

            ret = ReadAligned(pInode, ulBlockOffset, ulBlockCount, &pbBuffer[ulReadIndex]);

            if(ret == 0)
            {
                ulReadIndex += ulBlockCount << BLOCK_SIZE_P2;
                ulRemaining -= ulBlockCount << BLOCK_SIZE_P2;
            }
        }

        /*  Aligned partial block at end.
        */
        if((ret == 0) && (ulRemaining > 0U))
        {
            REDASSERT(ulRemaining < REDCONF_BLOCK_SIZE);
            REDASSERT(((ullStart + ulReadIndex) & (REDCONF_BLOCK_SIZE - 1U)) == 0U);

            ret = ReadUnaligned(pInode, ullStart + ulReadIndex, ulRemaining, &pbBuffer[ulReadIndex]);
        }

        if(ret == 0)
        {
            *pulLen = ulLen;
        }
    }

    return ret;
}


#if REDCONF_READ_ONLY == 0
/** @brief Write to an inode.

    @param pInode   A pointer to the cached inode structure of the inode into
                    which to write.
    @param ullStart The file offset at which to write.
    @param pulLen   On input, the number of bytes to attempt to write.  On
                    successful return, populated with the number of bytes
                    actually written.
    @param pBuffer  The buffer to write from.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EFBIG  @p ullStart is greater than the maximum file size; or
                        @p ullStart is equal to the maximum file size and the
                        write length is non-zero.
    @retval -RED_EINVAL @p pInode is not a mounted cached inode pointer; or
                        @p pulLen is `NULL`; or @p pBuffer is `NULL`.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_ENOSPC No data can be written because there is insufficient
                        free space.
*/
REDSTATUS RedInodeDataWrite(
    CINODE     *pInode,
    uint64_t    ullStart,
    uint32_t   *pulLen,
    const void *pBuffer)
{
    REDSTATUS   ret = 0;

    if(!CINODE_IS_DIRTY(pInode) || (pulLen == NULL) || (pBuffer == NULL))
    {
        ret = -RED_EINVAL;
    }
    else if((ullStart > INODE_SIZE_MAX) || ((ullStart == INODE_SIZE_MAX) && (*pulLen > 0U)))
    {
        ret = -RED_EFBIG;
    }
    else if(*pulLen == 0U)
    {
        /*  Do nothing, just return success.
        */
    }
    else
    {
        const uint8_t  *pbBuffer = CAST_VOID_PTR_TO_CONST_UINT8_PTR(pBuffer);
        uint32_t        ulWriteIndex = 0U;
        uint32_t        ulLen = *pulLen;
        uint32_t        ulRemaining;

        if((INODE_SIZE_MAX - ullStart) < ulLen)
        {
            ulLen = (uint32_t)(INODE_SIZE_MAX - ullStart);
        }

        ulRemaining = ulLen;

        /*  If the write is beyond the current end of the file, and the current
            end of the file is not block-aligned, then there may be some data
            that needs to be zeroed in the last block.
        */
        if(ullStart > pInode->pInodeBuf->ullSize)
        {
            ret = ExpandPrepare(pInode);
        }

        /*  Partial block at start.
        */
        if((ret == 0) && (((ullStart & (REDCONF_BLOCK_SIZE - 1U)) != 0U) || (ulRemaining < REDCONF_BLOCK_SIZE)))
        {
            uint32_t ulBytesInFirstBlock = REDCONF_BLOCK_SIZE - (uint32_t)(ullStart & (REDCONF_BLOCK_SIZE - 1U));
            uint32_t ulThisWrite = REDMIN(ulRemaining, ulBytesInFirstBlock);

            ret = WriteUnaligned(pInode, ullStart, ulThisWrite, pbBuffer);

            if(ret == 0)
            {
                ulWriteIndex += ulThisWrite;
                ulRemaining -= ulThisWrite;
            }
        }

        /*  Whole blocks.
        */
        if((ret == 0) && (ulRemaining >= REDCONF_BLOCK_SIZE))
        {
            uint32_t ulBlockOffset = (uint32_t)((ullStart + ulWriteIndex) >> BLOCK_SIZE_P2);
            uint32_t ulBlockCount = ulRemaining >> BLOCK_SIZE_P2;
            uint32_t ulBlocksWritten = ulBlockCount;

            REDASSERT(((ullStart + ulWriteIndex) & (REDCONF_BLOCK_SIZE - 1U)) == 0U);

            ret = WriteAligned(pInode, ulBlockOffset, &ulBlocksWritten, &pbBuffer[ulWriteIndex]);

            if((ret == -RED_ENOSPC) && (ulWriteIndex > 0U))
            {
                ulBlocksWritten = 0U;
                ret = 0;
            }

            if(ret == 0)
            {
                ulWriteIndex += ulBlocksWritten << BLOCK_SIZE_P2;
                ulRemaining -= ulBlocksWritten << BLOCK_SIZE_P2;

                if(ulBlocksWritten < ulBlockCount)
                {
                    ulRemaining = 0U;
                }
            }
        }

        /*  Partial block at end.
        */
        if((ret == 0) && (ulRemaining > 0U))
        {
            REDASSERT(ulRemaining < REDCONF_BLOCK_SIZE);
            REDASSERT(((ullStart + ulWriteIndex) & (REDCONF_BLOCK_SIZE - 1U)) == 0U);
            REDASSERT(ulWriteIndex > 0U);

            ret = WriteUnaligned(pInode, ullStart + ulWriteIndex, ulRemaining, &pbBuffer[ulWriteIndex]);

            if(ret == -RED_ENOSPC)
            {
                ret = 0;
            }
            else if(ret == 0)
            {
                ulWriteIndex += ulRemaining;

                REDASSERT(ulWriteIndex == ulLen);
            }
            else
            {
                /*  Unexpected error, return it.
                */
            }
        }

        if(ret == 0)
        {
            *pulLen = ulWriteIndex;

            if((ullStart + ulWriteIndex) > pInode->pInodeBuf->ullSize)
            {
                pInode->pInodeBuf->ullSize = ullStart + ulWriteIndex;
            }
        }
    }

    return ret;
}


#if DELETE_SUPPORTED || TRUNCATE_SUPPORTED
/** @brief Change the size of an inode.

    @param pInode   A pointer to the cached inode structure.
    @praam ullSize  The new file size for the inode.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EFBIG  @p ullSize is greater than the maximum file size.
    @retval -RED_EINVAL @p pInode is not a mounted cached inode pointer.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_ENOSPC Insufficient free space to perform the truncate.
*/
REDSTATUS RedInodeDataTruncate(
    CINODE     *pInode,
    uint64_t    ullSize)
{
    REDSTATUS   ret = 0;

    /*  The inode does not need to be dirtied when it is being deleted, because
        the inode buffer will be discarded without ever being written to disk.
        Thus, we only check to see if it's mounted here.
    */
    if(!CINODE_IS_MOUNTED(pInode))
    {
        ret = -RED_EINVAL;
    }
    else if(ullSize > INODE_SIZE_MAX)
    {
        ret = -RED_EFBIG;
    }
    else
    {
        if(ullSize > pInode->pInodeBuf->ullSize)
        {
            ret = ExpandPrepare(pInode);
        }
        else if(ullSize < pInode->pInodeBuf->ullSize)
        {
            ret = Shrink(pInode, ullSize);
        }
        else
        {
            /*  Size is staying the same, nothing to do.
            */
        }

        if(ret == 0)
        {
            pInode->pInodeBuf->ullSize = ullSize;
        }
    }

    return ret;
}


/** @brief Free all file data beyond a specified point.

    @param pInode   A pointer to the cached inode structure.
    @param ullSize  The point beyond which to free all file data.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_ENOSPC Insufficient free space to perform the truncate.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS Shrink(
    CINODE     *pInode,
    uint64_t    ullSize)
{
    REDSTATUS   ret = 0;

    /*  pInode->fDirty is checked explicitly here, instead of using the
        CINODE_IS_DIRTY() macro, to avoid a duplicate mount check.
    */
    if(!CINODE_IS_MOUNTED(pInode) || ((ullSize > 0U) && !pInode->fDirty))
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        uint32_t ulTruncBlock = (uint32_t)((ullSize + REDCONF_BLOCK_SIZE - 1U) >> BLOCK_SIZE_P2);

        RedInodePutData(pInode);

      #if REDCONF_DIRECT_POINTERS > 0U
        while(ulTruncBlock < REDCONF_DIRECT_POINTERS)
        {
            ret = TruncDataBlock(pInode, &pInode->pInodeBuf->aulEntries[ulTruncBlock], true);

            if(ret != 0)
            {
                break;
            }

            ulTruncBlock++;
        }
      #endif

      #if REDCONF_INDIRECT_POINTERS > 0U
        while((ret == 0) && (ulTruncBlock < (REDCONF_DIRECT_POINTERS + INODE_INDIR_BLOCKS)))
        {
            ret = RedInodeDataSeek(pInode, ulTruncBlock);

            if((ret == 0) || (ret == -RED_ENODATA))
            {
                bool fFreed;

                ret = TruncIndir(pInode, &fFreed);

                if(ret == 0)
                {
                    if(fFreed)
                    {
                        pInode->pInodeBuf->aulEntries[pInode->uInodeEntry] = BLOCK_SPARSE;
                    }

                    /*  The next seek will go to the beginning of the next
                        indirect.
                    */
                    ulTruncBlock += (INDIR_ENTRIES - pInode->uIndirEntry);
                }
            }
        }
      #endif

      #if DINDIR_POINTERS > 0U
        while((ret == 0) && (ulTruncBlock < INODE_DATA_BLOCKS))
        {
            ret = RedInodeDataSeek(pInode, ulTruncBlock);

            if((ret == 0) || (ret == -RED_ENODATA))
            {
                bool fFreed;

                /*  TruncDindir() invokes seek as it goes along, which will
                    update the entry values (possibly all three of these);
                    make a copy so we can compute things correctly after.
                */
                uint16_t uOrigInodeEntry = pInode->uInodeEntry;
                uint16_t uOrigDindirEntry = pInode->uDindirEntry;
                uint16_t uOrigIndirEntry = pInode->uIndirEntry;

                ret = TruncDindir(pInode, &fFreed);

                if(ret == 0)
                {
                    if(fFreed)
                    {
                        pInode->pInodeBuf->aulEntries[uOrigInodeEntry] = BLOCK_SPARSE;
                    }

                    /*  The next seek will go to the beginning of the next
                        double indirect.
                    */
                    ulTruncBlock += (DINDIR_DATA_BLOCKS - (uOrigDindirEntry * INDIR_ENTRIES)) - uOrigIndirEntry;
                }
            }
        }
      #endif
    }

    return ret;
}


#if DINDIR_POINTERS > 0U
/** @brief Truncate a double indirect.

    @param pInode   A pointer to the cached inode, whose coordinates indicate
                    the truncation boundary.
    @param pfFreed  On successful return, populated with whether the double
                    indirect node was freed.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_ENOSPC Insufficient free space to perform the truncate.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS TruncDindir(
    CINODE     *pInode,
    bool       *pfFreed)
{
    REDSTATUS   ret = 0;

    if(!CINODE_IS_MOUNTED(pInode) || (pfFreed == NULL))
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else if(pInode->pDindir == NULL)
    {
        *pfFreed = false;
    }
    else
    {
        bool        fBranch = false;
        uint16_t    uEntry;

        /*  The double indirect is definitely going to be branched (instead of
            deleted) if any of its indirect pointers which are entirely prior to
            the truncation boundary are non-sparse.
        */
        for(uEntry = 0U; !fBranch && (uEntry < pInode->uDindirEntry); uEntry++)
        {
            fBranch = pInode->pDindir->aulEntries[uEntry] != BLOCK_SPARSE;
        }

        /*  Unless we already know for a fact that the double indirect is going
            to be branched, examine the contents of the indirect pointer which
            straddles the truncation boundary.  If the indirect is going to be
            deleted, we know this indirect pointer is going away, and that might
            mean the double indirect is going to be deleted also.
        */
        if(!fBranch && (pInode->pDindir->aulEntries[pInode->uDindirEntry] != BLOCK_SPARSE))
        {
            for(uEntry = 0U; !fBranch && (uEntry < pInode->uIndirEntry); uEntry++)
            {
                fBranch = pInode->pIndir->aulEntries[uEntry] != BLOCK_SPARSE;
            }
        }

        if(fBranch)
        {
            ret = BranchBlock(pInode, BRANCHDEPTH_DINDIR, false);
        }

        if(ret == 0)
        {
            uint32_t ulBlock = pInode->ulLogicalBlock;
            uint16_t uStart = pInode->uDindirEntry; /* pInode->uDindirEntry will change. */

            for(uEntry = uStart; uEntry < INDIR_ENTRIES; uEntry++)
            {
                /*  Seek so that TruncIndir() has the correct indirect
                    buffer and indirect entry.
                */
                ret = RedInodeDataSeek(pInode, ulBlock);

                if(ret == -RED_ENODATA)
                {
                    ret = 0;
                }

                if((ret == 0) && (pInode->ulIndirBlock != BLOCK_SPARSE))
                {
                    bool fIndirFreed;

                    ret = TruncIndir(pInode, &fIndirFreed);

                    if(ret == 0)
                    {
                        /*  All of the indirects after the one which straddles
                            the truncation boundary should definitely end up
                            deleted.
                        */
                        REDASSERT((uEntry == uStart) || fIndirFreed);

                        /*  If the double indirect is being freed, all of the
                            indirects should be freed too.
                        */
                        REDASSERT(fIndirFreed || fBranch);

                        if(fBranch && fIndirFreed)
                        {
                            pInode->pDindir->aulEntries[uEntry] = BLOCK_SPARSE;
                        }
                    }
                }

                if(ret != 0)
                {
                    break;
                }

                ulBlock += (INDIR_ENTRIES - pInode->uIndirEntry);
            }

            if(ret == 0)
            {
                *pfFreed = !fBranch;

                if(!fBranch)
                {
                    RedInodePutDindir(pInode);

                    ret = RedImapBlockSet(pInode->ulDindirBlock, false);
                }
            }
        }
    }

    return ret;
}
#endif /* DINDIR_POINTERS > 0U */


#if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
/** @brief Truncate a indirect.

    @param pInode   A pointer to the cached inode, whose coordinates indicate
                    the truncation boundary.
    @param pfFreed  On successful return, populated with whether the indirect
                    node was freed.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_ENOSPC Insufficient free space to perform the truncate.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS TruncIndir(
    CINODE     *pInode,
    bool       *pfFreed)
{
    REDSTATUS   ret = 0;

    if(!CINODE_IS_MOUNTED(pInode) || (pfFreed == NULL))
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else if(pInode->pIndir == NULL)
    {
        *pfFreed = false;
    }
    else
    {
        bool        fBranch = false;
        uint16_t    uEntry;

        /*  Scan the range of entries which are not being truncated.  If there
            is anything there, then the indirect will not be empty after the
            truncate, so it is branched and modified instead of deleted.
        */
        for(uEntry = 0U; !fBranch && (uEntry < pInode->uIndirEntry); uEntry++)
        {
            fBranch = pInode->pIndir->aulEntries[uEntry] != BLOCK_SPARSE;
        }

        if(fBranch)
        {
            ret = BranchBlock(pInode, BRANCHDEPTH_INDIR, false);
        }

        if(ret == 0)
        {
            for(uEntry = pInode->uIndirEntry; uEntry < INDIR_ENTRIES; uEntry++)
            {
                ret = TruncDataBlock(pInode, &pInode->pIndir->aulEntries[uEntry], fBranch);

                if(ret != 0)
                {
                    break;
                }
            }

            if(ret == 0)
            {
                *pfFreed = !fBranch;

                if(!fBranch)
                {
                    RedInodePutIndir(pInode);

                    ret = RedImapBlockSet(pInode->ulIndirBlock, false);
                }
            }
        }
    }

    return ret;
}
#endif /* REDCONF_DIRECT_POINTERS < INODE_ENTRIES */


/** @brief Truncate a file data block.

    @param pInode       A pointer to the cached inode structure.
    @param pulBlock     On entry, contains the block to be truncated.  On
                        successful return, if @p fPropagate is true, populated
                        with BLOCK_SPARSE, otherwise unmodified.
    @param fPropagate   Whether the parent node is being branched.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS TruncDataBlock(
    const CINODE   *pInode,
    uint32_t       *pulBlock,
    bool            fPropagate)
{
    REDSTATUS       ret = 0;

    if(!CINODE_IS_MOUNTED(pInode) || (pulBlock == NULL))
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else if(*pulBlock != BLOCK_SPARSE)
    {
        ret = RedImapBlockSet(*pulBlock, false);

      #if REDCONF_INODE_BLOCKS == 1
        if(ret == 0)
        {
            if(pInode->pInodeBuf->ulBlocks == 0U)
            {
                CRITICAL_ERROR();
                ret = -RED_EFUBAR;
            }
            else
            {
                pInode->pInodeBuf->ulBlocks--;
            }
        }
      #endif

        if((ret == 0) && fPropagate)
        {
            *pulBlock = BLOCK_SPARSE;
        }
    }
    else
    {
        /*  Data block is sparse, nothing to truncate.
        */
    }

    return ret;
}
#endif /* DELETE_SUPPORTED || TRUNCATE_SUPPORTED */


/** @brief Prepare to increase the file size.

    When the inode size is increased, a sparse region is created.  It is
    possible that a prior shrink operation to an unaligned size left stale data
    beyond the end of the file in the last data block.  That data is not zeroed
    while shrinking the inode in order to transfer the disk full burden from the
    shrink operation to the expand operation.

    @param pInode   A pointer to the cached inode structure.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_ENOSPC Insufficient free space to perform the truncate.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS ExpandPrepare(
    CINODE     *pInode)
{
    REDSTATUS   ret = 0;

    if(!CINODE_IS_DIRTY(pInode))
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        uint32_t ulOldSizeByteInBlock = (uint32_t)(pInode->pInodeBuf->ullSize & (REDCONF_BLOCK_SIZE - 1U));

        if(ulOldSizeByteInBlock != 0U)
        {
            ret = RedInodeDataSeek(pInode, (uint32_t)(pInode->pInodeBuf->ullSize >> BLOCK_SIZE_P2));

            if(ret == -RED_ENODATA)
            {
                ret = 0;
            }
            else if(ret == 0)
            {
                ret = BranchBlock(pInode, BRANCHDEPTH_FILE_DATA, true);

                if(ret == 0)
                {
                    RedMemSet(&pInode->pbData[ulOldSizeByteInBlock], 0U, REDCONF_BLOCK_SIZE - ulOldSizeByteInBlock);
                }
            }
            else
            {
                REDERROR();
            }
        }
    }

    return ret;
}
#endif /* REDCONF_READ_ONLY == 0 */


/** @brief Seek to a given position within an inode, then buffer the data block.

    On successful return, pInode->pbData will be populated with a buffer
    corresponding to the @p ulBlock block offset.

    @param pInode   A pointer to the cached inode structure.
    @param ulBlock  The block offset to seek to and buffer.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0               Operation was successful.
    @retval -RED_ENODATA    The block offset is sparse.
    @retval -RED_EINVAL     @p ulBlock is too large.
    @retval -RED_EIO        A disk I/O error occurred.
*/
REDSTATUS RedInodeDataSeekAndRead(
    CINODE     *pInode,
    uint32_t    ulBlock)
{
    REDSTATUS   ret;

    ret = RedInodeDataSeek(pInode, ulBlock);

    if((ret == 0) && (pInode->pbData == NULL))
    {
        REDASSERT(pInode->ulDataBlock != BLOCK_SPARSE);

        ret = RedBufferGet(pInode->ulDataBlock, 0U, CAST_VOID_PTR_PTR(&pInode->pbData));
    }

    return ret;
}


/** @brief Seek to a given position within an inode.

    On successful return, pInode->ulDataBlock will be populated with the
    physical block number corresponding to the @p ulBlock block offset.

    Note: Callers of this function depend on its parameter checking.

    @param pInode   A pointer to the cached inode structure.
    @param ulBlock  The block offset to seek to.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0               Operation was successful.
    @retval -RED_ENODATA    The block offset is sparse.
    @retval -RED_EINVAL     @p ulBlock is too large; or @p pInode is not a
                            mounted cached inode pointer.
    @retval -RED_EIO        A disk I/O error occurred.
*/
REDSTATUS RedInodeDataSeek(
    CINODE     *pInode,
    uint32_t    ulBlock)
{
    REDSTATUS   ret = 0;

    if(!CINODE_IS_MOUNTED(pInode) || (ulBlock >= INODE_DATA_BLOCKS))
    {
        ret = -RED_EINVAL;
    }
    else
    {
        SeekCoord(pInode, ulBlock);

      #if DINDIR_POINTERS > 0U
        if(pInode->uDindirEntry != COORD_ENTRY_INVALID)
        {
            if(pInode->ulDindirBlock == BLOCK_SPARSE)
            {
                /*  If the double indirect is unallocated, so is the indirect.
                */
                pInode->ulIndirBlock = BLOCK_SPARSE;
            }
            else
            {
                if(pInode->pDindir == NULL)
                {
                    ret = RedBufferGet(pInode->ulDindirBlock, BFLAG_META_DINDIR, CAST_VOID_PTR_PTR(&pInode->pDindir));
                }

                if(ret == 0)
                {
                    pInode->ulIndirBlock = pInode->pDindir->aulEntries[pInode->uDindirEntry];
                }
            }
        }
      #endif

      #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
        if((ret == 0) && (pInode->uIndirEntry != COORD_ENTRY_INVALID))
        {
            if(pInode->ulIndirBlock == BLOCK_SPARSE)
            {
                /*  If the indirect is unallocated, so is the data block.
                */
                pInode->ulDataBlock = BLOCK_SPARSE;
            }
            else
            {
                if(pInode->pIndir == NULL)
                {
                    ret = RedBufferGet(pInode->ulIndirBlock, BFLAG_META_INDIR, CAST_VOID_PTR_PTR(&pInode->pIndir));
                }

                if(ret == 0)
                {
                    pInode->ulDataBlock = pInode->pIndir->aulEntries[pInode->uIndirEntry];
                }
            }
        }
      #endif

        if((ret == 0) && (pInode->ulDataBlock == BLOCK_SPARSE))
        {
            ret = -RED_ENODATA;
        }
    }

    return ret;
}


/** @brief Seek to the coordinates.

    Compute the new coordinates, and put any buffers which are not needed or are
    no longer appropriate.

    @param pInode   A pointer to the cached inode structure.
    @param ulBlock  The block offset to seek to.
*/
static void SeekCoord(
    CINODE     *pInode,
    uint32_t    ulBlock)
{
    if(!CINODE_IS_MOUNTED(pInode) || (ulBlock >= INODE_DATA_BLOCKS))
    {
        REDERROR();
    }
    else if((pInode->ulLogicalBlock != ulBlock) || !pInode->fCoordInited)
    {
        RedInodePutData(pInode);
        pInode->ulLogicalBlock = ulBlock;

      #if REDCONF_DIRECT_POINTERS > 0U
      #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
        if(ulBlock < REDCONF_DIRECT_POINTERS)
      #endif
        {
          #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
            RedInodePutCoord(pInode);
          #endif

            pInode->uInodeEntry = (uint16_t)ulBlock;
            pInode->ulDataBlock = pInode->pInodeBuf->aulEntries[pInode->uInodeEntry];

          #if DINDIR_POINTERS > 0U
            pInode->uDindirEntry = COORD_ENTRY_INVALID;
          #endif
          #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
            pInode->uIndirEntry = COORD_ENTRY_INVALID;
          #endif
        }
      #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
        else
      #endif
      #endif
      #if REDCONF_INDIRECT_POINTERS > 0U
      #if REDCONF_INDIRECT_POINTERS < INODE_ENTRIES
        if(ulBlock < (INODE_INDIR_BLOCKS + REDCONF_DIRECT_POINTERS))
      #endif
        {
            uint32_t ulIndirRangeOffset = ulBlock - REDCONF_DIRECT_POINTERS;
            uint16_t uInodeEntry = (uint16_t)((ulIndirRangeOffset / INDIR_ENTRIES) + REDCONF_DIRECT_POINTERS);
            uint16_t uIndirEntry = (uint16_t)(ulIndirRangeOffset % INDIR_ENTRIES);

          #if DINDIR_POINTERS > 0U
            RedInodePutDindir(pInode);
          #endif

            /*  If the inode entry is not changing, then the previous indirect
                is still the correct one.  Otherwise, the old indirect will be
                released and the new one will be read later.
            */
            if((pInode->uInodeEntry != uInodeEntry) || !pInode->fCoordInited)
            {
                RedInodePutIndir(pInode);

                pInode->uInodeEntry = uInodeEntry;

                pInode->ulIndirBlock = pInode->pInodeBuf->aulEntries[pInode->uInodeEntry];
            }

          #if DINDIR_POINTERS > 0U
            pInode->uDindirEntry = COORD_ENTRY_INVALID;
          #endif
            pInode->uIndirEntry = uIndirEntry;

            /*  At this point, the following pInode members are needed but not
                yet populated:

                - pIndir
                - ulDataBlock
            */
        }
      #if DINDIR_POINTERS > 0U
        else
      #endif
      #endif
      #if DINDIR_POINTERS > 0U
        {
            uint32_t ulDindirRangeOffset = (ulBlock - REDCONF_DIRECT_POINTERS) - INODE_INDIR_BLOCKS;
            uint16_t uInodeEntry = (uint16_t)((ulDindirRangeOffset / DINDIR_DATA_BLOCKS) + REDCONF_DIRECT_POINTERS + REDCONF_INDIRECT_POINTERS);
            uint32_t ulDindirNodeOffset = ulDindirRangeOffset % DINDIR_DATA_BLOCKS;
            uint16_t uDindirEntry = (uint16_t)(ulDindirNodeOffset / INDIR_ENTRIES);
            uint16_t uIndirEntry = (uint16_t)(ulDindirNodeOffset % INDIR_ENTRIES);

            /*  If the inode entry is not changing, then the previous double
                indirect is still the correct one.  Otherwise, the old double
                indirect will be released and the new one will be read later.
            */
            if((pInode->uInodeEntry != uInodeEntry) || !pInode->fCoordInited)
            {
                RedInodePutIndir(pInode);
                RedInodePutDindir(pInode);

                pInode->uInodeEntry = uInodeEntry;

                pInode->ulDindirBlock = pInode->pInodeBuf->aulEntries[pInode->uInodeEntry];
            }
            /*  If neither the inode entry nor double indirect entry are
                changing, then the previous indirect is still the correct one.
                Otherwise, it old indirect will be released and the new one will
                be read later.
            */
            else if(pInode->uDindirEntry != uDindirEntry)
            {
                RedInodePutIndir(pInode);
            }
            else
            {
                /*  Data buffer has already been put, nothing to do.
                */
            }

            pInode->uDindirEntry = uDindirEntry;
            pInode->uIndirEntry = uIndirEntry;

            /*  At this point, the following pInode members are needed but not
                yet populated:

                - pDindir
                - pIndir
                - ulIndirBlock
                - ulDataBlock
            */
        }
      #elif (REDCONF_DIRECT_POINTERS > 0U) && (REDCONF_INDIRECT_POINTERS > 0U)
        else
        {
            /*  There are no double indirects, so the block should have been in
                the direct or indirect range.
            */
            REDERROR();
        }
      #endif

        pInode->fCoordInited = true;
    }
    else
    {
        /*  Seeking to the current position, nothing to do.
        */
    }
}


/** @brief Read an unaligned portion of a block.

    @param pInode   A pointer to the cached inode structure.
    @param ullStart The file offset at which to read.
    @param ulLen    The number of bytes to read.
    @param pbBuffer The buffer to read into.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS ReadUnaligned(
    CINODE     *pInode,
    uint64_t    ullStart,
    uint32_t    ulLen,
    uint8_t    *pbBuffer)
{
    REDSTATUS   ret;

    /*  This read should not cross a block boundary.
    */
    if(    ((ullStart >> BLOCK_SIZE_P2) != (((ullStart + ulLen) - 1U) >> BLOCK_SIZE_P2))
        || (pbBuffer == NULL))
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        ret = RedInodeDataSeekAndRead(pInode, (uint32_t)(ullStart >> BLOCK_SIZE_P2));

        if(ret == 0)
        {
            RedMemCpy(pbBuffer, &pInode->pbData[ullStart & (REDCONF_BLOCK_SIZE - 1U)], ulLen);
        }
        else if(ret == -RED_ENODATA)
        {
            /*  Sparse block, return zeroed data.
            */
            RedMemSet(pbBuffer, 0U, ulLen);
            ret = 0;
        }
        else
        {
            /*  No action, just return the error.
            */
        }
    }

    return ret;
}


/** @brief Read one or more whole blocks.

    @param pInode       A pointer to the cached inode structure.
    @param ulBlockStart The file block offset at which to read.
    @param ulBlockCount The number of blocks to read.
    @param pbBuffer     The buffer to read into.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS ReadAligned(
    CINODE     *pInode,
    uint32_t    ulBlockStart,
    uint32_t    ulBlockCount,
    uint8_t    *pbBuffer)
{
    REDSTATUS   ret = 0;

    if(pbBuffer == NULL)
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        uint32_t ulBlockIndex = 0U;

        /*  Read the data from disk one contiguous extent at a time.
        */
        while((ret == 0) && (ulBlockIndex < ulBlockCount))
        {
            uint32_t ulExtentStart;
            uint32_t ulExtentLen = ulBlockCount - ulBlockIndex;

            ret = GetExtent(pInode, ulBlockStart + ulBlockIndex, &ulExtentStart, &ulExtentLen);

            if(ret == 0)
            {
              #if REDCONF_READ_ONLY == 0
                /*  Before reading directly from disk, flush any dirty file data
                    buffers in the range to avoid reading stale data.
                */
                ret = RedBufferFlush(ulExtentStart, ulExtentLen);

                if(ret == 0)
              #endif
                {
                    ret = RedIoRead(gbRedVolNum, ulExtentStart, ulExtentLen, &pbBuffer[ulBlockIndex << BLOCK_SIZE_P2]);

                    if(ret == 0)
                    {
                        ulBlockIndex += ulExtentLen;
                    }
                }
            }
            else if(ret == -RED_ENODATA)
            {
                /*  Sparse block, return zeroed data.
                */
                RedMemSet(&pbBuffer[ulBlockIndex << BLOCK_SIZE_P2], 0U, REDCONF_BLOCK_SIZE);
                ulBlockIndex++;
                ret = 0;
            }
            else
            {
                /*  An unexpected error occurred; the loop will terminate.
                */
            }
        }
    }

    return ret;
}


#if REDCONF_READ_ONLY == 0
/** @brief Write an unaligned portion of a block.

    @param pInode   A pointer to the cached inode structure.
    @param ullStart The file offset at which to write.
    @param ulLen    The number of bytes to write.
    @param pbBuffer The buffer to write from.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_ENOSPC No data can be written because there is insufficient
                        free space.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS WriteUnaligned(
    CINODE         *pInode,
    uint64_t        ullStart,
    uint32_t        ulLen,
    const uint8_t  *pbBuffer)
{
    REDSTATUS       ret;

    /*  This write should not cross a block boundary.
    */
    if(    ((ullStart >> BLOCK_SIZE_P2) != (((ullStart + ulLen) - 1U) >> BLOCK_SIZE_P2))
        || (pbBuffer == NULL))
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        ret = RedInodeDataSeek(pInode, (uint32_t)(ullStart >> BLOCK_SIZE_P2));

        if((ret == 0) || (ret == -RED_ENODATA))
        {
            ret = BranchBlock(pInode, BRANCHDEPTH_FILE_DATA, true);

            if(ret == 0)
            {
                RedMemCpy(&pInode->pbData[ullStart & (REDCONF_BLOCK_SIZE - 1U)], pbBuffer, ulLen);
            }
        }
    }

    return ret;
}


/** @brief Write one or more whole blocks.

    @param pInode           A pointer to the cached inode structure.
    @param ulBlockStart     The file block offset at which to write.
    @param pulBlockCount    On entry, the number of blocks to attempt to write.
                            On successful return, the number of blocks actually
                            written.
    @param pbBuffer         The buffer to write from.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_ENOSPC No data can be written because there is insufficient
                        free space.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS WriteAligned(
    CINODE         *pInode,
    uint32_t        ulBlockStart,
    uint32_t       *pulBlockCount,
    const uint8_t  *pbBuffer)
{
    REDSTATUS       ret = 0;

    if((pulBlockCount == NULL) || (pbBuffer == NULL))
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        bool     fFull = false;
        uint32_t ulBlockCount = *pulBlockCount;
        uint32_t ulBlockIndex;

        /*  Branch all of the file data blocks in advance.
        */
        for(ulBlockIndex = 0U; (ulBlockIndex < ulBlockCount) && !fFull; ulBlockIndex++)
        {
            ret = RedInodeDataSeek(pInode, ulBlockStart + ulBlockIndex);

            if((ret == 0) || (ret == -RED_ENODATA))
            {
                ret = BranchBlock(pInode, BRANCHDEPTH_FILE_DATA, false);

                if(ret == -RED_ENOSPC)
                {
                    if(ulBlockIndex > 0U)
                    {
                        ret = 0;
                    }

                    fFull = true;
                }
            }

            if(ret != 0)
            {
                break;
            }
        }

        ulBlockCount = ulBlockIndex;
        ulBlockIndex = 0U;

        if(fFull)
        {
            ulBlockCount--;
        }

        /*  Write the data to disk one contiguous extent at a time.
        */
        while((ret == 0) && (ulBlockIndex < ulBlockCount))
        {
            uint32_t ulExtentStart;
            uint32_t ulExtentLen = ulBlockCount - ulBlockIndex;

            ret = GetExtent(pInode, ulBlockStart + ulBlockIndex, &ulExtentStart, &ulExtentLen);

            if(ret == 0)
            {
                ret = RedIoWrite(gbRedVolNum, ulExtentStart, ulExtentLen, &pbBuffer[ulBlockIndex << BLOCK_SIZE_P2]);

                if(ret == 0)
                {
                    /*  If there is any buffered file data for the extent we
                        just wrote, those buffers are now stale.
                    */
                    ret = RedBufferDiscardRange(ulExtentStart, ulExtentLen);
                }

                if(ret == 0)
                {
                    ulBlockIndex += ulExtentLen;
                }
            }
        }

        if(ret == 0)
        {
            *pulBlockCount = ulBlockCount;
        }
    }

    return ret;
}
#endif /* REDCONF_READ_ONLY == 0 */


/** @brief Get the physical block number and count of contiguous blocks given a
           starting logical block number.

    @param pInode           A pointer to the cached inode structure.
    @param ulBlockStart     The file block offset for the start of the extent.
    @param pulExtentStart   On successful return, the starting physical block
                            number of the contiguous extent.
    @param pulExtentLen     On entry, the maximum length of the extent; on
                            successful return, the length of the contiguous
                            extent.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0               Operation was successful.
    @retval -RED_EIO        A disk I/O error occurred.
    @retval -RED_ENODATA    The block offset is sparse.
    @retval -RED_EINVAL     Invalid parameters.
*/
static REDSTATUS GetExtent(
    CINODE     *pInode,
    uint32_t    ulBlockStart,
    uint32_t   *pulExtentStart,
    uint32_t   *pulExtentLen)
{
    REDSTATUS   ret;

    if((pulExtentStart == NULL) || (pulExtentLen == NULL))
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        ret = RedInodeDataSeek(pInode, ulBlockStart);

        if(ret == 0)
        {
            uint32_t ulExtentLen = *pulExtentLen;
            uint32_t ulFirstBlock = pInode->ulDataBlock;
            uint32_t ulRunLen = 1U;

            while((ret == 0) && (ulRunLen < ulExtentLen))
            {
                ret = RedInodeDataSeek(pInode, ulBlockStart + ulRunLen);

                /*  The extent ends when we find a sparse data block or when the
                    data block is not contiguous with the preceding data block.
                */
                if((ret == -RED_ENODATA) || ((ret == 0) && (pInode->ulDataBlock != (ulFirstBlock + ulRunLen))))
                {
                    ret = 0;
                    break;
                }

                ulRunLen++;
            }

            if(ret == 0)
            {
                *pulExtentStart = ulFirstBlock;
                *pulExtentLen = ulRunLen;
            }
        }
    }

    return ret;
}


#if REDCONF_READ_ONLY == 0
/** @brief Allocate or branch the file metadata path and data block if necessary.

    Optionally, can stop allocating/branching at a certain depth.

    @param pInode   A pointer to the cached inode structure.
    @param depth    A BRANCHDEPTH_ value indicating the lowest depth to branch.
    @param fBuffer  Whether to buffer the data block.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_ENOSPC No data can be written because there is insufficient
                        free space.
*/
static REDSTATUS BranchBlock(
    CINODE     *pInode,
    BRANCHDEPTH depth,
    bool        fBuffer)
{
    REDSTATUS   ret;
    uint32_t    ulCost = 0U; /* Init'd to quiet warnings. */

    ret = BranchBlockCost(pInode, depth, &ulCost);

    if((ret == 0) && (ulCost > FreeBlockCount()))
    {
        ret = -RED_ENOSPC;
    }

    if(ret == 0)
    {
      #if DINDIR_POINTERS > 0U
        if(pInode->uDindirEntry != COORD_ENTRY_INVALID)
        {
            ret = BranchOneBlock(&pInode->ulDindirBlock, CAST_VOID_PTR_PTR(&pInode->pDindir), BFLAG_META_DINDIR);

            if(ret == 0)
            {
                /*  In case we just created the double indirect.
                */
                pInode->pDindir->ulInode = pInode->ulInode;

                pInode->pInodeBuf->aulEntries[pInode->uInodeEntry] = pInode->ulDindirBlock;
            }
        }

        if(ret == 0)
      #endif
      #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
        {
            if((pInode->uIndirEntry != COORD_ENTRY_INVALID) && (depth >= BRANCHDEPTH_INDIR))
            {
                ret = BranchOneBlock(&pInode->ulIndirBlock, CAST_VOID_PTR_PTR(&pInode->pIndir), BFLAG_META_INDIR);

                if(ret == 0)
                {
                    /*  In case we just created the indirect.
                    */
                    pInode->pIndir->ulInode = pInode->ulInode;

                  #if DINDIR_POINTERS > 0U
                    if(pInode->uDindirEntry != COORD_ENTRY_INVALID)
                    {
                        pInode->pDindir->aulEntries[pInode->uDindirEntry] = pInode->ulIndirBlock;
                    }
                    else
                  #endif
                    {
                        pInode->pInodeBuf->aulEntries[pInode->uInodeEntry] = pInode->ulIndirBlock;
                    }
                }
            }
        }

        if(ret == 0)
      #endif
        {
            if(depth == BRANCHDEPTH_FILE_DATA)
            {
              #if REDCONF_INODE_BLOCKS == 1
                bool    fAllocedNew = (pInode->ulDataBlock == BLOCK_SPARSE);
              #endif
                void  **ppBufPtr = (fBuffer || (pInode->pbData != NULL)) ? CAST_VOID_PTR_PTR(&pInode->pbData) : NULL;

                ret = BranchOneBlock(&pInode->ulDataBlock, ppBufPtr, 0U);

                if(ret == 0)
                {
                  #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
                    if(pInode->uIndirEntry != COORD_ENTRY_INVALID)
                    {
                        pInode->pIndir->aulEntries[pInode->uIndirEntry] = pInode->ulDataBlock;
                    }
                    else
                  #endif
                    {
                        pInode->pInodeBuf->aulEntries[pInode->uInodeEntry] = pInode->ulDataBlock;
                    }

                  #if REDCONF_INODE_BLOCKS == 1
                    if(fAllocedNew)
                    {
                        if(pInode->pInodeBuf->ulBlocks < INODE_DATA_BLOCKS)
                        {
                            pInode->pInodeBuf->ulBlocks++;
                        }
                        else
                        {
                            CRITICAL_ERROR();
                            ret = -RED_EFUBAR;
                        }
                    }
                  #endif
                }
            }
        }

        CRITICAL_ASSERT(ret == 0);
    }

    return ret;
}


/** @brief Branch a block.

    The block can be a double indirect, indirect, or file data block.

    The caller should have already handled the disk full implications of
    branching this block.

    @param pulBlock On entry, the current block number, which may be
                    BLOCK_SPARSE if the block is to be newly allocated.  On
                    successful return, populated with the new block number,
                    which may be the same as the original block number if it
                    was not BLOCK_SPARSE and the block was already branched.
    @param ppBuffer If NULL, indicates that the caller does not want to buffer
                    the branched block.  If non-NULL, the caller does want the
                    branched block buffered, and the following is true:  On
                    entry, the current buffer for the block, if there is one, or
                    NULL if there is no buffer.  On successful exit, populated
                    with a buffer for the block, which will be dirty.  If the
                    block number is initially BLOCK_SPARSE, there should be no
                    buffer for the block.
    @param uBFlag   The buffer type flags: BFLAG_META_DINDIR, BFLAG_META_INDIR,
                    or zero for file data.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS BranchOneBlock(
    uint32_t   *pulBlock,
    void      **ppBuffer,
    uint16_t    uBFlag)
{
    REDSTATUS   ret = 0;

    if(pulBlock == NULL)
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        ALLOCSTATE  state = ALLOCSTATE_FREE;
        uint32_t    ulPrevBlock = *pulBlock;

        if(ulPrevBlock != BLOCK_SPARSE)
        {
            ret = RedImapBlockState(ulPrevBlock, &state);
        }

        if(ret == 0)
        {
            if(state == ALLOCSTATE_NEW)
            {
                /*  Block is already branched, so simply get it buffered dirty
                    if requested.
                */
                if(ppBuffer != NULL)
                {
                    if(*ppBuffer != NULL)
                    {
                        RedBufferDirty(*ppBuffer);
                    }
                    else
                    {
                        ret = RedBufferGet(ulPrevBlock, uBFlag | BFLAG_DIRTY, ppBuffer);
                    }
                }
            }
            else
            {
                /*  Block does not exist or is committed state, so allocate a
                    new block for the branch.
                */
                ret = RedImapAllocBlock(pulBlock);

                if(ret == 0)
                {
                    if(ulPrevBlock == BLOCK_SPARSE)
                    {
                        /*  Block did not exist previously, so just get it
                            buffered if requested.
                        */
                        if(ppBuffer != NULL)
                        {
                            if(*ppBuffer != NULL)
                            {
                                /*  How could there be an existing buffer when
                                    the block did not exist?
                                */
                                REDERROR();
                                ret = -RED_EINVAL;
                            }
                            else
                            {
                                ret = RedBufferGet(*pulBlock, (uint16_t)((uint32_t)uBFlag | BFLAG_NEW | BFLAG_DIRTY), ppBuffer);
                            }
                        }
                    }
                    else
                    {
                        /*  Branch the buffer for the committed state block to
                            the newly allocated location.
                        */
                        if(ppBuffer != NULL)
                        {
                            if(*ppBuffer == NULL)
                            {
                                ret = RedBufferGet(ulPrevBlock, uBFlag, ppBuffer);
                            }

                            if(ret == 0)
                            {
                                RedBufferBranch(*ppBuffer, *pulBlock);
                            }
                        }

                        /*  Mark the committed state block almost free.
                        */
                        if(ret == 0)
                        {
                            ret = RedImapBlockSet(ulPrevBlock, false);
                        }
                    }
                }
            }
        }
    }

    return ret;
}


/** @brief Compute the free space cost of branching a block.

    The caller must first use RedInodeDataSeek() to the block to be branched.

    @param pInode   A pointer to the cached inode structure, whose coordinates
                    indicate the block to be branched.
    @param depth    A BRANCHDEPTH_ value indicating how much of the file
                    metadata structure needs to be branched.
    @param pulCost  On successful return, populated with the number of blocks
                    that must be allocated from free space in order to branch
                    the given block.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_EINVAL Invalid parameters.
*/
static REDSTATUS BranchBlockCost(
    const CINODE   *pInode,
    BRANCHDEPTH     depth,
    uint32_t       *pulCost)
{
    REDSTATUS       ret = 0;

    if(!CINODE_IS_MOUNTED(pInode) || !pInode->fCoordInited || (depth > BRANCHDEPTH_MAX) || (pulCost == NULL))
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        ALLOCSTATE  state;

        /*  ulCost is initialized to the maximum number of blocks that could
            be branched, and decremented for every block we determine does not
            need to be branched.
        */
      #if DINDIR_POINTERS > 0U
        uint32_t    ulCost = 3U;
      #elif REDCONF_DIRECT_POINTERS < INODE_ENTRIES
        uint32_t    ulCost = 2U;
      #else
        uint32_t    ulCost = 1U;
      #endif

      #if DINDIR_POINTERS > 0U
        if(pInode->uDindirEntry != COORD_ENTRY_INVALID)
        {
            if(pInode->ulDindirBlock != BLOCK_SPARSE)
            {
                ret = RedImapBlockState(pInode->ulDindirBlock, &state);

                if((ret == 0) && (state == ALLOCSTATE_NEW))
                {
                    /*  Double indirect already branched.
                    */
                    ulCost--;
                }
            }
        }
        else
        {
            /*  At this inode offset there are no double indirects.
            */
            ulCost--;
        }

        if(ret == 0)
      #endif
      #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
        {
            if((pInode->uIndirEntry != COORD_ENTRY_INVALID) && (depth >= BRANCHDEPTH_INDIR))
            {
                if(pInode->ulIndirBlock != BLOCK_SPARSE)
                {
                    ret = RedImapBlockState(pInode->ulIndirBlock, &state);

                    if((ret == 0) && (state == ALLOCSTATE_NEW))
                    {
                        /*  Indirect already branched.
                        */
                        ulCost--;
                    }
                }
            }
            else
            {
                /*  Either not branching this deep, or at this inode offset
                    there are no indirects.
                */
                ulCost--;
            }
        }

        if(ret == 0)
      #endif
        {
            if(depth == BRANCHDEPTH_FILE_DATA)
            {
                if(pInode->ulDataBlock != BLOCK_SPARSE)
                {
                    ret = RedImapBlockState(pInode->ulDataBlock, &state);

                    if((ret == 0) && (state == ALLOCSTATE_NEW))
                    {
                        /*  File data block already branched.
                        */
                        ulCost--;

                        /*  If the file data block is branched, then its parent
                            nodes should be branched as well.
                        */
                        REDASSERT(ulCost == 0U);
                    }
                }
            }
            else
            {
                /*  Not branching this deep.
                */
                ulCost--;
            }
        }

        if(ret == 0)
        {
            *pulCost = ulCost;
        }
    }

    return ret;
}


/** @brief Yields the number of currently available free blocks.

    Accounts for reserved blocks, subtracting the number of reserved blocks if
    they are unavailable.

    @return Number of currently available free blocks.
*/
static uint32_t FreeBlockCount(void)
{
    uint32_t ulFreeBlocks = gpRedMR->ulFreeBlocks;

  #if RESERVED_BLOCKS > 0U
    if(!gpRedCoreVol->fUseReservedBlocks)
    {
        if(ulFreeBlocks >= RESERVED_BLOCKS)
        {
            ulFreeBlocks -= RESERVED_BLOCKS;
        }
        else
        {
            ulFreeBlocks = 0U;
        }
    }
  #endif

    return ulFreeBlocks;
}
#endif /* REDCONF_READ_ONLY == 0 */

