netatalk.io

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:

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:

  1. 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.

  2. 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.

  3. 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