I decided to take a look into the NetWare 386 filesystem, which was used in NetWare 3.x and 4.x and perhaps later versions as well. This post serves to give a high-level background on the design and layout. Tools to analyze and extract content from such a filesystem can be found at https://github.com/zhmu/nwfs386.
If you have any corrections, additional information or questions, please reach out to me by email at firstname.lastname@example.org. I’ll try to update this post as necessary. I also accept pull requests on Github!
Even though NWFS386 was introduced in the late 80-ies, it is more than a filesystem: it performs bad sector remapping, mirroring and divides the partition into volumes, which can span multiple partitions. This is indeed no ordinary filesystem. A brief overview:
- A physical disk can have a single NetWare partition
- A NetWare partition can be mirrored (similar to RAID-1) and performs its own bad sector remapping (redirection)
- A NetWare volume contains files/directories and is allocated to one or more partitions
Hence, a volume can span multiple partitions, which may can be mirrored as desired for added reliability. This is indeed much more than just a filesystem!
In-depth feature list
- NWFS386 uses 32-bit block numbers and 32-bit file/directory identifiers
- Important NWFS386 partition structures are stored 4 times
- A volume has its own block size, which can be 4KB, 8KB, 16KB, 32KB or 64KB in size.
- Volumes can span multiple partitions, and volumes may be extended at any time
- All meta-data is read in memory when mounting
- Volume meta-data (FAT chain, directory contents) are stored twice
- File data is stored a single time
NetWare 386 uses the MBR to locate partitions. There can be a single partition per device, and this partition must have an ID of 0x65.
- The first 16KB of the NetWare partition is not in use
- The next 16KB contains the hotfix/mirror area. These detail the number of redirection sectors and data sectors available, and how many sectors are allocated to remap bad sectors (the redirection area). This information is replicated 4 times.
- The redirection area hasn’t been thoroughly explored; the idea is that bad sectors are remapped here by the NetWare OS.
- The volume area contains which volumes are stored on this partition. There can be multiple volumes stored on a single partition, and each volume can span multiple partitions. This area contains 16KB of data which is replicated 4 times.
- Finally, the data area stores the FAT chain and data blocks for the files/directories on the volume.
We’ll go in depth on these areas in the next sections.
Being the first structure in the partition, it describes how many sectors of the partition are allocated for block data as well as redirection purposes.
|id||byte||Identification string (HOTFIX00)|
|data_area_sectors||u32||Number of sectors for data|
|redir_area_sectors||u32||Number of sectors for redirection|
Our main field of interest is redir_area_sectors which contains the number of sectors used for the hotfix/mirror area and the redirection area. In other words, it allows to calculate where the volume area starts within the partition.
The next sector contains the mirror header as illustrated below.
The second structure in the partition, this describes whether the partition is mirrored – and if so, with with other partitions.
|id||byte||Identification string (MIRROR00)|
|v_id1||u32||Hotfix area ID #1|
|v_id2||u32||Hotfix area ID #2|
v_idN contain the hotfix header v_id values of all partitions that appear within the mirror. Hence, the v_id of the partition’s hotfix header should always be appear in any of the v_idN fields.
Note: there are likely be more than 2 area ID’s allowed, but I haven’t looked into this due to my inability to add SCSI disks to my qemu instance (the dc390 driver seems the only supported one in NetWare 3.x and I can’t get it to work) as I ran out of IDE devices. Any help would be appreciated.
The volume area is located directly past the redirection area; the offset is 16384 + redir_area_sectors * 512 bytes within the partition.
|magic||byte||“NetWare Volumes” + \0|
|num_volumes||u32||Number of volume entries|
This header is followed by num_volumes times a volume header, which is detailed below. Regardless of the number of volumes, 16KB will be used to store the volume information. Furthermore, the volume information is replicated 4 times, which means 64KB is used for the volume area.
|name_length||u8||Volume name length, in bytes|
|segment_num||u16||Volume segment number|
|first_sector||u32||Always 160 (?)|
|num_sectors||u32||Number of sectors in this segment|
|total_blocks||u32||Total number of blocks in this volume|
|first_segment_block||u32||First data block this segment contains|
|block_value||u32||Used to calculate the block size|
|rootdir_block_nr||u32||Block number containing the directory|
|rootdir_copy_block_nr||u32||Block number containing the directory copy|
The volume’s block size is (256 / block_value) * 1024. The smallest block size is 1KB, whereas the largest would be 256KB. The installer does not allow you to create such large blocks, and I haven’t tried if they work at all.
If your volume spans multiple partitions, all partitions will have the volume listed in their volume area. However, the first segment will have first_segment_block = 0, whereas the second segment contains a non-zero value. Thus, all blocks within the segment are relative to first_segment_block, and given a block number you must determine which partition must be accessed.
The NetWare 386 filesystem was clearly inspired by the FAT file system, as it also uses a singly linked list to be able to find the next block of each file. Like FAT, this linked list is stored twice. As the entire FAT is read into memory at mount time and updated on disk as necessary, this is quite speedy.
The root directory is not fixed-length or fixed-size: the rootdir_block_nr and rootdir_copy_block_nr fields of the volume determine where the initial block of the root directory is – the FAT chain can then be used to determine the subsequent blocks as needed. This is similar to the approach taken in FAT32, except for the extra copy.
An important difference is that the root directory is the only directory stored in NWFS386. Every directory entry contains the ID of the directory in which the entry resides. There are a few magic values with their own respective entry content, such as volume information and additional trustee lists. The root directory uses ID 0.
Every FAT entry is completely incompatible with DOS FAT. Every entry is 128 bytes, which ensures it will never span across multiple sectors.
|parent_dir_id||u32||Directory ID where the item resides|
|attr||u32||Entry attributes (must not have directory bit set)|
|name_len||byte||File name length, in bytes|
|name||byte||8.3 file name|
|create_time||u32||File creation time|
|owner_id||bu32||Object ID of the current file owner|
|modify_time||u32||File last modification time|
|modifier_id||bu32||Object ID of the last file modifier|
|length||u32||File length, in bytes|
|block_nr||u32||First file block number|
|delete_time||u32||When was file deleted, zero if not deleted|
|delete_id||bu32||Object ID of who deleted the file|
|parent_dir_id||u32||Directory ID where the item resides|
|attr||u32||Entry attributes (must have directory bit set)|
|name_len||u8||File name length, in bytes|
|name||u8||8.3 file name|
|create_time||u32||Directory creation time|
|owner_id||bu32||Directory owner object ID|
|modify_time||u32||Directory last modification time|
|inherited_rights_mask||u16||Mask for inherited rights|
|directory_id||u32||ID of this directory|
Available entries contain a parent_dir_id of 0xffff ffff. The other 124 bytes tend to be zeros.
Whenever more trustees are added to a file/directory which cannot be stored in the corresponding FAT entry itself, a grant list will be added to the volume with the responding information. This hasn’t been decoded in too much detail.
|create_time||u32||Volume creation time|
|owner_id||u32||Object ID of volume owner|
|modify_time||u32||Last modification time|
|object_id||bu32||Object ID where the trustee applies to|
|rights||u16||Bitmask containing trustee rights|
The rights mask contains the following bits:
|2||Seems to be used internally?|
|3||C||Access to create subentries|
|8||S||Supervisory (overrides all others)|
|0||Ro (Rw if clear)||Read-only|
A timestamp is a 32-bit value, which is to be interpreted as two 16-bit values: the high part is the date and the low part is the time.
|0:4||Second divided by 2|
|Date||9:15||Year minus 1980|