0

I have two serialize operator overloads:

friend CArchive& operator<<(CArchive& rArchive, S_MEMORIAL_INFO const& rsMI)
{
    return rArchive << rsMI.strHost
        << rsMI.strCohost
        << rsMI.strZoomAttendant
        << rsMI.strChairman
        << rsMI.strPrayerOpen
        << rsMI.strPrayerClose
        << rsMI.strSpeaker
        << rsMI.strImagePath
        << rsMI.strTextBeforeImage
        << rsMI.strTextAfterImage
        << rsMI.iImageWidthAsPercent
        << rsMI.iSongOpen
        << rsMI.iSongClose;

}

friend CArchive& operator>>(CArchive& rArchive, S_MEMORIAL_INFO& rsMI)
{
    return rArchive >> rsMI.strHost
        >> rsMI.strCohost
        >> rsMI.strZoomAttendant
        >> rsMI.strChairman
        >> rsMI.strPrayerOpen
        >> rsMI.strPrayerClose
        >> rsMI.strSpeaker
        >> rsMI.strImagePath
        >> rsMI.strTextBeforeImage
        >> rsMI.strTextAfterImage
        >> rsMI.iImageWidthAsPercent
        >> rsMI.iSongOpen
        >> rsMI.iSongClose;
}

But I now wat to introduce version tracking so that I can cope with new fields without causing crashes in my software for users.

So now I would like:

friend CArchive& operator>>(CArchive& rArchive, S_MEMORIAL_INFO const& rsMI)
{
    WORD wVersion{};

    rArchive >> wVersion
        >> rsMI.strHost
        >> rsMI.strCohost
        >> rsMI.strZoomAttendant
        >> rsMI.strChairman
        >> rsMI.strPrayerOpen
        >> rsMI.strPrayerClose
        >> rsMI.strSpeaker;

    rsMI.strTheme.Empty();
    if(wVersion >= 2)
        rArchive >> rsMI.strTheme;

    return rArchive >> rsMI.strImagePath
        >> rsMI.strTextBeforeImage
        >> rsMI.strTextAfterImage
        >> rsMI.iImageWidthAsPercent
        >> rsMI.iSongOpen
        >> rsMI.iSongClose;

}

friend CArchive& operator<<(CArchive& rArchive, S_MEMORIAL_INFO& rsMI)
{
    WORD wVersion = 2;
    return rArchive << wVersion
        << rsMI.strHost
        << rsMI.strCohost
        << rsMI.strZoomAttendant
        << rsMI.strChairman
        << rsMI.strPrayerOpen
        << rsMI.strPrayerClose
        << rsMI.strSpeaker
        << rsMI.strTheme
        << rsMI.strImagePath
        << rsMI.strTextBeforeImage
        << rsMI.strTextAfterImage
        << rsMI.iImageWidthAsPercent
        << rsMI.iSongOpen
        << rsMI.iSongClose;
}

How can I now cope with the fact that for some users there is no WORD value there in the archive?

