/*
    File:       MFSCore.c

    Contains:   Core MFS implementation for MFSLives.

    Written by: DTS

    Copyright:  Copyright (c) 2006 by Apple Computer, Inc., All Rights Reserved.

    Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple Computer, Inc.
                ("Apple") in consideration of your agreement to the following terms, and your
                use, installation, modification or redistribution of this Apple software
                constitutes acceptance of these terms.  If you do not agree with these terms,
                please do not use, install, modify or redistribute this Apple software.

                In consideration of your agreement to abide by the following terms, and subject
                to these terms, Apple grants you a personal, non-exclusive license, under Apple's
                copyrights in this original Apple software (the "Apple Software"), to use,
                reproduce, modify and redistribute the Apple Software, with or without
                modifications, in source and/or binary forms; provided that if you redistribute
                the Apple Software in its entirety and without modifications, you must retain
                this notice and the following text and disclaimers in all such redistributions of
                the Apple Software.  Neither the name, trademarks, service marks or logos of
                Apple Computer, Inc. may be used to endorse or promote products derived from the
                Apple Software without specific prior written permission from Apple.  Except as
                expressly stated in this notice, no other rights or licenses, express or implied,
                are granted by Apple herein, including but not limited to any patent rights that
                may be infringed by your derivative works or by other works in which the Apple
                Software may be incorporated.

                The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO
                WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
                WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
                PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
                COMBINATION WITH YOUR PRODUCTS.

                IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
                CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
                GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
                OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT
                (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
                ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    Change History (most recent first):

$Log: MFSCore.c,v $
Revision 1.1  2006/07/27 15:47:43  eskimo1
First checked in.


*/

/////////////////////////////////////////////////////////////////////

#include "MFSCore.h"

#if KERNEL
    #include <kern/assert.h>
#else
    #include <assert.h>
#endif
#include <string.h>
#include <sys/errno.h>
#include <sys/stat.h>
#include <libkern/OSByteOrder.h>

#if KERNEL
    #include <sys/utfconv.h>
#else
    // We need the utf8_decodestr routine, but it's not publically available 
    // to user space code.  So we implement our own version, but only for 
    // user space.

    #include "utf8_decodestr.h"

    // In kernel space, user space, offsetof comes <sys/_types.h>, which you get 
    // implicitly from an include above.  In user space, we have to explicitly 
    // bring it in from <stddef.h>.
    
    #include <stddef.h>
#endif

/////////////////////////////////////////////////////////////////////
#pragma mark ***** String Conversion Tables

// These tables are explained in detail in comments in the header.

// These tables were generated by the TableGenerator program (see "TableGenerator.c").

static const char * kMacRomanToUTF8[128] = {
    /* 0x80 */ /* U+0041 U+0308 */ "A\xCC\x88",    /* U+0041 U+030A */ "A\xCC\x8A",    /* U+0043 U+0327 */ "C\xCC\xA7",    /* U+0045 U+0301 */ "E\xCC\x81",    
    /* 0x84 */ /* U+004E U+0303 */ "N\xCC\x83",    /* U+004F U+0308 */ "O\xCC\x88",    /* U+0055 U+0308 */ "U\xCC\x88",    /* U+0061 U+0301 */ "a\xCC\x81",    
    /* 0x88 */ /* U+0061 U+0300 */ "a\xCC\x80",    /* U+0061 U+0302 */ "a\xCC\x82",    /* U+0061 U+0308 */ "a\xCC\x88",    /* U+0061 U+0303 */ "a\xCC\x83",    
    /* 0x8C */ /* U+0061 U+030A */ "a\xCC\x8A",    /* U+0063 U+0327 */ "c\xCC\xA7",    /* U+0065 U+0301 */ "e\xCC\x81",    /* U+0065 U+0300 */ "e\xCC\x80",    
    /* 0x90 */ /* U+0065 U+0302 */ "e\xCC\x82",    /* U+0065 U+0308 */ "e\xCC\x88",    /* U+0069 U+0301 */ "i\xCC\x81",    /* U+0069 U+0300 */ "i\xCC\x80",    
    /* 0x94 */ /* U+0069 U+0302 */ "i\xCC\x82",    /* U+0069 U+0308 */ "i\xCC\x88",    /* U+006E U+0303 */ "n\xCC\x83",    /* U+006F U+0301 */ "o\xCC\x81",    
    /* 0x98 */ /* U+006F U+0300 */ "o\xCC\x80",    /* U+006F U+0302 */ "o\xCC\x82",    /* U+006F U+0308 */ "o\xCC\x88",    /* U+006F U+0303 */ "o\xCC\x83",    
    /* 0x9C */ /* U+0075 U+0301 */ "u\xCC\x81",    /* U+0075 U+0300 */ "u\xCC\x80",    /* U+0075 U+0302 */ "u\xCC\x82",    /* U+0075 U+0308 */ "u\xCC\x88",    
    /* 0xA0 */ /* U+2020 */        "\xE2\x80\xA0", /* U+00B0 */        "\xC2\xB0",     /* U+00A2 */        "\xC2\xA2",     /* U+00A3 */        "\xC2\xA3",     
    /* 0xA4 */ /* U+00A7 */        "\xC2\xA7",     /* U+2022 */        "\xE2\x80\xA2", /* U+00B6 */        "\xC2\xB6",     /* U+00DF */        "\xC3\x9F",     
    /* 0xA8 */ /* U+00AE */        "\xC2\xAE",     /* U+00A9 */        "\xC2\xA9",     /* U+2122 */        "\xE2\x84\xA2", /* U+00B4 */        "\xC2\xB4",     
    /* 0xAC */ /* U+00A8 */        "\xC2\xA8",     /* U+003D U+0338 */ "=\xCC\xB8",    /* U+00C6 */        "\xC3\x86",     /* U+00D8 */        "\xC3\x98",     
    /* 0xB0 */ /* U+221E */        "\xE2\x88\x9E", /* U+00B1 */        "\xC2\xB1",     /* U+2264 */        "\xE2\x89\xA4", /* U+2265 */        "\xE2\x89\xA5", 
    /* 0xB4 */ /* U+00A5 */        "\xC2\xA5",     /* U+00B5 */        "\xC2\xB5",     /* U+2202 */        "\xE2\x88\x82", /* U+2211 */        "\xE2\x88\x91", 
    /* 0xB8 */ /* U+220F */        "\xE2\x88\x8F", /* U+03C0 */        "\xCF\x80",     /* U+222B */        "\xE2\x88\xAB", /* U+00AA */        "\xC2\xAA",     
    /* 0xBC */ /* U+00BA */        "\xC2\xBA",     /* U+03A9 */        "\xCE\xA9",     /* U+00E6 */        "\xC3\xA6",     /* U+00F8 */        "\xC3\xB8",     
    /* 0xC0 */ /* U+00BF */        "\xC2\xBF",     /* U+00A1 */        "\xC2\xA1",     /* U+00AC */        "\xC2\xAC",     /* U+221A */        "\xE2\x88\x9A", 
    /* 0xC4 */ /* U+0192 */        "\xC6\x92",     /* U+2248 */        "\xE2\x89\x88", /* U+2206 */        "\xE2\x88\x86", /* U+00AB */        "\xC2\xAB",     
    /* 0xC8 */ /* U+00BB */        "\xC2\xBB",     /* U+2026 */        "\xE2\x80\xA6", /* U+00A0 */        "\xC2\xA0",     /* U+0041 U+0300 */ "A\xCC\x80",    
    /* 0xCC */ /* U+0041 U+0303 */ "A\xCC\x83",    /* U+004F U+0303 */ "O\xCC\x83",    /* U+0152 */        "\xC5\x92",     /* U+0153 */        "\xC5\x93",     
    /* 0xD0 */ /* U+2013 */        "\xE2\x80\x93", /* U+2014 */        "\xE2\x80\x94", /* U+201C */        "\xE2\x80\x9C", /* U+201D */        "\xE2\x80\x9D", 
    /* 0xD4 */ /* U+2018 */        "\xE2\x80\x98", /* U+2019 */        "\xE2\x80\x99", /* U+00F7 */        "\xC3\xB7",     /* U+25CA */        "\xE2\x97\x8A", 
    /* 0xD8 */ /* U+0079 U+0308 */ "y\xCC\x88",    /* U+0059 U+0308 */ "Y\xCC\x88",    /* U+2044 */        "\xE2\x81\x84", /* U+20AC */        "\xE2\x82\xAC", 
    /* 0xDC */ /* U+2039 */        "\xE2\x80\xB9", /* U+203A */        "\xE2\x80\xBA", /* U+FB01 */        "\xEF\xAC\x81", /* U+FB02 */        "\xEF\xAC\x82", 
    /* 0xE0 */ /* U+2021 */        "\xE2\x80\xA1", /* U+00B7 */        "\xC2\xB7",     /* U+201A */        "\xE2\x80\x9A", /* U+201E */        "\xE2\x80\x9E", 
    /* 0xE4 */ /* U+2030 */        "\xE2\x80\xB0", /* U+0041 U+0302 */ "A\xCC\x82",    /* U+0045 U+0302 */ "E\xCC\x82",    /* U+0041 U+0301 */ "A\xCC\x81",    
    /* 0xE8 */ /* U+0045 U+0308 */ "E\xCC\x88",    /* U+0045 U+0300 */ "E\xCC\x80",    /* U+0049 U+0301 */ "I\xCC\x81",    /* U+0049 U+0302 */ "I\xCC\x82",    
    /* 0xEC */ /* U+0049 U+0308 */ "I\xCC\x88",    /* U+0049 U+0300 */ "I\xCC\x80",    /* U+004F U+0301 */ "O\xCC\x81",    /* U+004F U+0302 */ "O\xCC\x82",    
    /* 0xF0 */ /* U+F8FF */        "\xEF\xA3\xBF", /* U+004F U+0300 */ "O\xCC\x80",    /* U+0055 U+0301 */ "U\xCC\x81",    /* U+0055 U+0302 */ "U\xCC\x82",    
    /* 0xF4 */ /* U+0055 U+0300 */ "U\xCC\x80",    /* U+0131 */        "\xC4\xB1",     /* U+02C6 */        "\xCB\x86",     /* U+02DC */        "\xCB\x9C",     
    /* 0xF8 */ /* U+00AF */        "\xC2\xAF",     /* U+02D8 */        "\xCB\x98",     /* U+02D9 */        "\xCB\x99",     /* U+02DA */        "\xCB\x9A",     
    /* 0xFC */ /* U+00B8 */        "\xC2\xB8",     /* U+02DD */        "\xCB\x9D",     /* U+02DB */        "\xCB\x9B",     /* U+02C7 */        "\xCB\x87"      
};

