Dev Docs Virtual Icon
Virtual Icon — Custom Volume Icons Over AFP
Synthesizing Icon\r resource forks in memory so Classic Mac OS clients display custom volume icons without any physical file on the host filesystem. This feature was 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.
Source Files
| File | Purpose |
|---|---|
| etc/afpd/virtual_icon.c | Resource fork construction, public API |
| etc/afpd/virtual_icon.h | Constants (VIRTUAL_ICON_NAME, VIRTUAL_ICON_CNID), function prototypes |
| etc/afpd/icon.c | Icon pixel data arrays (ICN#, icl4, icl8 for each icon) |
| etc/afpd/icon.h | Icon size constants, resource type codes, extern declarations |
| etc/afpd/fork.c | materialize_virtual_icon(), virtual fork handling in afp_openfork(), afp_read(), afp_write() |
| etc/afpd/fork.h | AFPFORK_VIRTUAL flag, struct ofork with virtual data fields |
| etc/afpd/ofork.c | Virtual fork close handling in of_closefork() |
| etc/afpd/filedir.c | Virtual icon interception in FPGetFilDirParms, FPDelete, FPRename, FPMoveAndRename, FPSetFilDirParms |
| etc/afpd/enumerate.c | Appending virtual Icon\r entry during enumeration |
| etc/afpd/directory.c | Setting FINDERINFO_HASCUSTOMICON on volume root, incrementing offspring count |
| etc/afpd/volume.c | Calling virtual_icon_init() during volume mount |
| etc/afpd/status.c | Server status block with ICN# icon (independent of virtual icon feature) |
Implementation
Initialization
When a volume is mounted, virtual_icon_init() in etc/afpd/virtual_icon.c checks whether the legacy icon option is set. If so, it selects the matching icon data arrays from etc/afpd/icon.c and calls build_resource_fork() to construct the complete resource fork binary in a heap-allocated buffer stored in vol->v_icon_rfork.
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
flowchart TD
A["afp.conf parsed<br/><i>netatalk_conf.c</i>"]:::yellow
A -->|"legacy icon = globe"| B["vol→v_legacyicon = 'globe'"]:::yellow
B --> C["Volume mount<br/><i>volume.c</i>"]:::blue
C --> D["virtual_icon_init(vol)<br/><i>virtual_icon.c</i>"]:::blue
D --> E{"v_legacyicon set?"}:::grey
E -->|No| F["Feature disabled<br/>v_icon_rfork = NULL"]:::grey
E -->|Yes| G["Select icon data arrays<br/>(ICN#, icl4, icl8)<br/><i>icon.h / icon.c</i>"]:::purple
G --> H["build_resource_fork()<br/><i>virtual_icon.c</i>"]:::purple
H --> I["Allocate buffer<br/>256 + data_len + map_len bytes"]:::cyan
I --> J["Write resource header<br/>offsets and lengths"]:::orange
J --> K["Write data section<br/>ICN# 256B + icl4 512B + icl8 1024B"]:::orange
K --> L["Write resource map<br/>type list + reference list"]:::orange
L --> M["vol→v_icon_rfork = buffer<br/>vol→v_icon_rfork_len = size"]:::green
classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
classDef grey fill:#dfe6e9,stroke:#333,rx:10,ry:10
classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10
classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10
Available Icon Themes
The virtual_icon_init() function maps the legacy icon config value to icon data arrays. Each icon provides three formats (ICN#, icl4, icl8):
| Config Value | ICN# Array | icl4 Array | icl8 Array |
|---|---|---|---|
daemon |
daemon_icon |
daemon_icon_icl4 |
daemon_icon_icl8 |
declogo |
declogo_icon |
declogo_icon_icl4 |
declogo_icon_icl8 |
fileserver |
fileserver_icon |
fileserver_icon_icl4 |
fileserver_icon_icl8 |
globe |
globe_icon |
globe_icon_icl4 |
globe_icon_icl8 |
hagar |
hagar_icon |
hagar_icon_icl4 |
hagar_icon_icl8 |
nas |
nas_icon |
nas_icon_icl4 |
nas_icon_icl8 |
sdcard |
sdcard_icon |
sdcard_icon_icl4 |
sdcard_icon_icl8 |
sunlogo |
sunlogo_icon |
sunlogo_icon_icl4 |
sunlogo_icon_icl8 |
All arrays are declared in etc/afpd/icon.h and defined in etc/afpd/icon.c.
Key Constants
Defined in etc/afpd/virtual_icon.h and etc/afpd/icon.h:
| Constant | Value | Description |
|---|---|---|
VIRTUAL_ICON_NAME |
"Icon\x0D" |
The Mac filename: “Icon” + carriage return |
VIRTUAL_ICON_CNID |
16 | Reserved CNID below CNID_START (17) |
ICN_HASH_SIZE |
256 | ICN# resource size (icon + mask) |
ICL4_SIZE |
512 | icl4 resource size (32×32 4-bit) |
ICL8_SIZE |
1024 | icl8 resource size (32×32 8-bit) |
RESTYPE_ICNH |
0x49434E23 | Resource type code for ‘ICN#’ |
RESTYPE_ICL4 |
0x69636C34 | Resource type code for ‘icl4’ |
RESTYPE_ICL8 |
0x69636C38 | Resource type code for ‘icl8’ |
ICON_RES_ID |
−16455 | Finder volume custom icon resource ID (0xBFB9) |
RFORK_HEADER_SIZE |
16 | Size of resource fork header (4 uint32 fields) |
Public API Functions
Defined in etc/afpd/virtual_icon.c:
| Function | Description |
|---|---|
virtual_icon_init() |
Initialize the virtual icon for a volume; builds the resource fork buffer |
virtual_icon_enabled() |
Returns true if vol->v_icon_rfork is non-NULL |
real_icon_exists() |
Returns true if a physical Icon\r file exists at the volume root (via lstat()) |
is_virtual_icon_name() |
Returns true if the name matches VIRTUAL_ICON_NAME |
virtual_icon_get_rfork() |
Returns a pointer to the pre-built resource fork buffer and its length |
virtual_icon_getfilparams() |
Synthesizes AFP file parameters for the virtual file, matching the requested bitmap |
Icon Data
Icon Resource Formats
Each icon is 32×32 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 32×32 packed 1-bit, MSB first, 4 bytes per row |
icl4 |
4 | 512 | 32×32 pixels, 4 bits each (2 pixels per byte, high nibble first), indexed into the standard Mac 4-bit CLUT |
icl8 |
8 | 1024 | 32×32 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.
Resource Fork Binary Layout
The build_resource_fork() function in etc/afpd/virtual_icon.c constructs the classic Mac resource fork format:
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
flowchart LR
subgraph "Resource Header — 256 bytes (offset 0)"
RH["data_offset=256 | map_offset=256+data_len<br/>data_len=1804 | map_len=90<br/>+ 240 bytes reserved (zero)"]:::yellow
end
subgraph "Resource Data — 1804 bytes (offset 256)"
RD["[4B len=256] ICN# 256B<br/>[4B len=512] icl4 512B<br/>[4B len=1024] icl8 1024B"]:::blue
end
subgraph "Resource Map — 90 bytes (offset 2060)"
RM["Header copy (28B) | Type list: 3 types (26B)<br/>Reference list: 3 entries, ID −16455 (36B)"]:::purple
end
RH --> RD --> RM
classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
Resource header (256 bytes at offset 0):
- Bytes 0–3: data_offset = 256 (offset to resource data)
- Bytes 4–7: map_offset = 256 + data_len (offset to resource map)
- Bytes 8–11: data_len = 1804 (total data section length)
- Bytes 12–15: map_len = 90 (total map section length)
- Bytes 16–255: reserved (zero-filled)
Resource data section (1804 bytes at offset 256):
- Each resource is prefixed by a 4-byte big-endian length, followed by the raw data
- ICN#: 4 + 256 = 260 bytes
- icl4: 4 + 512 = 516 bytes
- icl8: 4 + 1024 = 1028 bytes
Resource map (90 bytes at offset 2060): - 22 bytes: copy of header fields + reserved handle + file ref - 2 bytes: attributes (0) - 2 bytes: type list offset (28, from map start) - 2 bytes: name list offset (90, past all entries — no names used) - Type list: 2 bytes (num_types − 1 = 2) + 3 × 8 bytes (type code + count + ref offset) - Reference list: 3 × 12 bytes (resource ID + name offset 0xFFFF + attrs + data offset + handle)
All three resources use resource ID −16455 (0xBFB9), which is the Finder’s volume custom icon resource ID.
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, arranged in a 6×6×6 color cube (indices 0–214) plus a grayscale ramp and system colors. Key entries: 0x00 = White, 0xFF = Black, 0xD4 = medium blue. A full table can be found in Inside Macintosh: Imaging With QuickDraw, Plate 2.
Integration Points
AFP Operation Interception
The virtual icon hooks into multiple AFP command paths. Each check follows the same guard pattern: the request targets the volume root (DIRDID_ROOT), the name matches VIRTUAL_ICON_NAME, the feature is enabled (virtual_icon_enabled(vol)), and no real Icon\r file exists (!real_icon_exists(vol)).
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
flowchart TD
REQ["AFP Request from Client"]:::grey
REQ --> CMD{"AFP Command?"}:::grey
CMD -->|FPEnumerate| ENUM["enumerate.c"]:::blue
ENUM --> ENUMCHK{"At volume root?<br/>Icon enabled?<br/>No real Icon\\r?<br/>Past last real entry?"}:::grey
ENUMCHK -->|Yes| ENUMADD["Append virtual Icon\\r entry<br/>via virtual_icon_getfilparams()<br/>Increment offspring count"]:::green
ENUMCHK -->|No| ENUMREAL["Normal enumeration only"]:::grey
CMD -->|FPGetFilDirParms| GFDP["filedir.c"]:::blue
GFDP --> GFDPCHK{"CNID = VIRTUAL_ICON_CNID<br/>or name = Icon\\r<br/>at volume root?"}:::grey
GFDPCHK -->|Yes| GFDPVIRT["Return synthesized params<br/>virtual_icon_getfilparams()"]:::green
GFDPCHK -->|No| GFDPREAL["Normal file/dir params"]:::grey
CMD -->|FPOpenFork| OPEN["fork.c"]:::blue
OPEN --> OPENCHK{"Name = Icon\\r?<br/>At volume root?<br/>Icon enabled?"}:::grey
OPENCHK -->|No| OPENREAL["Normal open"]:::grey
OPENCHK -->|Yes| OPENWR{"Write access<br/>requested?"}:::grey
OPENWR -->|Yes| MATERIALIZE["materialize_virtual_icon()<br/>Create real file, then normal open"]:::salmon
OPENWR -->|No| OPENVIRT["Allocate virtual ofork<br/>flags |= AFPFORK_VIRTUAL<br/>Store rfork pointer"]:::green
CMD -->|FPRead| READ["fork.c"]:::blue
READ --> READCHK{"AFPFORK_VIRTUAL<br/>flag set?"}:::grey
READCHK -->|No| READREAL["Normal read from disk"]:::grey
READCHK -->|Yes| READFORK{"Which fork?"}:::grey
READFORK -->|Data fork| READEOF["Return AFPERR_EOF<br/>(empty data fork)"]:::yellow
READFORK -->|Resource fork| READBUF["Serve from in-memory buffer<br/>via dsi_readinit()"]:::green
CMD -->|FPCloseFork| CLOSE["ofork.c"]:::blue
CLOSE --> CLOSECHK{"AFPFORK_VIRTUAL?"}:::grey
CLOSECHK -->|No| CLOSEREAL["Normal close<br/>ad_close / flush"]:::grey
CLOSECHK -->|Yes| CLOSEVIRT["of_dealloc(ofork)<br/>Skip flush/close"]:::green
CMD -->|"FPWrite /<br/>FPSetForkParms"| REJECT1["fork.c"]:::blue
REJECT1 --> REJCHK1{"AFPFORK_VIRTUAL?"}:::grey
REJCHK1 -->|Yes| MATWR{"Write access?"}:::grey
MATWR -->|Yes| MATERIALIZE2["materialize_virtual_icon()"]:::salmon
MATWR -->|No| OLOCK1["Return AFPERR_OLOCK"]:::red
REJCHK1 -->|No| NORMAL1["Normal write path"]:::grey
CMD -->|"FPDelete / FPRename /<br/>FPMoveAndRename /<br/>FPSetFilDirParms"| REJECT2["filedir.c"]:::blue
REJECT2 --> REJCHK2{"Icon\\r at root?<br/>Icon enabled?<br/>No real Icon\\r?"}:::grey
REJCHK2 -->|Yes| OLOCK2["Return AFPERR_OLOCK"]:::red
REJCHK2 -->|No| NORMAL2["Normal operation"]:::grey
classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
classDef grey fill:#dfe6e9,stroke:#333,rx:10,ry:10
classDef salmon fill:#fab1a0,stroke:#333,rx:10,ry:10
classDef red fill:#ee5a5a,stroke:#333,rx:10,ry:10
Interception Summary
| AFP Command | Source File | Virtual Behavior |
|---|---|---|
| FPEnumerate | etc/afpd/enumerate.c | Appends one extra entry for Icon\r after normal directory enumeration at volume root; offspring count incremented by 1 |
| FPGetFilDirParms | etc/afpd/filedir.c | When cname() returns ENOENT for Icon\r at volume root, or CNID matches VIRTUAL_ICON_CNID (16), returns synthesized file parameters via virtual_icon_getfilparams() |
| FPOpenFork | etc/afpd/fork.c | Read-only: allocates a virtual ofork with AFPFORK_VIRTUAL flag, pointing to the pre-built buffer. Write access: calls materialize_virtual_icon() |
| FPRead | etc/afpd/fork.c | Virtual forks serve data from of_virtual_data via dsi_readinit(). Data fork reads return AFPERR_EOF immediately |
| FPCloseFork | etc/afpd/ofork.c | Virtual forks skip ad_close/flush and simply call of_dealloc() |
| FPWrite / FPSetForkParms | etc/afpd/fork.c | With write access: calls materialize_virtual_icon(). Without: returns AFPERR_OLOCK |
| FPDelete / FPRename / FPMoveAndRename / FPSetFilDirParms | etc/afpd/filedir.c | Returns AFPERR_OLOCK when targeting Icon\r at volume root |
| FPGetSrvrInfo | etc/afpd/status.c | Unchanged — the ICN# icon continues to be embedded in the server status block via status_icon() independently of the virtual icon feature |
Volume Root Directory
The getdirparams() function in etc/afpd/directory.c modifies two things when the virtual icon is active:
-
Finder Info flags: Sets
FINDERINFO_HASCUSTOMICON(0x0400) in the Finder info flags forDIRDID_ROOT. This tells the Finder to look for theIcon\rfile. -
Offspring count: Increments the directory offspring count by 1 to account for the virtual
Icon\rentry.
Both modifications are conditional on: dir->d_did == DIRDID_ROOT && virtual_icon_enabled(vol) && !real_icon_exists(vol).
Virtual CNID
The virtual file uses CNID 16 (VIRTUAL_ICON_CNID), 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, as synthesized by virtual_icon_getfilparams(), reports:
| Attribute | Value | Description |
|---|---|---|
| Finder flags | ATTRBIT_INVISIBLE |
Hidden from normal directory listings |
| Finder info flags | FINDERINFO_INVISIBLE |
Set at FINDERINFO_FRFLAGOFF |
| Data fork length | 0 | Empty data fork |
| Resource fork length | ~2150 bytes | The pre-built resource fork buffer |
| Unix permissions | S_IFREG \| 0444 |
Read-only regular file |
| Parent DID | DIRDID_ROOT (2) |
Always at volume root |
| CNID | 16 | VIRTUAL_ICON_CNID |
The ofork Structure
The struct ofork in etc/afpd/fork.h includes virtual icon support fields:
| Field | Type | Purpose |
|---|---|---|
of_flags |
int |
Bitfield; AFPFORK_VIRTUAL (1 << 8) indicates a virtual fork |
of_virtual_data |
const unsigned char * |
Pointer to the pre-built resource fork buffer |
of_virtual_len |
off_t |
Length of the virtual data |
The AFPFORK_VIRTUAL flag (defined at value 1 << 8 in etc/afpd/fork.h) is checked throughout the fork operation paths to bypass normal file I/O.
Virtual Fork Lifecycle
%%{ init: { 'themeVariables': { 'fontSize': '14px' } } }%%
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=2150)
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
Materialization
When a client opens the virtual Icon\r file with write access, the server materializes it into a real file on disk. This is handled by materialize_virtual_icon() in etc/afpd/fork.c.
Materialization Flow
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
flowchart TD
A["FPOpenFork with write access<br/><i>fork.c</i>"]:::blue
A --> B["Create real 'Icon\\r' file on disk<br/>open(path, O_CREAT | O_WRONLY)"]:::orange
B --> C["Seed resource fork<br/>ad_write(ADEID_RFORK) with virtual data"]:::orange
C --> D["Set FinderInfo<br/>FINDERINFO_INVISIBLE"]:::purple
D --> E["ad_flush() + ad_close()"]:::purple
E --> F["Re-init adouble for real I/O<br/>ad_open(path, ADFLAGS_SETSHRMD | ADFLAGS_RDWR)"]:::blue
F --> G["Stat new file<br/>Update ofork key (dev, inode)"]:::cyan
G --> H["Clear AFPFORK_VIRTUAL flag<br/>of_virtual_data = NULL"]:::green
H --> I["real_icon_exists() → true<br/>All subsequent AFP ops<br/>use normal file paths"]:::green
classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
classDef grey fill:#dfe6e9,stroke:#333,rx:10,ry:10
classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10
classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10
Materialization Steps
- Create file:
open()creates the physicalIcon\rat the volume root - Seed resource fork:
ad_write()copies the virtual icon data into the real file’s resource fork via adouble - Set FinderInfo: Writes
FINDERINFO_INVISIBLEflag to the adouble metadata - Flush and close:
ad_flush()+ad_close()ensures all metadata is written - Re-open for real I/O: Reinitializes the
adoublestructure and re-opens the fork withADFLAGS_SETSHRMD | ADFLAGS_RDWR - Update ofork: Stats the new file to update dev/inode in the ofork key
- Transition: Clears
AFPFORK_VIRTUAL, setsof_virtual_data = NULL - Bypass disabled: Since
real_icon_exists()now returns true, all subsequent AFP operations use normal code paths
Volume Root Finder Info
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
flowchart LR
A["FPGetFilDirParms<br/>for volume root<br/><i>directory.c</i>"]:::blue
A --> B{"d_did == DIRDID_ROOT<br/>AND virtual_icon_enabled(vol)<br/>AND !real_icon_exists(vol)?"}:::grey
B -->|Yes| C["Read existing Finder flags"]:::purple
C --> D["flags |= FINDERINFO_HASCUSTOMICON<br/>(0x0400)"]:::green
D --> E["Write modified flags<br/>back to response"]:::green
D --> F["Increment offspring count"]:::green
B -->|No| G["Return unmodified<br/>Finder info"]:::grey
E --> H["Finder sees hasCustomIcon bit<br/>→ looks for Icon\\r file"]:::yellow
classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
classDef grey fill:#dfe6e9,stroke:#333,rx:10,ry:10
Configuration
Enabling Virtual Icons
The legacy icon option in afp.conf enables the feature on a per-volume basis:
[MyVolume]
path = /path/to/volume
legacy icon = globe
Set the value to one of the available icon themes: daemon, declogo, fileserver, globe, hagar, nas, sdcard, sunlogo.
To disable (default), either omit the option or set it to an empty string.
Custom Icons
To use a custom icon instead of the built-in themes:
- Create a 32×32 pixel icon in the three required formats (ICN#, icl4, icl8)
- Add the icon data arrays to etc/afpd/icon.c
- Declare the arrays in etc/afpd/icon.h
- Add the new theme name to the
if/elsechain invirtual_icon_init()in etc/afpd/virtual_icon.c
The contrib/scripts/icn_hex_to_c.pl Perl script in contrib/scripts/ can assist with converting hex icon data to C array format.
Converting From Modern Formats
To convert a 32×32 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.
Interaction With Real Icons
If a real Icon\r file exists on disk at the volume root, the virtual icon feature is automatically bypassed. All checks include the !real_icon_exists(vol) guard, which calls lstat() on the path vol->v_path/Icon\r.
Once materialized (via write-open), the real_icon_exists() check returns true, and the virtual interception is permanently disabled for that volume (until the real file is deleted).
Footnotes
This is a mirror of the Netatalk GitHub Wiki
Last updated 2026-04-25