Dev Docs Virtual Icon
Virtual Icon — Custom Volume Icons Over AFP
Serving a synthetic Icon resource to Classic Mac OS clients – this feature is being introduced in Netatalk 4.5.0
Overview
When a classic Macintosh connects to an AFP file share, the Finder looks for
a specially named file at the root of each volume called Icon\r (the four
letters “Icon” followed by a carriage return character, 0x0D). If this file
exists and the volume root directory has the hasCustomIcon bit set in its
Finder info, the Finder uses the icon resources inside the file to display a
custom volume icon instead of the generic shared folder.
Netatalk synthesizes a virtual Icon\r file entirely in memory.
No file is written to the host filesystem. The server intercepts the relevant
AFP operations and serves the icon data on the fly.
This feature is disabled by default and can be enabled on a per-volume basis
with the legacy icon option in afp.conf.
How It Works
Resource Fork Binary Format
Offset 0: 256-byte resource header
[4] data_offset = 256
[4] map_offset = 256 + data_len
[4] data_len = 1804
[4] map_len = 78
[240] reserved (zero)
Offset 256: Resource data section
[4] ICN# length (256)
[256] ICN# data (32x32 1-bit icon + 32x32 mask)
[4] icl4 length (512)
[512] icl4 data (32x32 4-bit color)
[4] icl8 length (1024)
[1024] icl8 data (32x32 8-bit color)
Offset 2060: Resource map
[16] copy of header fields
[4] next_map_handle (0)
[2] file_ref (0)
[2] attributes (0)
[2] type_list_offset (28, from map start)
[2] name_list_offset (90, from map start)
Type list:
[2] num_types - 1 (2, meaning 3 types)
3x [4 type + 2 count-1 + 2 ref_offset] = 24 bytes
Reference list:
3x [2 id + 2 name_off + 1 attrs + 3 data_off + 4 handle] = 36 bytes
AFP Operation Interception
| AFP Command | Behavior |
|---|---|
| FPEnumerate | After normal directory enumeration at volume root, one extra entry is appended for Icon\r. Offspring count is incremented by 1. |
| FPGetFileDirParms | When cname() returns ENOENT for Icon\r at volume root, synthesized file parameters are returned (invisible file, empty data fork, resource fork with known length). |
| FPOpenFork | When cname() returns ENOENT for Icon\r at volume root, a virtual ofork is allocated with the AFPFORK_VIRTUAL flag. The fork points to the pre-built resource fork buffer. Write access is rejected with AFPERR_OLOCK. |
| FPRead | For virtual forks, data is served directly from the in-memory buffer via dsi_readinit(). Data fork reads return EOF immediately. |
| FPCloseFork | Virtual forks skip all ad_close/flush logic and simply deallocate the ofork. |
| FPWrite / FPSetForkParms | Rejected with AFPERR_OLOCK. |
| FPDelete / FPRename / FPMoveAndRename / FPSetFilDirParms | Rejected with AFPERR_OLOCK when targeting Icon\r at volume root. |
| FPGetSrvrInfo | Unchanged. The ICN# icon continues to be embedded in the server status block by status.c. |
Volume Root Directory
getdirparams() in directory.c sets the FINDERINFO_HASCUSTOMICON
(0x0400) bit in the Finder info flags for DIRDID_ROOT when the feature is
enabled. This tells the Finder to look for the Icon\r file.
Virtual CNID
The virtual file uses CNID 16, which is below CNID_START (17) and above the
special directory IDs (DIRDID_ROOT_PARENT = 1, DIRDID_ROOT = 2). CNIDs
3-16 are unassigned and never used by the CNID database, making 16 safe
from collisions.
File Attributes
The virtual Icon\r file reports:
- Finder flags:
ATTRBIT_INVISIBLE(hidden from normal directory listings) - Data fork length: 0 (empty)
- Resource fork length: ~2150 bytes (the pre-built resource fork)
- Unix permissions: 0444 (read-only regular file)
- Parent DID:
DIRDID_ROOT(2)
Icon Resource Formats
Each icon is 32x32 pixels. The three resource types store different color depths:
| Resource | Bits/pixel | Size (bytes) | Format |
|---|---|---|---|
ICN# |
1 | 256 | 128 bytes icon bitmap + 128 bytes mask, both 32x32 packed 1-bit, MSB first, 4 bytes per row |
icl4 |
4 | 512 | 32x32 pixels, 4 bits each (2 pixels per byte, high nibble first), indexed into the standard Mac 4-bit CLUT |
icl8 |
8 | 1024 | 32x32 pixels, 8 bits each (1 pixel per byte), indexed into the standard Mac 8-bit CLUT |
All formats are raw pixel data with no header — the dimensions are implied by the resource type.
Standard Macintosh Color Lookup Tables
The 4-bit and 8-bit icons use indexed color with fixed system CLUTs:
4-bit CLUT (16 colors):
| Index | Color |
|---|---|
| 0x00 | White |
| 0x01 | Yellow |
| 0x02 | Orange |
| 0x03 | Red |
| 0x04 | Magenta |
| 0x05 | Purple |
| 0x06 | Blue |
| 0x07 | Cyan |
| 0x08 | Green |
| 0x09 | Dark Green |
| 0x0A | Brown |
| 0x0B | Tan |
| 0x0C | Light Gray |
| 0x0D | Medium Gray |
| 0x0E | Dark Gray |
| 0x0F | Black |
8-bit CLUT (256 colors): The standard Macintosh 8-bit system palette. A full table can be found in Inside Macintosh: Imaging With QuickDraw, Plate 2. Key entries include: 0x00 = White, 0xFF = Black, 0xD4 = medium blue. The palette is arranged in a 6x6x6 color cube (indices 0-214) plus a grayscale ramp and system colors.
Converting From Modern Formats
To convert a 32x32 PNG to the Mac icon formats:
-
ICN#: Threshold the image to 1-bit (black/white), pack into 128 bytes. Generate the mask similarly (typically the icon silhouette). Concatenate icon + mask = 256 bytes.
-
icl4: For each pixel, find the nearest color in the 4-bit CLUT. Pack two pixels per byte (high nibble = left pixel). Total: 512 bytes.
-
icl8: For each pixel, find the nearest color in the 8-bit CLUT. One pixel per byte. Total: 1024 bytes.
Virtual Icon Diagrams
1. Initialization Flow
flowchart TD
A["afp.conf parsed<br><i>netatalk_conf.c:1150</i>"] -->|"legacy icon = globe"| B["vol->v_legacyicon = 'globe'"]
B --> C["Volume mount<br><i>volume.c:950</i>"]
C --> D["virtual_icon_init(vol)<br><i>virtual_icon.c:238</i>"]
D --> E{"v_legacyicon set?"}
E -->|No| F["Feature disabled<br>v_icon_rfork = NULL"]
E -->|Yes| G["Select icon data arrays<br>(ICN#, icl4, icl8)<br><i>icon.h / icon.c</i>"]
G --> H["build_resource_fork()<br><i>virtual_icon.c:78</i>"]
H --> I["Allocate buffer<br>256 + 1804 + 78 bytes"]
I --> J["Write resource header<br>offsets & lengths"]
J --> K["Write data section<br>ICN# 256B + icl4 512B + icl8 1024B"]
K --> L["Write resource map<br>type list + reference list"]
L --> M["vol->v_icon_rfork = buffer<br>vol->v_icon_rfork_len = size"]
2. AFP Operation Routing
flowchart TD
REQ["AFP Request from Client"] --> CMD{"AFP Command?"}
CMD -->|FPEnumerate| ENUM["enumerate.c:570"]
ENUM --> ENUMCHK{"At volume root?<br>Icon enabled?<br>No real Icon\r?<br>Past last real entry?"}
ENUMCHK -->|Yes| ENUMADD["Append virtual Icon\r entry<br>via virtual_icon_getfilparams()<br>Increment offspring count"]
ENUMCHK -->|No| ENUMREAL["Normal enumeration only"]
CMD -->|FPGetFilDirParms| GFDP["filedir.c:99"]
GFDP --> GFDPCHK{"cname() → ENOENT?<br>Name = Icon\r?<br>At volume root?<br>Icon enabled?"}
GFDPCHK -->|Yes| GFDPVIRT["Return synthesized params<br>virtual_icon_getfilparams()"]
GFDPCHK -->|No| GFDPREAL["Normal file/dir params"]
CMD -->|FPOpenFork| OPEN["fork.c:356"]
OPEN --> OPENCHK{"Name = Icon\r?<br>At volume root?<br>Icon enabled?"}
OPENCHK -->|No| OPENREAL["Normal open"]
OPENCHK -->|Yes| OPENWR{"Write access<br>requested?"}
OPENWR -->|Yes| MATERIALIZE["Materialize real Icon\r<br>to disk, then normal open"]
OPENWR -->|No| OPENVIRT["Allocate virtual ofork<br>flags |= AFPFORK_VIRTUAL<br>Store rfork pointer"]
CMD -->|FPRead| READ["fork.c:1141"]
READ --> READCHK{"AFPFORK_VIRTUAL<br>flag set?"}
READCHK -->|No| READREAL["Normal read from disk"]
READCHK -->|Yes| READFORK{"Which fork?"}
READFORK -->|Data fork| READEOF["Return AFPERR_EOF<br>(empty data fork)"]
READFORK -->|Resource fork| READBUF["Serve from in-memory buffer<br>via dsi_readinit()"]
CMD -->|FPCloseFork| CLOSE["ofork.c:459"]
CLOSE --> CLOSECHK{"AFPFORK_VIRTUAL?"}
CLOSECHK -->|No| CLOSEREAL["Normal close<br>ad_close / flush"]
CLOSECHK -->|Yes| CLOSEVIRT["of_dealloc(ofork)<br>Skip flush/close"]
CMD -->|"FPWrite /<br>FPSetForkParms"| REJECT1["fork.c:1610 / 783"]
REJECT1 --> REJCHK1{"AFPFORK_VIRTUAL?"}
REJCHK1 -->|Yes| OLOCK1["Return AFPERR_OLOCK"]
REJCHK1 -->|No| NORMAL1["Normal write path"]
CMD -->|"FPDelete / FPRename /<br>FPMoveAndRename /<br>FPSetFilDirParms"| REJECT2["filedir.c"]
REJECT2 --> REJCHK2{"Icon\r at root?<br>Icon enabled?<br>No real Icon\r?"}
REJCHK2 -->|Yes| OLOCK2["Return AFPERR_OLOCK"]
REJCHK2 -->|No| NORMAL2["Normal operation"]
3. Resource Fork Binary Layout
This describes the layout of Icon\r resource fork as of Mac OS 7.6 – from Mac OS 8.5 onward the layout changed drastically, using an icns container for the icon resources.
Resource Header (256 bytes) — Offset 0
block-beta
columns 5
A["data_offset<br>= 256<br>(4B)"]
B["map_offset<br>= 2060<br>(4B)"]
C["data_len<br>= 1804<br>(4B)"]
D["map_len<br>= 78<br>(4B)"]
E["reserved<br>(240B zero)"]
Resource Data Section (1804 bytes) — Offset 256
block-beta
columns 6
A["len<br>256<br>(4B)"]
B["ICN# data<br>32×32 1-bit<br>(256B)"]
C["len<br>512<br>(4B)"]
D["icl4 data<br>32×32 4-bit<br>(512B)"]
E["len<br>1024<br>(4B)"]
F["icl8 data<br>32×32 8-bit<br>(1024B)"]
Resource Map (78 bytes) — Offset 2060
block-beta
columns 3
A["Map Header<br>copy of hdr +<br>attrs (28B)"]
B["Type List<br>3 types: ICN# icl4 icl8<br>(26B)"]
C["Reference List<br>3 entries, ID −16455<br>(36B)"]
4. Virtual Fork Lifecycle
sequenceDiagram
participant Client as Mac Finder
participant AFP as afpd
participant Vol as Volume (in-memory)
Note over Vol: virtual_icon_init()<br>builds rfork buffer
Client->>AFP: FPEnumerate (volume root)
AFP->>AFP: Normal enumeration
AFP->>AFP: Append virtual Icon\r entry<br>offspring++
AFP-->>Client: Directory listing with Icon\r
Client->>AFP: FPGetFilDirParms("Icon\r")
AFP->>AFP: cname() → ENOENT
AFP->>AFP: virtual_icon_getfilparams()<br>CNID=16, invisible, rfork len
AFP-->>Client: Synthesized file params
Client->>AFP: FPOpenFork("Icon\r", read, resource)
AFP->>AFP: Allocate ofork<br>flags = AFPFORK_VIRTUAL | ACCRD
AFP->>Vol: Store pointer to v_icon_rfork
AFP-->>Client: Fork refnum
Client->>AFP: FPRead(refnum, offset=0, count=2138)
AFP->>Vol: Read from of_virtual_data
AFP-->>Client: Resource fork bytes
Client->>AFP: FPCloseFork(refnum)
AFP->>AFP: of_dealloc(ofork)<br>Skip ad_close/flush
AFP-->>Client: OK
Note over Client: Finder renders<br>custom volume icon
5. Volume Root Finder Info
flowchart LR
A["FPGetFilDirParms<br>for volume root<br><i>directory.c:2191</i>"] --> B{"dir->d_did == DIRDID_ROOT<br>AND virtual_icon_enabled(vol)<br>AND !real_icon_exists(vol)?"}
B -->|Yes| C["Read existing Finder flags"]
C --> D["flags |= FINDERINFO_HASCUSTOMICON<br>(0x0400)"]
D --> E["Write modified flags<br>back to response"]
D --> F["Increment offspring count<br><i>directory.c:2237</i>"]
B -->|No| G["Return unmodified<br>Finder info"]
E --> H["Finder sees hasCustomIcon bit<br>→ looks for Icon\r file"]
6. Write-Open Materialization Path
flowchart TD
A["FPOpenFork with write access<br><i>fork.c:375</i>"] --> B["Create real 'Icon\\r' file on disk"]
B --> C["Seed resource fork xattr<br>with virtual icon data"]
C --> D["Set FINDERINFO_INVISIBLE<br>via adouble metadata"]
D --> E["Fall through to normal<br>open code path"]
E --> F["File now exists on disk"]
F --> G["real_icon_exists() → true"]
G --> H["All subsequent AFP ops<br>use normal file paths<br>(virtual interception bypassed)"]
Footnotes
This is a mirror of the Netatalk GitHub Wiki
Last updated 2026-03-21