const int kMacRomanToUTF8Expansion = 3;

struct UniCharInfo {
    uint16_t   utf16Char;
    uint8_t    macRomanChar;
};

typedef struct UniCharInfo UniCharInfo;
static const UniCharInfo kUTF16ToMacRoman[128] = {
    {0x00A0, 0xCA}, {0x00A1, 0xC1}, {0x00A2, 0xA2}, {0x00A3, 0xA3}, {0x00A5, 0xB4}, {0x00A7, 0xA4}, {0x00A8, 0xAC}, {0x00A9, 0xA9},
    {0x00AA, 0xBB}, {0x00AB, 0xC7}, {0x00AC, 0xC2}, {0x00AE, 0xA8}, {0x00AF, 0xF8}, {0x00B0, 0xA1}, {0x00B1, 0xB1}, {0x00B4, 0xAB},
    {0x00B5, 0xB5}, {0x00B6, 0xA6}, {0x00B7, 0xE1}, {0x00B8, 0xFC}, {0x00BA, 0xBC}, {0x00BB, 0xC8}, {0x00BF, 0xC0}, {0x00C0, 0xCB},
    {0x00C1, 0xE7}, {0x00C2, 0xE5}, {0x00C3, 0xCC}, {0x00C4, 0x80}, {0x00C5, 0x81}, {0x00C6, 0xAE}, {0x00C7, 0x82}, {0x00C8, 0xE9},
    {0x00C9, 0x83}, {0x00CA, 0xE6}, {0x00CB, 0xE8}, {0x00CC, 0xED}, {0x00CD, 0xEA}, {0x00CE, 0xEB}, {0x00CF, 0xEC}, {0x00D1, 0x84},
    {0x00D2, 0xF1}, {0x00D3, 0xEE}, {0x00D4, 0xEF}, {0x00D5, 0xCD}, {0x00D6, 0x85}, {0x00D8, 0xAF}, {0x00D9, 0xF4}, {0x00DA, 0xF2},
    {0x00DB, 0xF3}, {0x00DC, 0x86}, {0x00DF, 0xA7}, {0x00E0, 0x88}, {0x00E1, 0x87}, {0x00E2, 0x89}, {0x00E3, 0x8B}, {0x00E4, 0x8A},
    {0x00E5, 0x8C}, {0x00E6, 0xBE}, {0x00E7, 0x8D}, {0x00E8, 0x8F}, {0x00E9, 0x8E}, {0x00EA, 0x90}, {0x00EB, 0x91}, {0x00EC, 0x93},
    {0x00ED, 0x92}, {0x00EE, 0x94}, {0x00EF, 0x95}, {0x00F1, 0x96}, {0x00F2, 0x98}, {0x00F3, 0x97}, {0x00F4, 0x99}, {0x00F5, 0x9B},
    {0x00F6, 0x9A}, {0x00F7, 0xD6}, {0x00F8, 0xBF}, {0x00F9, 0x9D}, {0x00FA, 0x9C}, {0x00FB, 0x9E}, {0x00FC, 0x9F}, {0x00FF, 0xD8},
    {0x0131, 0xF5}, {0x0152, 0xCE}, {0x0153, 0xCF}, {0x0178, 0xD9}, {0x0192, 0xC4}, {0x02C6, 0xF6}, {0x02C7, 0xFF}, {0x02D8, 0xF9},
    {0x02D9, 0xFA}, {0x02DA, 0xFB}, {0x02DB, 0xFE}, {0x02DC, 0xF7}, {0x02DD, 0xFD}, {0x03A9, 0xBD}, {0x03C0, 0xB9}, {0x2013, 0xD0},
    {0x2014, 0xD1}, {0x2018, 0xD4}, {0x2019, 0xD5}, {0x201A, 0xE2}, {0x201C, 0xD2}, {0x201D, 0xD3}, {0x201E, 0xE3}, {0x2020, 0xA0},
    {0x2021, 0xE0}, {0x2022, 0xA5}, {0x2026, 0xC9}, {0x2030, 0xE4}, {0x2039, 0xDC}, {0x203A, 0xDD}, {0x2044, 0xDA}, {0x20AC, 0xDB},
    {0x2122, 0xAA}, {0x2202, 0xB6}, {0x2206, 0xC6}, {0x220F, 0xB8}, {0x2211, 0xB7}, {0x221A, 0xC3}, {0x221E, 0xB0}, {0x222B, 0xBA},
    {0x2248, 0xC5}, {0x2260, 0xAD}, {0x2264, 0xB2}, {0x2265, 0xB3}, {0x25CA, 0xD7}, {0xF8FF, 0xF0}, {0xFB01, 0xDE}, {0xFB02, 0xDF},
};

static const uint16_t kMacRomanToUpper[256] = {
    /* 0x00 */ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
    /* 0x10 */ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
    /* 0x20 */ 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
    /* 0x30 */ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
    /* 0x40 */ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
    /* 0x50 */ 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
    /* 0x60 */ 0x60, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
    /* 0x70 */ 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
    /* 0x80 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x80, 0x8B, 0x81, 0x82, 0x83, 0x8F,
    /* 0x90 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x84, 0x97, 0x98, 0x99, 0x85, 0x9B, 0x9C, 0x9D, 0x9E, 0x86,
    /* 0xA0 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
    /* 0xB0 */ 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xAE, 0xAF,
    /* 0xC0 */ 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0x88, 0x8B, 0x9B, 0xCE, 0xCE,
    /* 0xD0 */ 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD8, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
    /* 0xE0 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0x89, 0x90, 0x87, 0x91, 0x8F, 0x92, 0x94, 0x95, 0x93, 0x97, 0x99,
    /* 0xF0 */ 0xF0, 0x98, 0x9C, 0x9E, 0x9D, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF
};

// End of automatically generated tables.

/////////////////////////////////////////////////////////////////////
#pragma mark ***** String Manipulation

extern size_t MFSNameToUTF8(const uint8_t *name, char *utf8Name, size_t utf8NameSize)
    // Converts an MFS name to its UTF-8 equivalent.
    //
    // name must be a MacRoman encoded Pascal string.
    //
    // utf8Name must be a pointer to a buffer of utf8NameSize bytes.  On 
    // return, this will hold a UTF-8 (decomposed) C string equivalent to 
    // name.
    //
    // utf8NameSize must be at least 1 (to hold the terminating null character 
    // of the C string).
    //
    // Returns the size of the buffer that would be needed to hold a full 
    // conversion of name.  If this is less than utf8NameSize, the string 
    // was truncated.  If it was truncated, it was truncated at a MacRoman 
    // character boundary.
    //
    // This routine is carefully crafted to drop any null characters embeddedd 
    // in name.  null characters are valid in MFS names (yikes!) but not in 
    // C strings.
{
    int             nameLen;
    int             i;
    char *          outputPtr;
    size_t          outputTotalSize;
    const char *    thisStr;
    size_t          thisStrLen;
    
    assert(name != NULL);
    assert(utf8Name != NULL);
    assert(utf8NameSize > 0);       // must at least give us room for null terminator
    
    outputPtr       = utf8Name;
    outputTotalSize = 0;
    
    nameLen = name[0];
    for (i = 1; i <= nameLen; i++) {
        uint8_t ch;
        
        ch = name[i];
        if (ch < 128) {
            thisStr    = (const char *) &ch;
            thisStrLen = (ch != 0);                 // if ch is 0, thisStrLen is 0, so we add no bytes to the string
        } else {
            thisStr    = kMacRomanToUTF8[ch - 128];
            thisStrLen = strlen(thisStr);
        }
        
        outputTotalSize += thisStrLen;
    
        if (outputTotalSize < utf8NameSize) {       // strictly less than guarantees that we always have space for null terminator
            memcpy(outputPtr, thisStr, thisStrLen);
            outputPtr += thisStrLen;
        }
    }
    assert(outputPtr < (utf8Name + utf8NameSize));  
    *outputPtr = 0;
    
    return outputTotalSize + 1;                         // including null terminator
}