With hindsight I would have designed the serialization to write a version number right from the outset, but too late for that now.

Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
  • 2
    I would try reading the `wVersion` first, and if it's not 2 (or any other expected version number) then go back (`GetFile()->SeekToBegin();` - don't know if this works, you will have to test) and read the rest. It's very unlikely that a string (`strHost`) would read as 2 in a `WORD` variable. I would recommend also adding a "signature" field (a 4-byte int, like 596831197) before the version number, denoting your file format, and check against this value instead, to determine whether the file contains a version tracking construct (signature + version) or not. – Constantine Georgiou Oct 22 '22 at 09:54
  • @ConstantineGeorgiou I don't think I can simply seek back to the beginning because this is just a structure part is part of a larger archive. It can appear anywhere in the archive. – Andrew Truckle Oct 22 '22 at 09:59
  • 1
    I *guess* you can introduce versioning into `CArchive`s after they have been deployed. [`CArchive::GetObjectSchema`](https://learn.microsoft.com/en-us/cpp/mfc/reference/carchive-class#getobjectschema) is the core `CArchive` implementation responsible for versioning. I'm sure there is supplemental documentation available, somewhere. – IInspectable Oct 22 '22 at 10:06
  • @IInspectable Actually, I am making use of the object schema, but at the moment I have it set to a specific value `MSA_SCHEMA`. This is because the scheme can't have large numbers, so I then save my own version info into the archive. I was assuming that this structure should have a distinct versioning system as opposed to the global archive versioning. In theory I could introduce a MSA_VERSION_V1 and a MSA_VERSION_V2 schema etc and then do conditional processing. I was just hoping to keep it localized to the structure. – Andrew Truckle Oct 22 '22 at 10:10
  • 1
    Then you could try `GetFile()->Seek(-2, CFile::current);` and if this doesn't work either then try `lPos=GetFile()->Seek(0, CFile::current); rArchive >> wVersion; if (wVersion!=2) GetFile()->Seek(lPos, CFile::begin);` ie get the file position before attempting reading `wVersion` and reposition it there, if it isn't a valid version number. – Constantine Georgiou Oct 22 '22 at 11:35
  • @ConstantineGeorgiou It seems to return 65279 if it is not a version number – Andrew Truckle Oct 22 '22 at 12:23
  • @ConstantineGeorgiou Does not work. After the test of the version and the reset of file post I get then exception when it does normal reading. – Andrew Truckle Oct 22 '22 at 12:27
  • @IInspectable The problem with GetObjectSchema is that it can only be called once. And I already call it when I begin serializing, so I can't call it again from inside this structures operator because it returns -1. – Andrew Truckle Oct 22 '22 at 13:00
  • 1
    Hmm yes, 65279 is the hex value FF FE, which is the prefix for `CString` objects. `CArchive` performs its own buffer management, and resetting the file's current position isn't enough; `rArchive>>wVersion` eats 2 bytes from the CString prefix, which causes the exception when reading the string is attempted. I'll see what I can do. – Constantine Georgiou Oct 22 '22 at 14:35
  • 1
    `FF FE` isn't the prefix for `CString` objects. It's the UTF-16 [BOM](https://en.wikipedia.org/wiki/Byte_order_mark). – IInspectable Oct 23 '22 at 16:21
  • 1
    @IInspectable, `FF FE` is really the BOM hdr for UTF16LE-encoded text-files, however the file format written by MFC's `CArchive` class is not text (it's custom) and the value `FF FE` value is just coincidental, it's a "tag" in this case. You can see this in the `AfxWriteStringLength()` function in arccore.cpp, called in afx.inl. It isn't stored necessarily at the beginning of the file, instead it is indeed used as a prefix for `CString` objects. And removing the first two bytes of it causes the string reading to fail. – Constantine Georgiou Oct 23 '22 at 17:26

1 Answers1

2

As noted in the comments, the problem is that when reading wVersion from the archive this removes two bytes from the CString object following. CArchive performs its own buffer management. The class definition contains several member variables for this, and it's quite easy to find out what they are doing. These members are protected though, so we need to define a derived class, containing a member which sets the current buffer pointer two bytes back:

class CArchiveHlp : public CArchive
{
    public :
    void GoBack2()
    {
        if (m_lpBufCur - m_lpBufStart < 2)
            AfxThrowFileException(CFileException::genericException);
        m_lpBufCur -= 2;
    }
};

Then you can use this class in your code like this:

    rArchive >> wVersion;
    if (wVersion != 2) // Or any other valid version number
    {
        ((CArchiveHlp*)&rArchive)->GoBack2();
        wVersion = 0;
    }
    rArchive >> rsMI.strHost
    .
    .

The (CArchiveHlp*) cast is wrong in some way because the actual object is not CArchiveHlp (a dynamic_cast would fail here), however the classes are almost identical, and the GoBack2() member is not virtual (so no v-table), therefore you can call it without any problem - it calls CArchiveHlp code for a CArchive class instance, whose data in memory are identical.

It's tested and works.

Constantine Georgiou
  • 2,412
  • 1
  • 13
  • 17