IFF Parsing Library -- Documentation 0. Background This documents the design and programmer interface for the low-level IFF parsing library ("iffparse.library"). This is written for programmers, especially those who are familiar with what it takes to read and write IFF files correctly. It also assumes that the reader is familiar with the IFF file format and it's restrictions. Take the time to read the EA IFF 85 specification -- it's quite interesting, and it helps to clarify what the format is about. After that, read it again. Unfortunately, there are quite a few areas that the original specification leaves ambiguous, but we have tried to write the library in as much of the original spirit of IFF as possible. Programs that write bad IFF files will break. Programmers that straddle the line into the ambiguous areas may find their code broken too; it can't be avoided. Hopefully the negative impact will be minimal. 1. Scope The IFF library is intended to make reading and writing IFF files easy. While it is a relatively straightforward matter to write a program to read a simple ILBM, writing code that correctly handles all the complexity of the full IFF specification is a fairly difficult task. The IFF parsing library is designed to deal with the general case in a way that makes the specific case easy as well. The IFF parsing library only deals with those elements that are common to all IFF files, such as parsing generic chunks and dealing with the proper scoping for property chunks. It provides "hooks" (in the form of custom handlers) for any more advanced features that a programmer might need to implement. The IFF parsing library operates on arbitrary streams, including whatever new streams the client programmer may care to invent. Currently DOS files and the clipboard are supported internally. The programmer can use these types of streams or can define his own by providing functions to read, write and seek them. The library can operate equally well on seekable (random access) streams such as disk files, and non-seekable streams such as pipes. 2. Programmer Interface Most of the functions in the parsing library operate on an instance of an IFF_File struct. This structure is a handle on an IFF class stream which is to be read or written. The client programmer must create, initialize and open this structure before the associated IFF file can be read or written. The library provides some functions for doing this required setup. 2a. AllocIFF() / FreeIFF() First thing a client program must do is create an new IFF_File structure with AllocIFF(). The calling sequence is: struct IFF_File *iff; iff = AllocIFF (); If the result is a null pointer, the allocation failed. An IFF_File struct created with AllocIFF() should be disposed of with FreeIFF(), like so: FreeIFF (iff); 2b. Stream initialization The client program must specify what stream this raw IFF_File struct is to be associated with. The supported stream types are DOS files and the clipboard (see the internal implementation details to see how to define your own stream types). 2b(i). DOS streams To initialize an IFF_File as a DOS stream, use the InitIFFasDOS() function and set the iff_Stream field of the IFF_File structure to be the BPTR to a FileHandle (the result from a call to Open()). InitIFFasDOS() takes as arguments a pointer to the IFF_File struct to initialize, and a mode flag to indicate if the stream is a read or write stream. For example, to initialize a stream for read, do: struct IFF_File *iff; iff -> iff_Stream = Open ("myfile", MODE_OLDFILE); ... /* check for Open errors */ InitIFFasDOS (iff, IFFM_READ); 2b(ii). Clipboard streams For the purpose of supporting clipboard streams, the IFF library defines a handle structure to access the clipboard.device. Each clipboard IFF stream has one of these handles (like the FileHandle for DOS streams). The OpenClipboard() and CloseClipboard() functions create and delete these handles and are analogous to Open() and Close() under DOS. To initialize an IFF_File struct as a clipboard stream, assign the iff_Stream field in the structure to be an open clipboard handle and call InitIFFasClip(), like so: struct IFF_File *iff; iff -> iff_Stream = OpenClipboard (PRIMARY_CLIP); ... /* check for Open errors */ InitIFFasClip (iff); The argument to OpenClipboard() is the clipboard "unit" number and is normally PRIMARY_CLIP. Notice that neither of these calls specifies the I/O direction for the clipboard. This is because the same clipboard stream can be read or written many times. To read or write to a clipboard stream, use the BeginCB_Read()/EndCB_Read() or BeginCB_Write()/EndCB_Write() function pairs around the reads or writes you want to perform. 2b(iii). User Defined Streams The client programmer can define his own streams by creating read, write and seek functions for his stream and passing them to the library. The function for this is: InitIFF (iff, flags, stub, read, write, seek); where "stub," "read," "write" and "seek" are function pointers. Their format is described in more detail in the documentation for this function. The client defines the type of stream by setting the following bits in "flags": IFFM_MODE - this bit should have value IFFM_READ or IFFM_WRITE and indicates the I/O direction. IFFM_FSEEK - set this if the stream accepts forward seeks only. IFFM_RSEEK - set this if the stream accepts random access seeks (supercedes _FSEEK). Non-seeking streams will have neither bit set. 2c. OpenIFF() / CloseIFF() Once the IFF stream is ready to be read or written, the operation is initiated with OpenIFF() and ended with CloseIFF(). OpenIFF() simply prepares the IFF_File structure for a new I/O operation. CloseIFF() cleans up the IFF_File structure, freeing anything left over from the read or write operation. CloseIFF() can be used to end a read or write at any time. 2d. Summary In summary, there are three preparatory steps for using the IFF parsing library -- creating an IFF_File struct, initializing the stream and opening the IFF. After reading or writing the IFF file, the IFF_File structure used to do it must be closed and freed. The steps are summarized below (error checking has been removed for clarity -- KIDS: don't try this in your own code!): struct IFF_File *iff; /* * DOS_Stream and Clipboard_Stream are flags for the two * supported stream classes. */ /* BEGIN */ /* * Get an instance of an IFF_File struct. */ iff = AllocIFF (); /* * Initialize the stream. */ if (DOS_stream) { iff -> iff_Stream = Open (...); InitIFFasDOS (iff, IFFM_READ); /* or IFFM_WRITE */ } else if (Clipboard_stream) { iff -> iff_Stream = OpenClipboard (PRIMARY_CLIP); InitIFFasClip (iff); } /* * Do stuff -- parse or write an IFF file. */ OpenIFF (iff); if (Clipboard_stream) BeginCB_Read (iff); /* or BeginCB_Write() */ DoStuff (iff); if (Clipboard_stream) EndCB_Read (iff); /* or EndCB_Write() */ CloseIFF (iff); /* * Done with IFF -- close the stream. */ if (DOS_stream) { Close (iff -> iff_Stream); } else if (Clipboard_stream) { CloseClipboard (iff -> iff_Stream); } /* * Free up IFF_File struct. */ FreeIFF (iff); /* DONE */ The steps between AllocIFF() and FreeIFF() can be done any number of times for the same IFF_File structure, and in the case of the clipboard, the steps between OpenClipboard() and CloseClipboard() can be done any number of times. What goes on in DoStuff()? Read on. 3. Reading Reading IFF files with the parsing library can be as simple or as complex as required. All IFFs are parsed by calling the single function ParseIFF(). The client specifies what function he wants ParseIFF() to perform by specifying how ParseIFF() should respond when it encounters certain types of chunks in the IFF file. This set of responses get stored in the IFF_File struct. For a virgin IFF_File struct, the ParseIFF() function will do nothing special, but it will still traverse the entire file and return an error code if encounters anything odd about the file it's traversing. If this is a valid and syntactically correct IFF file, ParseIFF() will return IFFERR_EOF meaning that it reached the end of the file. The parsing library has several predefined actions supported for dealing with chunks (more can be added with custom handlers). The parser can be directed to halt and return to the caller when it encounters a certain type of chunk, or it can squirrel the contents of the chunk away for future use. These two actions represent the general method for handling the two common types of chunks in IFF files -- data chunks and property chunks. The client can say which chunks (if any) should cause these actions with StopChunk() and PropChunk(). 3a. StopChunk() StopChunk() allows the client to declare "stop" chunks, that is, chunks which cause the parser to return to the client when it enters them. Once StopChunk() is successfully called, the ParseIFF() function will return control to the client when the parser enters such a chunk. The chunk's contents can then be processed by the calling program and parsing resumed with ParseIFF() again. For example, suppose you wanted to read a color map out of an ILBM. This means that you would want to search through an IFF file looking for a CMAP chunk inside a FORM ILBM. You could use StopChunk() to do this: error = StopChunk (iff, ID_ILBM, ID_CMAP); If the return value, "error," is zero, then the specified IFF_File struct is now defined such that ParseIFF() will return when a CMAP chunk within an ILBM context is found. So the call, error = ParseIFF (iff, IFFPARSE_SCAN); will return zero if such a CMAP chunk is encountered, IFFERR_EOF if there is no such chunk, or an error code if there is something wrong with the IFF structure of the file. If it returns zero, then the stream associated with the IFF_File struct is positioned just at the beginning of the data in the CMAP chunk and can be read with one of the chunk reading functions, such as ReadChunkRecords(). So the whole sequence required to read a CMAP out of an open IFF file is just this: error = StopChunk (iff, ID_ILBM, ID_CMAP); if (error) fatal_error ("can't declare stop chunk", error); error = ParseIFF (iff, IFFPARSE_SCAN); if (error) fatal_error ("color map not found", error); ncolors = ReadChunkRecords (iff, &colormap, 3, 32); if (ncolors <= 0) fatal_error ("error reading color map", ncolors); printf ("found %d colors\n", ncolors); At this point, parsing could be resumed with another call to ParseIFF(). 3b. PropChunk() Now suppose you want to do something more elaborate, like read an ILBM and display a picture. To do this, you need to read not only the CMAP chunk but also the BODY chunk which contains the picture data itself and the BMHD chunk which contains parameters essential to interpreting the BODY data. You could do all this with StopChunk(), just setting the parser to stop on all these types of chunks and processing each as you encounter it. This is what many IFF readers do today, but it will not always work. The problem, of course, is that there can be many BMHD or BODY chunks in a single IFF file and you need to actually parse the structure to know which header goes with which body. Fortunately, the IFF parsing library provides a simple way to do this through the PropChunk() handler. In the language of IFF, BMHD and CMAP chunks are properties of the BODY data chunk. (CMAP is really more like a data chunk, but this is one of the somewhat ambiguous areas of the IFF specification.) Like StopChunk(), PropChunk() lets the client program associate a certain action with a certain type of chunk. In this case, the contents of the chunk will be stored during parsing without the client programmer knowing about it. At any point in the parse, the client can search for the stored version of the chunk valid in this context with FindProp(). The parser and PropChunk() take care of managing the lexical scoping rules for property chunks so that the data returned by FindProp() will always be valid for the given context. Although this may sound complex, the programmer interface is very simple. Here's example code to read a BODY chunk from an ILBM along with its associated properties. struct IFF_File *iff; struct StoredProperty *bmhd, *cmap; LONG ilbmprops[] = { ID_ILBM, ID_CMAP, ID_ILBM, ID_BMHD }; /* IFF_File struct already allocated and initialized for read. */ /* * Declare the properties to store. */ error = PropChunks (iff, ilbmprops, 2); /* * Declare the chunk to stop on. */ error = StopChunk (iff, ID_ILBM, ID_BODY); /* * Parse the file, searching for the BODY chunk and storing * the related properties on the way. */ error = ParseIFF (iff, IFFPARSE_SCAN); if (error == IFFERR_EOF) { ... no BODY chunk ... }; if (error != 0) { ... bad IFF file ... }; /* * "error" was 0, so the parser is stopped at the start of the * BODY chunk, and the properties are stored. Recall the stored * properties with FindProp(). */ bmhd = FindProp (iff, ID_ILBM, ID_BMHD); cmap = FindProp (iff, ID_ILBM, ID_CMAP); /* * If either value returned is null, then the property chunk was * not encountered in the IFF file before the BODY chunk. */ if (!bmhd || !cmap) { ... properties not found ... }; /* * Pass the header info and color map to another routine that * will read the data from the current position in the IFF * stream and do something with it. */ ProcessBody (iff, bmhd->sp_Data, cmap->sp_Data, cmap->sp_Size/3); After reading the body, the code could loop back to the ParseIFF() call and continue collecting BODY's. The CMAP and BMHD returned by FindProp() will always be the correct ones for each BODY chunk. 3c. Parse State Suppose you declare multiple stop chunks for a single IFF file. Once ParseIFF() returns with a zero value, you know that the parser has stopped at one of the chunks you wanted, but how do you tell which one? To determine this, the library provides methods for querying the current state of the parser. The functions CurrentChunk() and ParentChunk() get pointers to ContextNode structures which can be examined. CurrentChunk() returns a pointer to a context node for the chunk currently being parsed. For the example above, this is exactly the chunk you are interested in. Although much of the actual context node is private to the library there are a few public fields for finding information about the chunk. The cn_ID and cn_Type fields are the chunk identifier longword and the format type of the chunk, respectively, and cn_Size is the total size of this chunk in bytes. ParentChunk() takes a pointer to a context node and returns the pointer to the parent node, which corresponds to the chunk which encloses the given chunk. For a CMAP chunk in an ILBM for example, ParentChunk() will return the context node for the FORM ILBM chunk that contains the CMAP chunk. 3d. StopOnExit() The ILBM format is somewhat special in that it can be interpreted by only reading one data chunk (BODY) and its associated property chunks. Most IFF files, however, require that you read multiple data chunks before interpreting the meaning of the FORM. In order to facilitate this, the library provides the StopOnExit() call for pre-defining chunks that should cause the parser to return when they are exhausted and about to be popped. Calling this function for the FORM chunk itself provides a way for the client program to be notified when all the data chunks in a FORM have been read. When ParseIFF() returns when at the end of a chunk, it returns the value IFFERR_EOC rather than zero. The following code fragment demonstrates the use of StopOnExit() and how it can be used with StopChunk() to keep track of when the parser is traversing a particular chunk (in this case a FORM ILBM). struct IFF_File *iff; LONG error; /* "iff" allocated and initialized. */ /* * Set up the IFF_File struct to stop when entering or leaving * FORM chunks of type ILBM. */ StopChunk (iff, ID_ILBM, ID_FORM); StopOnExit (iff, ID_ILBM, ID_FORM); while (1) { /* * Advance the parser to the next stop point and exit at * end of file. Error code determines what we found. */ error = ParseIFF (iff, IFFPARSE_SCAN); if (error == IFFERR_EOF) break; if (error == 0) { puts ("entering FORM ILBM."); continue; } if (error == IFFERR_EOC) { puts ("leaving FORM ILBM."); continue; } printf ("IFF error: %d\n", error); break; } 3e. ParseIFF() Control Values. The ParseIFF() function takes a control argument. This can have one of three values, and sets, essentially, how much control the calling program wants to exercise over the parsing process. The normal control value, IFFPARSE_SCAN, causes the function to scan the file and invoke the handlers for the chunks. In this control mode, ParseIFF() will only return on end of file, an error, or a stop chunk. The other two possible control values are IFFPARSE_STEP and _RAWSTEP. In both these modes, ParseIFF() will return every time a new chunk is entered (pushed) or just before an exhausted chunk is exited (popped). If the parser just entered a chunk, ParseIFF() returns zero, and if the parser is pausing at the end of a chunk, ParseIFF() returns IFFERR_EOC. In STEP mode, the handlers for chunks will still be called, while in RAWSTEP mode, the parser will return without calling any handlers. The single step modes are useful for using the IFF parsing library as the "back end" for a custom parser (examples are 'sift.c' and 'tree.c'). 4. Writing Examples Writing IFF files is somewhat more straightforward than reading them, since you know in advance what you want to put in them and in what order. Initializing a stream for write depends on the stream: for DOS files, it just means opening the file with write access; for the clipboard it means calling BeginCB_Write(). For a custom stream type, set the IFFM_MODE bit in the iff_Flags word to IFFM_WRITE. Once the IFF file stream is ready to write, the client can write chunks and their data using PushChunk(), WriteChunkBytes() or WriteChunkRecords(), and PopChunk(). PushChunk() opens a new chunk and PopChunk() closes it and chunks can nest according to the rules of IFF files. Every IFF file is really a single chunk with all the data within as nested sub-chunks, so every IFF file is written with a single PushChunk() / PopChunk() pair surrounding all the writes. PushChunk() takes several arguments and returns an error code: LONG PushChunk (iff, type, id, size) The first argument is the IFF_File struct itself. The second is the format type code which is ignored for data chunks since they inherit their type from their parent chunk. The third is the chunk id code, which must be one of FORM, CAT or LIST for the first chunk pushed. Size is the size of this chunk in bytes. If this argument is IFF_SIZEUNKNOWN, the library will compute the size of the chunk from the data written into it. If it has some other value, the chunk will be forced to conform to the size given. As a writing example, a simple bitmap can be written as a FORM chunk of type ILBM with the following sub-chunks: BMHD - bitmap header data (always first). CMAP - color map data. BODY - interleaved bitmap data (always last). The following program might be used to write such a file: struct IFF_File *iff; struct BitmapHeader *header; LONG error, size; /* "iff" already allocated and stream initialized. */ OpenIFF (iff); /* * Start the all-encompassing FORM chunk for the file. */ error = PushChunk (iff, ID_ILBM, ID_FORM, IFF_SIZEUNKNOWN); if (error) ...fail; /* * Write the BMHD chunk. These are always the same size. */ error = PushChunk (iff, 0, ID_BMHD, sizeof (*header)); if (error) ...fail; size = WriteChunkBytes (iff, header, sizeof (*header)); if (size != sizeof (*header)) ...fail; error = PopChunk (iff); if (error) ...fail; /* * Write the CMAP chunk. Three times the number of colors. */ error = PushChunk (iff, 0, ID_CMAP, IFF_SIZEUNKNOWN); if (error) ...fail; size = WriteChunkRecords (iff, colors, 3, ncolor); if (size != ncolor) ...fail; error = PopChunk (iff); if (error) ...fail; /* * Write the BODY chunk. Calls WriteBODY() which would * presumably be a function that just calls WriteChunkBytes/Records * many times and then returns. */ error = PushChunk (iff, 0, ID_BODY, IFF_SIZEUNKNOWN); if (error) ...fail; error = WriteBODY (iff, header); if (error) ...fail; error = PopChunk (iff); if (error) ...fail; /* * Close the outermost FORM chunk context. Doing this makes the * file effectively unwritable from this point. */ error = PopChunk (iff); if (error) ...fail; CloseIFF (iff); You will notice that there are a lot of statements like: if (error) ...fail; How to deal with errors depends on how the code is written, but in general, once an error occurs, calling CloseIFF() on the offending file will shut things down cleanly. So the "...fail" expression could be a _goto_ passing control to the CloseIFF() call, or, if the writing calls are all kept in a subroutine, it could be a _return_ statement. Errors will occur not only for errors encountered writing to the stream, but also for IFF format errors. Programmers are encouraged to observe full error checking at all times, especially during initial testing since the library may signal IFF syntax errors that are part of the program itself. The writer is not guaranteed to catch all format errors, however, and programmers should always test their files with an IFF reader or scanner for the format they are writing. 5. Orthogonality In general, anything that works in read mode will work in write mode. The chunk context query functions, CurrentChunk() and ParentChunk(), as well as the value of the iff_Depth field, will give reasonable answers in write mode. Chunk handlers, however, do not work in write mode. By the same token, PushChunk() and PopChunk() will also work in read mode. Calling PushChunk() on a read mode file will force the parser to step into a new chunk at the given point in the file, and calling PopChunk() will force the parser to exit the current chunk, skipping past any data in the chunk that have not been read. Generally, however, you should not mix these low-level calls with calls to the more high-level ParseIFF(). 6. Custom Handlers All the types of actions that can be performed when encountering or leaving a certain chunk, such as those initialized with PropChunk(), StopChunk() and StopOnExit(), are just built-in chunk handlers. If these actions are not complex enough, the client programmer can install his own custom handlers to perform any action he needs on entering or exiting a chunk. Chunk handlers are added to a given IFF_File struct with the EntryHandler() and ExitHandler() functions: error = EntryHandler (iff, type, id, func, stub, pos) error = ExitHandler (iff, type, id, func, stub, pos) "Type" and "id," as always, indicate what chunk to handle. "Func" is the client supplied handler function. This function will be called using the call-back stub, "stub." The "pos" argument indicates where the handler will be valid relative to the current context. 6a. User Call-backs Stub The library calls user code by putting arguments in registers, but since this is not convenient for programmers writing in a high-level language such as 'C', the library allows for different calling conventions with the call-back stub. The call-back stub for a language, GenericStub(), is a small assembly language routine that translates the register arguments to the calling convention of the host language. GenericStub() takes the four registers A0, A1, D0 and D1 and passes them to the actual function being called as if they were arguments. (The GenericStub() for small model Aztec C programs also restores the base register A4.) In the case of chunk handlers, A0 is a pointer to the IFF_File struct being parsed and A1 is a pointer to the ContextNode struct for this chunk. As result, user handler functions should receive the following four arguments: LONG MyHandler (iff, chp, D0, D1) struct IFF_File *iff; struct ContextNode *chp; LONG D0, D1; { ... } The D0 and D1 arguments have no meaning but are included since the stub function will provide them. 6b. Handler Scoping The "pos" argument to the handler installation functions indicates the handler's scope -- where it's valid. There are three possible scopes: ROOT, TOP and PROP. A pos of IFFSLI_ROOT will install the handler into the global context which makes it valid throughout parsing. Any time a chunk with the given type and id is encountered, a handler in the global, or default, context will be called. A pos of IFFSLI_TOP will install the handler into the current context, meaning that it will only be valid as long as the current chunk is being parsed. It will also override any handlers installed at a lower level, such as one for a chunk containing this chunk, or a handler installed in the default context. Once the current chunk ends, the handler will be purged. IFFSLI_PROP scoping is like the TOP option except that the handler is valid for the nearest containing FORM or LIST chunk. This corresponds to the scoping for shared properties. The ability to override other handlers is useful for scanning nested formats. For example, the client might install default handlers for ILBM chunks to be used for interpreting a bitmap format. But if the parser encounters a FORM ANIM, the client might want to install different handlers for ILBM chunks for the course of parsing the ANIM. By installing handlers with IFFSLI_TOP position when first entering the FORM chunk for the ANIM, they will only be valid while the ANIM is being scanned and will automatically go away when that FORM chunk ends. All the built-in handler installers, such as PropChunk() and StopChunk(), use TOP position for their handlers. This is the same as ROOT position if no chunk is currently being parsed, like just before or after an OpenIFF() call. 6c. Handler Return Value Chunk handlers return a value in D0 (or are defined as LONG functions in 'C'). This is their error return code. Returning zero allows the parsing to continue normally, and any other value will cause ParseIFF() to return that value to the client as its returned error code. The exception is the special code, IFF_RETURN2CLIENT, which will signal ParseIFF() to return to the caller but to return zero. If a custom chunk handler gets an error from an IFF library function, it should normally pass that error code on in D0. A handler can in general return any value, but negative codes are reserved for library internal errors. Unless the handler is using its return value to signal something special to the calling program, or if it caught an error, it should return zero. 6d. Summary The client program can arrange for any function to be called when the parser enters or exits any chunk. These functions, called "handlers," should either accept their arguments in registers, or be set up to get their arguments translated by a stub function. The provided stub function for each language, GenericStub(), takes the arguments A0, A1, D0, D1 and passes them in that order to the user's function. Handlers get the two arguments: A0 -- pointer to the IFF_File struct being parsed. A1 -- pointer to the ContextNode struct for this chunk. Handlers are installed with the ExitHandler() and EntryHandler() functions, which specify a stub function, if any, and a context position argument. Entry handlers get called just after their chunk is pushed, and exit handlers get called just before their chunk is popped. 7. Internal Design - The Context Stack At the heart of the IFF_File structure lies the Context Stack. This is a LIFO stack of ContextNode structures, each one corresponding to a chunk in the current parse state. As the parser (or writer) traverses the file, it pushes and pops context nodes on and off the context stack to maintain an internal model of the file structure. Arbitrary data can be associated with a context node and will be handled appropriately to its scoping context. Each ContextNode element on the stack has fields for chunk ID, type, size, and a count of the number of bytes already read from this chunk. As an example of how this stack structure gets used to read IFF files, consider the simple image file (which might have been written by the writing example): "FORM" { "ILBM" "BMHD" {20 bytes} "CMAP" {21 bytes} "BODY" {32000 bytes} } When ParseIFF() is first called, the first chunk from the file is pushed onto the stack (remember - the entire file is one chunk). So at this step, the stack consists of one element: +----------+ |FORM 32070| |ILBM 4 | +----------+ This means that the current chunk being parsed has ID `FORM', type `ILBM' and contains 32070 bytes of which 4 bytes have been read (the type identifier itself). Because FORM chunks are grouping chunks in the IFF format, the parser will push a new chunk element on the context stack, in this case, the BMHD chunk. +----------+ +----------+ |BMHD 20 | |FORM 32070| |ILBM 0 | |ILBM 12 | +----------+ +----------+ At this point, the file is positioned ready to read the first data byte in the BMHD chunk, and the context stack correctly reflects this by showing that the top chunk is the BMHD chunk and zero bytes have been read. The bytes read count of the parent FORM chunk has increased to 12, since the 8 bytes required to push the BMHD chunk on the stack get counted against the total in the FORM. The application program or a handler could then read out the BMHD chunk's contents, whichever is more appropriate. In either case, the chunk reading routines will update the byte count to reflect the number of bytes read. +----------+ +----------+ |BMHD 20 | |FORM 32070| |ILBM 20 | |ILBM 12 | +----------+ +----------+ When the chunk is completely processed, any bytes unread will be skipped (optional pad bytes will be skipped at this point), the chunk will be popped off the stack, and it's parent context element will be updated to indicate how many bytes have been read out of it at this stage. The top context element will always have a correct scan count, even though lower elements may be out of step -- they will be caught up when they become the top element. After the BMHD chunk has been read, the context stack looks like this: +----------+ |FORM 32070| |ILBM 32 | +----------+ Reading the CMAP and BODY chunks go the same way ... +----------+ +----------+ |CMAP 21 | |FORM 32070| |ILBM 0 | |ILBM 40 | +----------+ +----------+ +----------+ +----------+ |BODY 32000| |FORM 32070| |ILBM 0 | |ILBM 70 | +----------+ +----------+ After everything is read, the top context element looks like this: +----------+ |FORM 32070| |ILBM 32070| +----------+ Once this main context is popped, the ParseIFF() function will return IFFERR_EOF. 7a. Local Context Items Each context node also has a list of associated items, called Local Context Items. These items can be anything, and are identified by an ident code as well as an ID and type code just like chunks. Each local context item also has its own purge function vector, so that the item can be disposed of by the library when it pops its context node. LocalContextItems are allocated by the client programmer with the AllocLocalItem function. item = AllocContextItem (ident, type, id, usize) The "ident" argument is the major class type of item being created. This is just a longword identifier like chunk type and id codes, and is created the same way (but out of lower-case letters to be distinct from chunk format codes). There are a few predefied local context class ident codes that are used internally: "enhd" -- entry handler "exhd" -- exit handler "prop" -- stored property "coll" -- collection list It's clear from these examples that many of the basic capabilities of the parser are implemented as special classes of local context items. In fact the context stack and local context items underlie most of what the parser does. The "type" and "id" arguments are two more identifying longword codes for the item being allocated. Often these are the analogous to the type and id for chunks, but they don't have to be. The "usize" argument is the number of bytes of use data space to allocate along with this local context item. To get access to this block of user data space, use the LocalItemData function: data = LocalItemData (item) The function returns a pointer to the user data associated with the local context item "item". This is the only supported way to get a handle on this data. Local context items can be attached to any context node in the context stack, as well as to the "default" context associated with an IFF_File structure. This can be done with the StoreLocalItem or StoreItemInContext functions: error = StoreLocalItem (iff, item, pos) StoreItemInContext (iff, item, cnode) StoreLocalItem stores an item relative to the current parse context according to the "pos" argument which can have values of IFFSLI_TOP, the "top" or current context, IFFSLI_ROOT, the "root" or default context, or IFFSLI_PROP, the property scope closest to the current context. StoreItemInContext attaches an item directly to a context node given a pointer to it. 7b. Local Context Item Purge Vectors. When a Context Node is popped off the context stack (i.e. when its chunk is exited) the local items associated with it get purged. Normally, the library just calls FreeLocalItem(item) to deallocate items, but since there can be any user data also stored in the local item, the library also provides a mechanism for the user to do his own clean up of items to be purged. The user cna set his own purge vectors with: SetLocalItemPurge (item, stub, purge) The purge vector will be called (via the stub, if any) with the following arguments: A0 -- pointer to the IFF_File struct. A1 -- pointer to the LocalContextItem struct to be deleted. The user should do his own clean up and then call FreeLocalItem on the item to free it and the user data block. Items that have been stored in the default (or root) context will not be purged until the IFF_File structure itself is disposed of with FreeIFF. 7c. Finding Local Context Items. Once items have been stored in the context stack, they can be retrieved using the FindLocalItem function. The way that FindLocalItem goes about locating the requested item allows for the transparent handling of the IFF scoping rules. item = FindLocalItem (iff, ident, type, id) Given a set of ident, type and id codes to look for, this function first looks at the current context level in the context stack for an item that matches the given codes. If none is found, it will look in the next deeper context, and then the next deeper and so on. If it cannot find any in the stack itself it will then search the default item list. It will return the first item it finds that matches the three code values. If it can find none, it will return NULL. 7d. Stored Properties: An Example of Local Context Items. The behavior of FindLocalItem makes dealing with stored properties almost trivial, so by way of illustration, the operation of how properties are stored will be explained with a detailed example. What happens internally is that as the reader runs into recognized property chunks, it stores them away as local context items. When the user wants them, the library uses FindLocalItem to find the instance of stored property that is closest to the top of the stack and returns that to the user. As context stack elements are popped, invalid stored property chunks are deleted and older chunks which are now valid come to the front again. As a specific example, take this complex IFF file: "LIST" { "JUNK" "PROP" { "ILBM" "BMHD" {...} "CMAP" {...} } "FORM" { "ILBM" "BODY" {...} } "FORM" { "ILBM" "CMAP" {...} "BODY" {...} } } It is a LIST containing two ILBM FORM chunks with a PROP chunk declaring default BMHD and CMAP chunks for ILBM FORMs. Since the user is interested in interpreting the ILBM BODY chunks with BMHD and CMAP chunks as properties, he will declare them as properties with the PropChunk function. What this function does is install an entry handler for ILBM BMHD and ILBM CMAP chunks which will store them as local context items in a property scope. The parsing sequence for this file starts with the context stack being initialized to the overall chunk for the file: +----------+ |LIST 64144| |JUNK 4 | +----------+ The parser then pushes elements for the PROP and its internal BMHD chunk. +----------+ +----------+ +----------+ |BMHD 20 | |PROP 62 | |LIST 64144| |ILBM 0 | |ILBM 12 | |JUNK 12 | +----------+ +----------+ +----------+ Now the parser invokes the handler for this ILBM.BMHD chunk and the handler reads it itself, storing the contents of the chunk in a buffer. It then attaches this buffer to a local context item and stores it in the PROP context for this chunk. This will be the LIST element in the context stack, since this chunk represents the context in which this property is valid. Once the reader exits the LIST chunk, the property will no longer be valid and will be automatically expunged. The local item will have an ident of "prop" and type and id of ILBM and BMHD. +----------+ +----------+ +----------+ |BMHD 20 | |PROP 62 | |LIST 64144| |ILBM 20 | |ILBM 12 | |JUNK 12 | +----------+ +----------+ +----------+ | prop ILBM.BMHD The parser processes the CMAP chunk the same way and pops the PROP element since it's exhausted. The remaining LIST context element has property chunks hanging off of it stored as local context items as it begins to parse the next chunk. +----------+ |LIST 64144| |JUNK 74 | +----------+ | prop ILBM.BMHD prop ILBM.CMAP When the parser reaches the BODY chunk, the stack looks like this: +----------+ +----------+ +----------+ |BODY 32000| |FORM 32012| |LIST 64144| |ILBM 0 | |ILBM 12 | |JUNK 82 | +----------+ +----------+ +----------+ | prop ILBM.BMHD prop ILBM.CMAP Since the user needs the stored properties to correctly process the BODY chunk, he will recall the stored properties with FindProp. This function just calls FindLocalItem for "prop" ident items with the requested type and id which finds the stored property chunks attached to the LIST context node. This is the correct behavior here since this FORM has no overriding properties. Parsing the next ILBM FORM is different, however. As the parser enters this FORM, it invoke the property handler for the CMAP chunk. +----------+ +----------+ +----------+ |CMAP 21 | |FORM 32042| |LIST 64144| |ILBM 0 | |ILBM 12 | |JUNK 32102| +----------+ +----------+ +----------+ | prop ILBM.BMHD prop ILBM.CMAP The handler reads the contents of the CMAP and attaches it to the context element for the FORM, since that is the scope for this property. Now, when the reader encounters the BODY chunk and passes control to the user to deal with it, the user sees the correct properties for the given context when using FindProp(). +----------+ +----------+ +----------+ |BODY 32000| |FORM 32042| |LIST 64144| |ILBM 0 | |ILBM 34 | |JUNK 32102| +----------+ +----------+ +----------+ | | prop ILBM.CMAP prop ILBM.BMHD prop ILBM.CMAP When the application asks for a CMAP, it gets the overriding one -- the one attached to the FORM element. When it asks for a BMHD, it gets the default one specified for this LIST. When the context nodes get popped as result of continuing to parse, or of the user program calling CloseIFF, the stored properties will be purged as if they were never even there.