extern errno_t UTF8ToMFSName(const char *utf8Name, size_t utf8NameLen, void *tempBuffer, uint8_t *mfsName)
    // Converts a UTF-8 encoding string (either precomposed or decomposed) to 
    // an MFS name (a MacRoman encoded Pascal string).
    //
    // utf8Name must point to a UTF-8 string containing utf8NameLen characters.
    //
    // tempBuffer must point to a temporary working buffer of kUTF8ToMFSNameTempBufferSize 
    // bytes.  On entry, its value is ignored.  On return, its value is undefined.
    //
    // mfsName must point to a buffer of 256 bytes.  On entry, its value is ignored. 
    // On success, this contains the MacRoman Pascal string equivalent to utf8Name.
    //
    // Likely error results include:
    //
    // o EINVAL, which means that utf8Name contains Unicode characters that can't 
    //   be mapped into MacRoman
    //
    // o ENAMETOOLONG, which mean that utf8Name is too longer to be held in a 
    //   MFS name
{
    int         err;
    uint16_t *  utf16Ptr;
    size_t      utf16Count;
    size_t      utf16Index;
    
    assert(utf8Name != NULL);
    assert(tempBuffer != NULL);
    assert(mfsName != NULL);
    
    utf16Ptr = (uint16_t *) tempBuffer;
    
    err = utf8_decodestr( (const uint8_t *) utf8Name, utf8NameLen, utf16Ptr, &utf16Count, kUTF8ToMFSNameTempBufferSize, 0, UTF_PRECOMPOSED);
    if (err == 0) {
        utf16Count /= sizeof(utf16Ptr[0]);      // it comes back as bytes, and we want chars
        
        assert(utf16Count <= 255);              // because it's limited by kUTF8ToMFSNameTempBufferSize
        mfsName[0] = (uint8_t) utf16Count;      // set up length byte
        
        for (utf16Index = 0; utf16Index < utf16Count; utf16Index++) {
            uint16_t            key;
            
            // Binary search kUTF16ToMacRoman for this UTF-16 character.
            // This scary algorithm was stolen from the libc implementation 
            // of <x-man-page://3/bsearch>.
            
            key = utf16Ptr[utf16Index];
            if (key < 128) {
                // Both MacRoman and UTF-8 inherit their bottom 128 characters from 
                // ASCII, so you can just copy it across.
                
                mfsName[utf16Index + 1] = (uint8_t) key;
            } else {
                size_t              limit;
                int                 compareResult;
                const UniCharInfo * base;
                const UniCharInfo * this;
                boolean_t           found;

                // Otherwise, we have to do it the hard way.
                
                found = FALSE;
                base = &kUTF16ToMacRoman[0];
                for (limit = (sizeof(kUTF16ToMacRoman) / sizeof(kUTF16ToMacRoman[0])); limit != 0; limit >>= 1) {
                    this = base + (limit >> 1);
                    
                    compareResult = ((int) key) - ((int) this->utf16Char);
                    if (compareResult == 0) {
                        found = TRUE;
                        break;
                    }
                    if (compareResult > 0) {
                        base = this + 1;
                        limit -= 1;
                    }
                }
                
                if (found) {
                    mfsName[utf16Index + 1] = this->macRomanChar;       // + 1 because the output is a Pascal string
                } else {
                    err = EINVAL;
                    break;
                }
            }
        }
    }
    
    return err;
}

extern void MFSNameToUpper(uint8_t *mfsName)
    // Converts the MFS name (a MacRoman encoding Pascal string) pointed to by 
    // mfsName to upper case.
    //
    // mfsName must point to a valid MFS name.  This routine makes no assumptions 
    // about the size of the buffer containing this name (for example, if you 
    // have a Str63, it's fine to call this routine on it as long as the 
    // length of the string is 63 or less).
{
    int     charCount;
    int     charIndex;
    
    charCount = mfsName[0];
    for (charIndex = 1; charIndex <= charCount; charIndex++) {
        mfsName[charIndex] = kMacRomanToUpper[mfsName[charIndex]];
    }
}

extern boolean_t MFSNameEqualToUpper(const uint8_t *mfsName, const uint8_t *mfsNameUpper)
    // Does a case sensitive comparison of mfsName and mfsNameUpper. 
    //
    // mfsName must point to a valid MFS name.
    //
    // mfsNameUpper must point to a valid MFS name that has already been 
    // uppercased by calling MFSNameToUpper.
    //
    // Returns true if the strings are equal.
{
    boolean_t   result;
    int         charCount;
    int         charIndex;

    charCount = mfsName[0];
    if (charCount == mfsNameUpper[0]) {
        result = TRUE;
        for (charIndex = 1; charIndex <= charCount; charIndex++) {
            if ( kMacRomanToUpper[mfsName[charIndex]] != mfsNameUpper[charIndex] ) {
                result = FALSE;
                break;
            }
        }
    } else {
        result = FALSE;
    }
    
    return result;
}

/////////////////////////////////////////////////////////////////////
#pragma mark ***** Date/Time Manipulation

extern struct timespec MFSDateTimeToTimeSpec(uint32_t mfsDateTime)
    // This routine converts an MFS date/time value to a standard BSD 
    // (struct timespec), accounting for the different epoch.  Note that 
    // this doesn't do UTC conversion, for reasons that are discussed in 
    // the "Dates/Time Values" comment in the header.
{
    // kMFSDateTimeToUNIXDelta is the number of seconds between 
    // the start of the MFS date/time counter (00:00:00 1 Jan 1904) 
    // and the start of the BSD date/time counter (00:00:00 1 Jan 1970).
    enum {
        kMFSDateTimeToUNIXDelta = 3600UL * 24 * ((365 * (1970 - 1904)) + (((1970 - 1904) / 4) + 1))
    };
    struct timespec result;

    // Correct for the difference between the MFS and UNIX epochs.
    
    if (mfsDateTime > kMFSDateTimeToUNIXDelta) {
        // The date/time is 1970 or later, and can be converted to a UNIX date/time 
        // counter correctly.
        result.tv_sec = mfsDateTime - kMFSDateTimeToUNIXDelta;
    } else {
        // The date/time is before 1970, so we set it as low as it can go.
        result.tv_sec = 0;
    }
    result.tv_nsec = 0;

    return result;
}

/////////////////////////////////////////////////////////////////////
#pragma mark ***** MFS On-Disk Structures

// The following structures map to the on-disk format of an MFS 
// MDB (MFSMasterDirectoryBlock) and directory entry (MFSDirectoryRecord). 
// See Inside Macintosh II for the definition of these structures, and a 
// discussion of the overall on-disk volume format.
//
// IMPORTANT
// MFS is big endian, so any time you access a multibyte field, you must 
// swap it to host endian.

enum {
    kMFSSigWord = 0xD2D7
};

struct __attribute__ ((__packed__)) MFSMasterDirectoryBlock {
    uint16_t    sigWord;                            // -> sigWord
    uint32_t    creationDate;
    uint32_t    backupDate;
    uint16_t    attributes;
    uint16_t    fileCount;
    uint16_t    directoryStartBlock;
    uint16_t    directoryBlockCount;
    uint16_t    allocationBlockCount;
    uint32_t    allocationBlockSizeInBytes;
    uint32_t    clumpSizeInBytes;
    uint16_t    allocationBlocksStartBlock;
    uint32_t    nextFileNumber;
    uint16_t    freeAllocationBlockCount;
    uint8_t     nameLength;
    uint8_t     name[27];
};
typedef struct MFSMasterDirectoryBlock MFSMasterDirectoryBlock;

struct __attribute__ ((__packed__)) MFSDirectoryRecord {
    uint8_t     attributes;
    uint8_t     versionNumber;
    uint8_t     finderInfo[16];
    uint32_t    fileNumber;
    uint16_t    dataFirstAllocationBlock;
    uint32_t    dataLengthInBytes;
    uint32_t    dataPhysicalLengthInBytes;
    uint16_t    rsrcFirstAllocationBlock;
    uint32_t    rsrcLengthInBytes;
    uint32_t    rsrcPhysicalLengthInBytes;
    uint32_t    creationDate;
    uint32_t    modificationDate;
    uint8_t     nameLength;
    uint8_t     name[255];
    
    // name field is of variable length and is followed, if the overall length 
    // of the MFSDirectoryRecord is odd, by a pad byte whose value is zero
};
typedef struct MFSDirectoryRecord MFSDirectoryRecord;

enum {
    kMFSDirectoryRecordFixedSize   = offsetof(MFSDirectoryRecord, name),        // everything up to nameLength is fixed
    kMFSDirectoryRecordMinimumSize = kMFSDirectoryRecordFixedSize + 1           // +1 because each name must have at least one char
};

// Constants for the attributes field of MFSDirectoryRecord.

enum {
    kMFSDirectoryRecordAllocatedAttr = 0x80,
    kMFSDirectoryRecordMysteryAttr   = 0x40,            // [1]
    kMFSDirectoryRecordReservedAttr  = 0x3E,            // [1]
    kMFSDirectoryRecordLockedAttr    = 0x01
};

// Notes
// [1] In testing with my entire archive of MFS disks (67 in total), I discovered a few  
// files on a few disks that has the 0x40 bit set in their attributes.  I have no idea 
// what this is; certainly, it's not described in the IM II.  I have a sneaking suspicion 
// that it was some sort of copy protected bit, but I have no real evidence for that.  
// Regardless, I'm happy to ignore it.

/////////////////////////////////////////////////////////////////////
#pragma mark ***** MDB Routines

static void SetErrorString(char *errStr, size_t errStrSize, const char *message)
    // MFSMDBCheckCore calls this routine to copy message into the user 
    // supplied string buffer, if any.
{
    if (errStr != NULL) {
        strncpy(errStr, message, errStrSize);   // no strlcpy in the kernel
        errStr[errStrSize - 1] = 0;             // grrr
    }
}

static int MFSMDBCheckCore(
    const void *        mdbBlockPtr, 
    uint64_t            containerBlockCount,
    size_t *            mdbAndVABMSizeInBytesPtr, 
    uint16_t *          directoryStartBlockPtr, 
    uint16_t *          directoryBlockCountPtr,
    uint16_t *          allocationBlocksStartBlockPtr, 
    uint32_t *          allocationBlockSizeInBytesPtr,
    char *              errStr,
    size_t              errStrSize
)
    // This routine is the guts of both MFSMDBCheck and MFSMDBGetError.  By rolling 
    // both of these functions into a common routine, I can keep the error string 
    // close to the validity check that generates it.
    //
    // All pointer parameters except mdbBlockPtr may be NULL.  errStrSize may be 
    // 0 if errStr is NULL.
{
    int                                 err;
    const MFSMasterDirectoryBlock *     mdbPtr;
    uint16_t                            directoryStartBlock;
    uint16_t                            directoryBlockCount;
    uint16_t                            allocationBlockCount;
    uint16_t                            allocationBlocksStartBlock;
    uint32_t                            allocationBlockSizeInBytes;
    
    // Parameter asserts.
    
    assert(mdbBlockPtr != NULL);
    assert( (errStrSize != 0) || (errStr == NULL) );

    // Quick sanity check of the MDB and directory entry structures.  These should 
    // be compile-time asserts, but there is no handy compile-time assert macro available 
    // to both user and kernel code.
    
    assert(sizeof(MFSMasterDirectoryBlock) == 64);
    assert(sizeof(MFSDirectoryRecord) == 306);          // includes maximal length name
    assert(kMFSDirectoryRecordFixedSize == 51);
    
    mdbPtr = (const MFSMasterDirectoryBlock *) mdbBlockPtr;
    
    // Check the sigWord.
    
    err = 0;
    if ( OSSwapBigToHostInt16(mdbPtr->sigWord) != kMFSSigWord ) {
        SetErrorString(errStr, errStrSize, "incorrect signature");
        err = EINVAL;
    }
    
    // Check that the MDB is compatible with the container block count.
    
    if (err == 0) {
        directoryStartBlock  = OSSwapBigToHostInt16(mdbPtr->directoryStartBlock);
        directoryBlockCount  = OSSwapBigToHostInt16(mdbPtr->directoryBlockCount);
        allocationBlockCount = OSSwapBigToHostInt16(mdbPtr->allocationBlockCount);
        allocationBlocksStartBlock = OSSwapBigToHostInt16(mdbPtr->allocationBlocksStartBlock);
        allocationBlockSizeInBytes = OSSwapBigToHostInt32(mdbPtr->allocationBlockSizeInBytes);
    }
    if ( (err == 0) && ( ((uint64_t) kMFSMDBBlock) >= containerBlockCount) ) {
        SetErrorString(errStr, errStrSize, "MDB falls outside of container");
        err = EINVAL;
    }
    if ( (err == 0) && (directoryBlockCount == 0) ) {
        SetErrorString(errStr, errStrSize, "no directory blocks");
        err = EINVAL;
    }
    if ( (err == 0) && ( ((uint64_t) directoryStartBlock) >= containerBlockCount) ) {
        SetErrorString(errStr, errStrSize, "directory starts outside of container");
        err = EINVAL;
    }
    if ( (err == 0) && ( (((uint64_t) directoryStartBlock) + directoryBlockCount) >= containerBlockCount) ) {
        SetErrorString(errStr, errStrSize, "directory ends outside of container");
        err = EINVAL;
    }
    if ( (err == 0) && ( ((uint64_t) allocationBlockCount) >= containerBlockCount) ) {
        SetErrorString(errStr, errStrSize, "more allocation blocks that container blocks");
        // This assumes that each allocation block is at least one disk block.
        err = EINVAL;
    }
    if ( (err == 0) && ( ((uint64_t) allocationBlocksStartBlock) >= containerBlockCount) ) {
        SetErrorString(errStr, errStrSize, "allocation blocks start outside of container");
        // This assumes that each allocation block is at least one disk block, which 
        // is implied by IM II's statement that the allocation block must be an even 
        // multiple of the block size.
        err = EINVAL;
    }

    // The following assumes that nextFileNumber doesn't wrap, which is a pretty 
    // safe assumption for MFS (although later on, for HFS, we had to allow for 
    // wrapping).
    
    if ( (err == 0) && (OSSwapBigToHostInt32(mdbPtr->nextFileNumber) <= OSSwapBigToHostInt16(mdbPtr->fileCount)) ) {
        SetErrorString(errStr, errStrSize, "next file number is not greater than file count");
        err = EINVAL;
    }
    
    // Can't have more free allocation blocks than we have allocation blocks.
    
    if ( (err == 0) && (OSSwapBigToHostInt16(mdbPtr->freeAllocationBlockCount) > allocationBlockCount) ) {
        SetErrorString(errStr, errStrSize, "more free allocation blocks than allocation blocks");
        err = EINVAL;
    }

    // As we're about to divide by allocationBlockSizeInBytes, let's check for zero first.  
    // And while we're at it, make sure it's a power of two.

    if ( (err == 0) && (allocationBlockSizeInBytes == 0) ) {
        SetErrorString(errStr, errStrSize, "allocation block size is zero");
        err = EINVAL;
    }
    if ( (err == 0) && ( (allocationBlockSizeInBytes & (allocationBlockSizeInBytes - 1)) != 0) ) {
        SetErrorString(errStr, errStrSize, "allocation block size is not a power of two");
        err = EINVAL;
    }
    
    // Clump size must be a multiple of the allocation block size, per IM II-122.
    
    if ( (err == 0) && ((OSSwapBigToHostInt32(mdbPtr->clumpSizeInBytes) % allocationBlockSizeInBytes) != 0) ) {
        SetErrorString(errStr, errStrSize, "clump size is not an even multiple of the allocation block size");
        err = EINVAL;
    }
    
    // We won't deal well with an empty volume name, or with names longer than the 
    // space allocated for them in the MDB.
    
    if ( (err == 0) && (mdbPtr->nameLength == 0) ) {
        SetErrorString(errStr, errStrSize, "volume name is empty");
        err = EINVAL;
    }
    if ( (err == 0) && (mdbPtr->nameLength > 27) ) {
        SetErrorString(errStr, errStrSize, "volume name too long");
        err = EINVAL;
    }

    if (err == 0) {

        // When calculating how many bytes make up the VABM, consider the following:
        //
        // o (allocationBlockCount + 1) / 2 is the number of allocation block /pairs/
        // o each pair needs three bytes
        
        if (mdbAndVABMSizeInBytesPtr != NULL) {
            *mdbAndVABMSizeInBytesPtr      = sizeof(MFSMasterDirectoryBlock) 
                + (OSSwapBigToHostInt16(mdbPtr->allocationBlockCount) + 1) / 2 * 3;
        }
        if (directoryStartBlockPtr != NULL) {
            *directoryStartBlockPtr        = directoryStartBlock;
        }
        if (directoryBlockCountPtr != NULL) {
            *directoryBlockCountPtr        = directoryBlockCount;
        }
        if (allocationBlocksStartBlockPtr != NULL) {
            *allocationBlocksStartBlockPtr = allocationBlocksStartBlock;
        }
        if (allocationBlockSizeInBytesPtr != NULL) {
            *allocationBlockSizeInBytesPtr = allocationBlockSizeInBytes;
        }
    }
    
    return err;
}

extern int MFSMDBCheck(
    const void *        mdbBlockPtr, 
    uint64_t            containerBlockCount,
    size_t *            mdbAndVABMSizeInBytesPtr, 
    uint16_t *          directoryStartBlockPtr, 
    uint16_t *          directoryBlockCountPtr,
    uint16_t *          allocationBlocksStartBlockPtr, 
    uint32_t *          allocationBlockSizeInBytesPtr
)
    // See comments in header.
{
    return MFSMDBCheckCore(
        mdbBlockPtr, 
        containerBlockCount,
        mdbAndVABMSizeInBytesPtr, 
        directoryStartBlockPtr, 
        directoryBlockCountPtr,
        allocationBlocksStartBlockPtr, 
        allocationBlockSizeInBytesPtr,
        NULL,
        0
    );
}

extern void MFSMDBGetError(
    const void *        mdbBlockPtr, 
    uint64_t            containerBlockCount,
    char *              errStr,
    size_t              errStrSize
)
    // See comments in header.
{
    int     junk;
    
    assert(errStr != NULL);
    assert(errStrSize > 0);
    
    errStr[0] = 0;
    
    junk = MFSMDBCheckCore(
        mdbBlockPtr, 
        containerBlockCount,
        NULL,
        NULL,
        NULL,
        NULL, 
        NULL,
        errStr,
        errStrSize
    );
    assert(junk == EINVAL);     // If MFSMDBCheck didn't fail, why are you calling us!?!
}

#if MACH_ASSERT

    static boolean_t MFSMDBValid(const MFSMasterDirectoryBlock *mdbPtr)
        // Does a basic check to see if mdbPtr is valid.
    {
        assert(mdbPtr != NULL);
        assert( OSSwapBigToHostInt16(mdbPtr->sigWord) == kMFSSigWord );
        return TRUE;
    }

#endif

static void MFSInitGetAttrListGoop(struct vfs_attr *attr)
    // Sets up the f_capabilities and f_attributes of attr.  This is sufficiently 
    // long winded that I pulled it out MFSMDBGetAttr to keep that code easy to read. 
{
    assert(attr != NULL);
    
    attr->f_capabilities.capabilities[VOL_CAPABILITIES_FORMAT] = 0
        | VOL_CAP_FMT_PERSISTENTOBJECTIDS
//      | VOL_CAP_FMT_SYMBOLICLINKS
//      | VOL_CAP_FMT_HARDLINKS
//      | VOL_CAP_FMT_JOURNAL
//      | VOL_CAP_FMT_JOURNAL_ACTIVE
//      | VOL_CAP_FMT_NO_ROOT_TIMES
//      | VOL_CAP_FMT_SPARSE_FILES
//      | VOL_CAP_FMT_ZERO_RUNS
//      | VOL_CAP_FMT_CASE_SENSITIVE
        | VOL_CAP_FMT_CASE_PRESERVING
        | VOL_CAP_FMT_FAST_STATFS
//      | VOL_CAP_FMT_2TB_FILESIZE
        ;
    attr->f_capabilities.valid[VOL_CAPABILITIES_FORMAT] = 0
        | VOL_CAP_FMT_PERSISTENTOBJECTIDS
        | VOL_CAP_FMT_SYMBOLICLINKS
        | VOL_CAP_FMT_HARDLINKS
        | VOL_CAP_FMT_JOURNAL
        | VOL_CAP_FMT_JOURNAL_ACTIVE
        | VOL_CAP_FMT_NO_ROOT_TIMES
        | VOL_CAP_FMT_SPARSE_FILES
        | VOL_CAP_FMT_ZERO_RUNS
        | VOL_CAP_FMT_CASE_SENSITIVE
        | VOL_CAP_FMT_CASE_PRESERVING
        | VOL_CAP_FMT_FAST_STATFS
        | VOL_CAP_FMT_2TB_FILESIZE
        ;
    attr->f_capabilities.capabilities[VOL_CAPABILITIES_INTERFACES] = 0
//      | VOL_CAP_INT_SEARCHFS
        | VOL_CAP_INT_ATTRLIST
//      | VOL_CAP_INT_NFSEXPORT
//      | VOL_CAP_INT_READDIRATTR
//      | VOL_CAP_INT_EXCHANGEDATA
//      | VOL_CAP_INT_COPYFILE
//      | VOL_CAP_INT_ALLOCATE
//      | VOL_CAP_INT_VOL_RENAME
//      | VOL_CAP_INT_ADVLOCK
//      | VOL_CAP_INT_FLOCK
//      | VOL_CAP_INT_EXTENDED_SECURITY
//      | VOL_CAP_INT_USERACCESS
        ;
    attr->f_capabilities.valid[VOL_CAPABILITIES_INTERFACES] = 0
        | VOL_CAP_INT_SEARCHFS
        | VOL_CAP_INT_ATTRLIST
        | VOL_CAP_INT_NFSEXPORT
        | VOL_CAP_INT_READDIRATTR
        | VOL_CAP_INT_EXCHANGEDATA
        | VOL_CAP_INT_COPYFILE
        | VOL_CAP_INT_ALLOCATE
        | VOL_CAP_INT_VOL_RENAME
        | VOL_CAP_INT_ADVLOCK
        | VOL_CAP_INT_FLOCK
        | VOL_CAP_INT_EXTENDED_SECURITY
        | VOL_CAP_INT_USERACCESS
        ;
    attr->f_capabilities.capabilities[VOL_CAPABILITIES_RESERVED1] = 0;
    attr->f_capabilities.valid[VOL_CAPABILITIES_RESERVED1] = 0;
    attr->f_capabilities.capabilities[VOL_CAPABILITIES_RESERVED2] = 0;
    attr->f_capabilities.valid[VOL_CAPABILITIES_RESERVED2] = 0;
    VFSATTR_SET_SUPPORTED(attr, f_capabilities);
    
    attr->f_attributes.validattr.commonattr = 0
        | ATTR_CMN_NAME
        | ATTR_CMN_DEVID
        | ATTR_CMN_FSID
        | ATTR_CMN_OBJTYPE
//      | ATTR_CMN_OBJTAG
        | ATTR_CMN_OBJID
        | ATTR_CMN_OBJPERMANENTID
        | ATTR_CMN_PAROBJID
//      | ATTR_CMN_SCRIPT
        | ATTR_CMN_CRTIME
        | ATTR_CMN_MODTIME
//      | ATTR_CMN_CHGTIME
//      | ATTR_CMN_ACCTIME
        | ATTR_CMN_BKUPTIME
        | ATTR_CMN_FNDRINFO
        | ATTR_CMN_OWNERID
        | ATTR_CMN_GRPID
        | ATTR_CMN_ACCESSMASK
        | ATTR_CMN_FLAGS
//      | ATTR_CMN_USERACCESS
//      | ATTR_CMN_EXTENDED_SECURITY
//      | ATTR_CMN_UUID
//      | ATTR_CMN_GRPUUID
        ;
    attr->f_attributes.validattr.volattr = 0
        | ATTR_VOL_FSTYPE
        | ATTR_VOL_SIGNATURE
        | ATTR_VOL_SIZE
        | ATTR_VOL_SPACEFREE
        | ATTR_VOL_SPACEAVAIL
        | ATTR_VOL_MINALLOCATION
        | ATTR_VOL_ALLOCATIONCLUMP
        | ATTR_VOL_IOBLOCKSIZE
        | ATTR_VOL_OBJCOUNT
        | ATTR_VOL_FILECOUNT
        | ATTR_VOL_DIRCOUNT
        | ATTR_VOL_MAXOBJCOUNT
        | ATTR_VOL_MOUNTPOINT
        | ATTR_VOL_NAME
        | ATTR_VOL_MOUNTFLAGS
        | ATTR_VOL_MOUNTEDDEVICE
        | ATTR_VOL_ENCODINGSUSED
        | ATTR_VOL_CAPABILITIES
        | ATTR_VOL_ATTRIBUTES
        ;
    attr->f_attributes.validattr.dirattr = 0
        | ATTR_DIR_LINKCOUNT
        | ATTR_DIR_ENTRYCOUNT
//      | ATTR_DIR_MOUNTSTATUS
        ;
    attr->f_attributes.validattr.fileattr = 0
        | ATTR_FILE_LINKCOUNT
        | ATTR_FILE_TOTALSIZE
        | ATTR_FILE_ALLOCSIZE
        | ATTR_FILE_IOBLOCKSIZE
        | ATTR_FILE_DEVTYPE
//      | ATTR_FILE_FORKCOUNT
//      | ATTR_FILE_FORKLIST
        | ATTR_FILE_DATALENGTH
        | ATTR_FILE_DATAALLOCSIZE
        | ATTR_FILE_RSRCLENGTH
        | ATTR_FILE_RSRCALLOCSIZE
        ;
    attr->f_attributes.validattr.forkattr = 0;
    
    // All attributes that we do support, we support natively.
    
    attr->f_attributes.nativeattr.commonattr = attr->f_attributes.validattr.commonattr;
    attr->f_attributes.nativeattr.volattr    = attr->f_attributes.validattr.volattr;
    attr->f_attributes.nativeattr.dirattr    = attr->f_attributes.validattr.dirattr;
    attr->f_attributes.nativeattr.fileattr   = attr->f_attributes.validattr.fileattr;
    attr->f_attributes.nativeattr.forkattr   = attr->f_attributes.validattr.forkattr;

    VFSATTR_SET_SUPPORTED(attr, f_attributes);
}

// When I need to return a time which MFS doesn't have available, I return the 
// beginning of time.  It's hand to have this around as a structure.

static const struct timespec kZeroTime = {0, 0};

extern int MFSMDBGetAttr(
    const void *        mdbBlockPtr,
    struct vfs_attr *   attr
)
    // See comments in header.
{
    size_t                              junkSize;
    const MFSMasterDirectoryBlock *     mdbPtr;

    mdbPtr = (const MFSMasterDirectoryBlock *) mdbBlockPtr;
    assert( MFSMDBValid(mdbPtr) );

    VFSATTR_RETURN(attr, f_objcount,    OSSwapBigToHostInt16(mdbPtr->fileCount) + 1);   // +1 for root directory
    VFSATTR_RETURN(attr, f_filecount,   OSSwapBigToHostInt16(mdbPtr->fileCount));
    VFSATTR_RETURN(attr, f_dircount,    1);
    VFSATTR_RETURN(attr, f_maxobjcount, ((512 / kMFSDirectoryRecordMinimumSize) * OSSwapBigToHostInt16(mdbPtr->directoryBlockCount)) + 1);
                                        // +1 for root directory
    
    VFSATTR_RETURN(attr, f_bsize,       OSSwapBigToHostInt32(mdbPtr->allocationBlockSizeInBytes));
    VFSATTR_RETURN(attr, f_iosize,      OSSwapBigToHostInt32(mdbPtr->allocationBlockSizeInBytes));
    VFSATTR_RETURN(attr, f_blocks,      OSSwapBigToHostInt16(mdbPtr->allocationBlockCount));
    VFSATTR_RETURN(attr, f_bfree,       OSSwapBigToHostInt16(mdbPtr->freeAllocationBlockCount));
    VFSATTR_RETURN(attr, f_bavail,      OSSwapBigToHostInt16(mdbPtr->freeAllocationBlockCount));
    VFSATTR_RETURN(attr, f_bused,       attr->f_blocks - attr->f_bfree);
    VFSATTR_RETURN(attr, f_files,       OSSwapBigToHostInt16(mdbPtr->fileCount));
    VFSATTR_RETURN(attr, f_ffree,       attr->f_maxobjcount - attr->f_files);
//  VFSATTR_RETURN(attr, f_fsid,        x);         // leave this up to higher layer software
//  VFSATTR_RETURN(attr, f_owner,       x);         // ditto
    
    MFSInitGetAttrListGoop(attr);

    // See MFSDirectoryEntryGetAttr for an important note about time attributes.
    
    VFSATTR_RETURN(attr, f_create_time, MFSDateTimeToTimeSpec(OSSwapBigToHostInt32(mdbPtr->creationDate)));
    VFSATTR_RETURN(attr, f_modify_time, attr->f_create_time);
    VFSATTR_RETURN(attr, f_access_time, kZeroTime);
    VFSATTR_RETURN(attr, f_backup_time, MFSDateTimeToTimeSpec(OSSwapBigToHostInt32(mdbPtr->backupDate)));

    VFSATTR_RETURN(attr, f_fssubtype, 0);

    if ( VFSATTR_IS_ACTIVE(attr, f_vol_name) ) {
        // Maximum volume name length is 27 characters; with a maximum UTF-8 expansion 
        // of 3x, this yields 81, for a buffer size of 82, which is way less than MAXPATHLEN.
        
        assert( ((27 * kMacRomanToUTF8Expansion) + 1) <= MAXPATHLEN );
        
        junkSize = MFSNameToUTF8(&mdbPtr->nameLength, attr->f_vol_name, MAXPATHLEN);
        assert(junkSize < MAXPATHLEN);
        
        VFSATTR_SET_SUPPORTED(attr, f_vol_name);
    }

    VFSATTR_RETURN(attr, f_signature,   OSSwapBigToHostInt16(mdbPtr->sigWord));
    VFSATTR_RETURN(attr, f_carbon_fsid, 0);         // MFS shares this with HFS
    
    return 0;
}

// Special allocation block numbers:

enum {
    // An allocation block number of zero is used, in a directory entry, to indicate that
    // a fork has no allocation blocks.
    
    kMFSEmptyAllocationBlock     = 0,
    
    // An allocation block number of one is used, in the VABM, to indicate the end of the 
    // allocation block chain.
    
    kMFSLastAllocationBlock      = 1,

    // To avoid using the special values (defined in the header), the VABM is indexed by
    // allocation block - kMFSFirstAllocationBlock.

    kMFSFirstAllocationBlock = 2,

    // Because 0x0FFF is used by kMFSDirectoryAllocationBlock, the maximum valid allocation 
    // block number is 0x0FFE.
    
    kMFSMaximumAllocationBlock   = 0x0FFE,

    // It is legal for the directory to be within the space managed by the VABM (althoug 
    // I've never seen an MFS disk constructed this way though).  Still, in that case, 
    // the directory blocks must be marked with a special value to avoid them being 
    // used for anything else.  
    
    kMFSDirectoryAllocationBlock = 0x0FFF
};

static uint16_t MFSVABMNextAllocationBlock(const void *mdbAndVABMPtr, uint16_t allocationBlock)
    // Looks up allocation block in the VABM and returns the next allocation block. 
    // The result can might be kMFSLastAllocationBlock.  See the pre- and post-condition 
    // asserts for the specifics.
{
    uint16_t                            result;
    const MFSMasterDirectoryBlock *     mdbPtr;
    const uint8_t *                     vabmBase;
    size_t                              byteIndex;

    mdbPtr = (const MFSMasterDirectoryBlock *) mdbAndVABMPtr;
    assert( MFSMDBValid(mdbPtr) );
    assert(allocationBlock != kMFSEmptyAllocationBlock);
    assert(allocationBlock != kMFSLastAllocationBlock);
    assert(allocationBlock <= kMFSMaximumAllocationBlock);
    assert(allocationBlock < (kMFSFirstAllocationBlock + OSSwapBigToHostInt16(mdbPtr->allocationBlockCount)));
    
    // VABM starts immediately after the MDB.
    
    vabmBase = (uint8_t *) (&mdbPtr[1]);
    
    // Remember that the VABM is a packed array of 12 bit entries,
    // which is a real pain to access from any language!
    // We first calculate the index of the first byte of the
    // VABM entry...
    
    byteIndex = (allocationBlock - kMFSFirstAllocationBlock) * 3 / 2;
    
    // ...then we do the bit shuffling depending on whether
    // allocationBlock is even or odd.
    
    if ( ((allocationBlock - kMFSFirstAllocationBlock) % 2) == 0 ) {
        result = (vabmBase[byteIndex] << 4) | ((vabmBase[byteIndex + 1] >> 4) & 0x0F);
    } else {
        result = ((vabmBase[byteIndex] & 0x0F) << 8) | vabmBase[byteIndex + 1];
    }
    
    assert(result != kMFSEmptyAllocationBlock);
    assert(result < kMFSMaximumAllocationBlock);
    assert(result < (kMFSFirstAllocationBlock + OSSwapBigToHostInt16(mdbPtr->allocationBlockCount)));
    
    return result;
}


/////////////////////////////////////////////////////////////////////
#pragma mark ***** Directory Entry Routines

extern int MFSDirectoryBlockIterate(
    const void *        directoryBlockPtr, 
    size_t              directoryBlockSizeInBytes, 
    size_t *            dirOffsetPtr, 
    struct vnode_attr * attr
)
    // See comments in header.
{
    int     err;
    size_t  dirOffset;
    size_t  dirOffsetInitial;
    
    assert(directoryBlockPtr != NULL);
    assert(directoryBlockSizeInBytes > kMFSDirectoryRecordFixedSize);
    assert(dirOffsetPtr != NULL);
    assert( (*dirOffsetPtr == kMFSDirectoryBlockIterateFromStart) || (*dirOffsetPtr < directoryBlockSizeInBytes) );
    // attr can be NULL
    
    dirOffset = *dirOffsetPtr;
    dirOffsetInitial = dirOffset;
    
    // Calculate the dirOffset of the next directory record based on the offset passed in.
    
    if (dirOffset == kMFSDirectoryBlockIterateFromStart) {
        dirOffset = 0;
    } else {
        const MFSDirectoryRecord *  lastDirRec;
        
        lastDirRec = ((const MFSDirectoryRecord *) (((const char *) directoryBlockPtr) + dirOffset));

        // lastDirRec falls within the directory block
        
        assert( ((const char *) lastDirRec) >= ((const char *) directoryBlockPtr) );
        assert( ((const char *) lastDirRec) < (((const char *) directoryBlockPtr) + directoryBlockSizeInBytes) );
        
        // lastDirRec->nameLength falls within the directory block
        
        assert( ((const char *) &lastDirRec->nameLength) < (((const char *) directoryBlockPtr) + directoryBlockSizeInBytes) );

        // The attributes field should be either 0x80 (allocated and not locked) or 
        // 0x81 (allocated and locked).
        
        assert(  lastDirRec->attributes & kMFSDirectoryRecordAllocatedAttr     );
        assert( (lastDirRec->attributes & kMFSDirectoryRecordReservedAttr) == 0);

        // Bump dirOffset by the size of the previous entry, including the pad byte if necessary.

        dirOffset += kMFSDirectoryRecordFixedSize + lastDirRec->nameLength;
        if ((dirOffset % 2) == 1) {
            dirOffset += 1;
        }
    }

    // Check that the result dirOffset is a valid directory record.  See the comment in 
    // MFSDirectoryBlockCheckDirOffset for a discussion of why I consider 
    // dirOffset == directoryBlockSizeInBytes to be legal.
    
    err = 0;
    if (dirOffset >= directoryBlockSizeInBytes) {
        assert(dirOffset == directoryBlockSizeInBytes);
        err = ENOENT;
    }
    
    if (err == 0) {
        const MFSDirectoryRecord *  thisDirRec;

        thisDirRec = ((const MFSDirectoryRecord *) (((const char *) directoryBlockPtr) + dirOffset));

        // thisDirRec falls within the directory block

        assert( ((const char *) thisDirRec) >= ((const char *) directoryBlockPtr) );
        assert( ((const char *) thisDirRec) < (((const char *) directoryBlockPtr) + directoryBlockSizeInBytes) );
        
        if (thisDirRec->attributes == 0) {
            err = ENOENT;
        } else {
            // thisDirRec->nameLength falls within the directory block
            
            assert( ((const char *) &thisDirRec->nameLength) < (((const char *) directoryBlockPtr) + directoryBlockSizeInBytes) );

            // entire name falls within the directory block
            
            assert( (const char *) (thisDirRec->name + thisDirRec->nameLength) <= (((const char *) directoryBlockPtr) + directoryBlockSizeInBytes) );

            // The attributes field should be either 0x80 (allocated and not locked) or 
            // 0x81 (allocated and locked).

            assert(  thisDirRec->attributes & kMFSDirectoryRecordAllocatedAttr     );
            assert( (thisDirRec->attributes & kMFSDirectoryRecordReservedAttr) == 0);
            
            if (attr != NULL) {
                err = MFSDirectoryEntryGetAttr(directoryBlockPtr, dirOffset, attr);
            }
        }
    }
    
    if (err == 0) {
        *dirOffsetPtr = dirOffset;
    }

    assert( (*dirOffsetPtr == kMFSDirectoryBlockIterateFromStart) || (*dirOffsetPtr < directoryBlockSizeInBytes) );
    assert( (err != 0) == (*dirOffsetPtr == dirOffsetInitial) );        // if we errored, *dirOffsetPtr must be unchanged
    
    return err;
}

extern int MFSDirectoryBlockCheckDirOffset(
    const void *        directoryBlockPtr, 
    size_t              directoryBlockSizeInBytes, 
    size_t              candidateDirOffset
)
    // See comments in header.
{
    int                         err;
    size_t                      dirOffset;
    const MFSDirectoryRecord *  thisDirRec;
    
    assert(directoryBlockPtr != NULL);
    assert(directoryBlockSizeInBytes > kMFSDirectoryRecordFixedSize);
    assert(candidateDirOffset != kMFSDirectoryBlockIterateFromStart);       // just don't call us in this case
    assert(candidateDirOffset < directoryBlockSizeInBytes);                 // ditto
    
    // Start at the beginning of the block.

    dirOffset = 0;
    do {
        // If this entry isn't valid, we're done.
        
        thisDirRec = ((const MFSDirectoryRecord *) (((const char *) directoryBlockPtr) + dirOffset));
        if (thisDirRec->attributes == 0) {
            err = EINVAL;                       // hit end of valid directory entries
            break;
        }
        
        // Check that this directory entry is reasonable.
        
        assert(  thisDirRec->attributes & kMFSDirectoryRecordAllocatedAttr     );
        assert( (thisDirRec->attributes & kMFSDirectoryRecordReservedAttr) == 0);
        assert( ((const char *) &thisDirRec->nameLength) < (((const char *) directoryBlockPtr) + directoryBlockSizeInBytes) );
        assert( (const char *) (thisDirRec->name + thisDirRec->nameLength) <= (((const char *) directoryBlockPtr) + directoryBlockSizeInBytes) );

        // We have a valid entry; if it's offset is the one we're looking for, we're done.
        
        if (dirOffset == candidateDirOffset) {
            err = 0;
            break;
        }
        
        // Advance to the next directory entry.
        
        dirOffset += kMFSDirectoryRecordFixedSize + thisDirRec->nameLength;
        if ((dirOffset % 2) == 1) {
            dirOffset += 1;
        }
        
        // If we run off the end of the block, we're done.  I don't know if an MFS 
        // directory block has to end with an empty entry (that is, a 0), or whether 
        // it's possible for the last entry to butt right up against the end of the 
        // block.  IM II is silent on the issue.  I'm going to take a stance and 
        // say that it's legal, but obviously only if you hit the /exact/ end of the block.
        
        if (dirOffset >= directoryBlockSizeInBytes ) {
            assert(dirOffset == directoryBlockSizeInBytes);
            err = EINVAL;
            break;
        }
    } while (TRUE);
    
    return err;
}

extern int MFSDirectoryBlockFindEntryByName(
    const void *        directoryBlockPtr, 
    size_t              directoryBlockSizeInBytes, 
    const char *        utf8Name,
    size_t              utf8NameLen,
    void *              tempBuffer,
    size_t *            dirOffsetPtr, 
    struct vnode_attr * attr
)
    // See comments in header.
{
    int         err;
    size_t      dirOffset;
    uint8_t *   mfsNameUpper;

    assert(directoryBlockPtr != NULL);
    assert(directoryBlockSizeInBytes > kMFSDirectoryRecordFixedSize);
    assert(utf8Name != NULL);
    assert(tempBuffer != NULL);
    assert(dirOffsetPtr != NULL);
    // *dirOffsetPtr ignored on input
    // attr can be NULL

    // If this is the first time we've seen tempBuffer, its first byte will be 0.
    // In that case, use it to cache a copy of the uppercased MFS name associated 
    // with utf8Name.  This avoids the cost of the UTF-8 -> MacRoman -> upper case 
    // if you're scanning multiple directory blocks.

    mfsNameUpper = (uint8_t *) tempBuffer;
    if (mfsNameUpper[0] != 0 ) {
        err = 0;
    } else {
        // Use tempBuffer + 256 as the tempory buffer for UTF8ToMFSName, and 
        // tempBuffer + 0 as the place to store the resulting MFS name.
    
        err = UTF8ToMFSName(utf8Name, utf8NameLen, ((char *) tempBuffer) + 256, mfsNameUpper);
        if (err == 0) {
            MFSNameToUpper(mfsNameUpper);
            
            // Empty names are unacceptable, in general /and/ because we use the first 
            // byte to determine if tempBuffer has been initialised yet.
            
            if (mfsNameUpper[0] == 0) {
                err = EINVAL;
            }
        }
    }
    
    // Iterate the directory block looking for the name.
    
    if (err == 0) {
        boolean_t       found;
        
        dirOffset = kMFSDirectoryBlockIterateFromStart;
        found = FALSE;
        do {
            err = MFSDirectoryBlockIterate(
                directoryBlockPtr,
                directoryBlockSizeInBytes,
                &dirOffset,
                NULL
            );
            
            if (err == 0) {
                const MFSDirectoryRecord *  dirRec;
                
                dirRec = ((const MFSDirectoryRecord *) (((const char *) directoryBlockPtr) + dirOffset));
                
                found = MFSNameEqualToUpper(&dirRec->nameLength, mfsNameUpper);
            }
        } while ( (err == 0) && ! found);

        assert(found == (err == 0));
    }

    // If we would it, return its offset (and attributes, if requested) to the caller.
    
    if (err == 0) {
        *dirOffsetPtr = dirOffset;
        if (attr != NULL) {
            err = MFSDirectoryEntryGetAttr(directoryBlockPtr, dirOffset, attr);
        }
    }

    assert( (err != 0) || (*dirOffsetPtr <= directoryBlockSizeInBytes) );
    
    return err;
}

extern int MFSDirectoryEntryGetAttr(
    const void *        directoryBlockPtr, 
    size_t              dirOffset, 
    struct vnode_attr * attr
)
    // See comments in header.
{
    const MFSDirectoryRecord *  dirRec;
    size_t                      junkSize;
    
    assert(directoryBlockPtr != NULL);
    assert(attr != NULL);
    
    dirRec = ((const MFSDirectoryRecord *) (((const char *) directoryBlockPtr) + dirOffset));

    VATTR_RETURN(attr, va_rdev,        0);
    VATTR_RETURN(attr, va_nlink,       1);
    VATTR_RETURN(attr, va_total_size,  OSSwapBigToHostInt32(dirRec->dataLengthInBytes) + OSSwapBigToHostInt32(dirRec->rsrcLengthInBytes));
    VATTR_RETURN(attr, va_total_alloc, OSSwapBigToHostInt32(dirRec->dataPhysicalLengthInBytes) + OSSwapBigToHostInt32(dirRec->rsrcPhysicalLengthInBytes));
    VATTR_RETURN(attr, va_data_size,   OSSwapBigToHostInt32(dirRec->dataLengthInBytes));
    VATTR_RETURN(attr, va_data_alloc,  OSSwapBigToHostInt32(dirRec->dataPhysicalLengthInBytes));
//  VATTR_RETURN(attr, va_iosize,      xxx);

//  VATTR_RETURN(attr, va_uid,   xxx);
//  VATTR_RETURN(attr, va_gid,   xxx);
    VATTR_RETURN(attr, va_mode,  S_IFREG | S_IRUSR | S_IRGRP | S_IROTH);
    VATTR_RETURN(attr, va_flags, (dirRec->attributes & kMFSDirectoryRecordLockedAttr) ? (SF_IMMUTABLE | UF_IMMUTABLE) : 0);

    // We have to support /all/ of the times, otherwise File Manager 
    // won't play with us.  Specifically, File Manager calls getattrlist to  
    // request these attributes, and the getattrlist implementation tests 
    // our VATTR_RETURN values to see if we support them and, if we don't, 
    // return an error to File Manager.  This causes File Manager to get 
    // very confused, and results in Very Bad Things (tm).
    //
    // So we claim to support everything and then just return a nice constant 
    // value for those which we don't have meaningful data.
    
    VATTR_RETURN(attr, va_create_time, MFSDateTimeToTimeSpec(OSSwapBigToHostInt32(dirRec->creationDate)));
    VATTR_RETURN(attr, va_access_time, kZeroTime);
    VATTR_RETURN(attr, va_modify_time, MFSDateTimeToTimeSpec(OSSwapBigToHostInt32(dirRec->modificationDate)));
    attr->va_change_time = kZeroTime;   // don't have to claim support for va_change_time because VFS sets it to va_modify_time
    VATTR_RETURN(attr, va_backup_time, kZeroTime);
    
    VATTR_RETURN(attr, va_fileid,   OSSwapBigToHostInt32(dirRec->fileNumber) - 1 + kMFSFirstFileInodeName);
//  VATTR_RETURN(attr, va_linkid,   xxx);
    VATTR_RETURN(attr, va_parentid, kMFSRootInodeNumber);
//  VATTR_RETURN(attr, va_fsid,     xxx);
//  VATTR_RETURN(attr, va_filerev,  xxx);
//  VATTR_RETURN(attr, va_gen,      xxx);
    VATTR_RETURN(attr, va_encoding, 0);         // 0 -> MacRoman

    VATTR_RETURN(attr, va_type, VREG);
    if ( VATTR_IS_ACTIVE(attr, va_name) ) {
        // Maximum volume name length is 255 characters; with a maximum UTF-8 expansion 
        // of 3x, this yields 765, for a buffer size of 766, which is definitely less than 
        // MAXPATHLEN.
        
        assert( ((255 * kMacRomanToUTF8Expansion) + 1) <= MAXPATHLEN );
        
        junkSize = MFSNameToUTF8(&dirRec->nameLength, attr->va_name, MAXPATHLEN);
        assert(junkSize < MAXPATHLEN);
        
        VATTR_SET_SUPPORTED(attr, va_name);
    }
//  VATTR_RETURN(attr, va_uuuid, xxx);
//  VATTR_RETURN(attr, va_guuid, xxx);

    VATTR_RETURN(attr, va_nchildren, 0);

    return 0;
}

extern int MFSDirectoryEntryGetFinderInfo(
    const void *        directoryBlockPtr, 
    size_t              dirOffset, 
    void *              finderInfoPtr
)
    // See comments in header.
{
    const MFSDirectoryRecord *  dirRec;
    
    assert(directoryBlockPtr != NULL);
    // nothing we can say about dirOffset, other than to use it to set up dirRec and then check dirRec itself
    dirRec = ((const MFSDirectoryRecord *) (((const char *) directoryBlockPtr) + dirOffset));
    assert(  dirRec->attributes & kMFSDirectoryRecordAllocatedAttr     );
    assert( (dirRec->attributes & kMFSDirectoryRecordReservedAttr) == 0);
    assert(finderInfoPtr != NULL);
    
    memcpy(finderInfoPtr, dirRec->finderInfo, sizeof(dirRec->finderInfo));
    
    return 0;
}

extern int MFSDirectoryEntryGetForkInfo(
    const void *        directoryBlockPtr, 
    size_t              dirOffset,
    size_t              forkIndex,
    MFSForkInfo *       forkInfo
)
    // See comments in header.
{
    const MFSDirectoryRecord *          dirRec;

    // Pre-conditions
    
    assert(directoryBlockPtr != NULL);
    // nothing we can say about dirOffset, other than to use it to set up dirRec and then check dirRec itself
    dirRec = ((const MFSDirectoryRecord *) (((const char *) directoryBlockPtr) + dirOffset));
    assert(  dirRec->attributes & kMFSDirectoryRecordAllocatedAttr     );
    assert( (dirRec->attributes & kMFSDirectoryRecordReservedAttr) == 0);
    assert( (forkIndex == 0) || (forkIndex == 1) );
    assert(forkInfo != NULL);
    
    // Implementation
    
    if (forkIndex == 0) {
        forkInfo->firstAllocationBlock  = OSSwapBigToHostInt16(dirRec->dataFirstAllocationBlock);
        forkInfo->lengthInBytes         = OSSwapBigToHostInt32(dirRec->dataLengthInBytes);
        forkInfo->physicalLengthInBytes = OSSwapBigToHostInt32(dirRec->dataPhysicalLengthInBytes);
    } else {
        forkInfo->firstAllocationBlock  = OSSwapBigToHostInt16(dirRec->rsrcFirstAllocationBlock);
        forkInfo->lengthInBytes         = OSSwapBigToHostInt32(dirRec->rsrcLengthInBytes);
        forkInfo->physicalLengthInBytes = OSSwapBigToHostInt32(dirRec->rsrcPhysicalLengthInBytes);
    }
    
    assert( (forkInfo->firstAllocationBlock != 0) == (forkInfo->lengthInBytes != 0) );
    assert( (forkInfo->firstAllocationBlock != 0) == (forkInfo->physicalLengthInBytes != 0) );
    assert( forkInfo->lengthInBytes <= forkInfo->physicalLengthInBytes );
    
    return 0;
}

/////////////////////////////////////////////////////////////////////
#pragma mark ***** File Fork Routines

extern int MFSForkGetExtent(
    const void *        mdbAndVABMPtr,
    const MFSForkInfo * forkInfo,
    uint32_t            forkOffsetInBytes,
    uint32_t *          offsetFromFirstAllocationBlockInBytesPtr,
    uint32_t *          contiguousPhysicalBytesPtr
)
    // See comments in header.
{
    int                             err;
    const MFSMasterDirectoryBlock * mdbPtr;
    uint32_t                        allocationBlockSizeInBytes;
    uint16_t                        currentAllocationBlock;
    uint32_t                        currentForkOffsetInBytes;
    uint32_t                        contiguousPhysicalBytes;

    // Pre-conditions

    assert(mdbAndVABMPtr != NULL);
    mdbPtr = (const MFSMasterDirectoryBlock *) mdbAndVABMPtr;
    assert( MFSMDBValid(mdbPtr) );
    assert(forkInfo != NULL);
    assert(forkInfo->firstAllocationBlock != kMFSLastAllocationBlock);
    assert(forkInfo->firstAllocationBlock <= kMFSMaximumAllocationBlock);
    assert(forkInfo->firstAllocationBlock < (kMFSFirstAllocationBlock + OSSwapBigToHostInt16(mdbPtr->allocationBlockCount)));
    // assert(forkOffsetInBytes <= forkInfo->lengthInBytes);        // I'm going to allow you to request up to the physical fork length
    assert(forkOffsetInBytes <= forkInfo->physicalLengthInBytes);
    assert(forkInfo->lengthInBytes <= forkInfo->physicalLengthInBytes);
    allocationBlockSizeInBytes = OSSwapBigToHostInt32(mdbPtr->allocationBlockSizeInBytes);
    assert( (forkOffsetInBytes % allocationBlockSizeInBytes) == 0 );
    assert(offsetFromFirstAllocationBlockInBytesPtr != NULL);
    assert(contiguousPhysicalBytesPtr != NULL);

    // Implementation
    
    if (forkInfo->firstAllocationBlock == 0) {
        assert(forkInfo->lengthInBytes == 0);
        assert(forkInfo->physicalLengthInBytes == 0);
        err = EINVAL;                       // asking for the on-disk extent of an empty fork is bogus
    } else {
        // Search for the allocation block that contains forkOffsetInBytes.

        currentAllocationBlock   = forkInfo->firstAllocationBlock;
        currentForkOffsetInBytes = 0;
        do {
            assert(currentAllocationBlock != kMFSEmptyAllocationBlock);
            assert(currentAllocationBlock != kMFSLastAllocationBlock);
            assert(currentAllocationBlock <= kMFSMaximumAllocationBlock);
            assert(currentAllocationBlock < (kMFSFirstAllocationBlock + OSSwapBigToHostInt16(mdbPtr->allocationBlockCount)));

            if (forkOffsetInBytes < (currentForkOffsetInBytes + allocationBlockSizeInBytes)) {
                err = 0;
            } else {
                currentAllocationBlock = MFSVABMNextAllocationBlock(mdbPtr, currentAllocationBlock);
                
                if (currentAllocationBlock == kMFSLastAllocationBlock) {
                    err = EPIPE;            // that is, end of file
                } else {
                    currentForkOffsetInBytes += allocationBlockSizeInBytes;
                    err = EAGAIN;
                }
            }
        } while (err == EAGAIN);
        
        // If that succeeded, work out how many contiguous bytes there are.

        if (err == 0) {
            uint16_t        currentContiguousAllocationBlock;
            uint16_t        nextContiguousAllocationBlock;
            
            currentContiguousAllocationBlock = currentAllocationBlock;
            contiguousPhysicalBytes          = allocationBlockSizeInBytes;
            do {
                assert(currentContiguousAllocationBlock != kMFSEmptyAllocationBlock);
                assert(currentContiguousAllocationBlock != kMFSLastAllocationBlock);
                assert(currentContiguousAllocationBlock <= kMFSMaximumAllocationBlock);
                assert(currentContiguousAllocationBlock < (kMFSFirstAllocationBlock + OSSwapBigToHostInt16(mdbPtr->allocationBlockCount)));

                nextContiguousAllocationBlock = MFSVABMNextAllocationBlock(mdbPtr, currentContiguousAllocationBlock);
                
                if (nextContiguousAllocationBlock == kMFSLastAllocationBlock) {
                    err = 0;
                } else if (nextContiguousAllocationBlock != (currentContiguousAllocationBlock + 1)) {
                    err = 0;
                } else {
                    currentContiguousAllocationBlock = nextContiguousAllocationBlock;
                    contiguousPhysicalBytes += allocationBlockSizeInBytes;
                    err = EAGAIN;
                } 
            } while (err == EAGAIN);
        }
    }

    if (err == 0) {
        assert(currentAllocationBlock != kMFSEmptyAllocationBlock);
        assert(currentAllocationBlock != kMFSLastAllocationBlock);
        assert(currentAllocationBlock <= kMFSMaximumAllocationBlock);
        assert(currentAllocationBlock < (kMFSFirstAllocationBlock + OSSwapBigToHostInt16(mdbPtr->allocationBlockCount)));

        *offsetFromFirstAllocationBlockInBytesPtr = (currentAllocationBlock - kMFSFirstAllocationBlock) * allocationBlockSizeInBytes;
        *contiguousPhysicalBytesPtr = contiguousPhysicalBytes;
    }

    // Post-conditions
    
    if (err == 0) {
        assert((*offsetFromFirstAllocationBlockInBytesPtr % allocationBlockSizeInBytes) == 0);
        assert((*contiguousPhysicalBytesPtr % allocationBlockSizeInBytes) == 0);
    }
    
    return err;
}
