Dev Docs Libatalk API
libatalk API Reference
Overview
The libatalk shared library provides the core APIs used by afpd, cnid_dbd, and CLI utilities. It is built from source modules in libatalk/ with public headers in include/atalk/. The build system at libatalk/meson.build links all sub-libraries into a single libatalk.so.19.
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph LR
style A fill:#74b9ff,rx:10,ry:10
style B fill:#a29bfe,rx:10,ry:10
style C fill:#55efc4,rx:10,ry:10
style D fill:#ffeaa7,rx:10,ry:10
style E fill:#dfe6e9,rx:10,ry:10
A["afpd / cnid_dbd / utilities"] --> B["libatalk.so.19"]
B --> C["adouble · cnid · dsi"]
B --> D["unicode · util · vfs"]
B --> E["acl · compat"]
Key design points:
- Header files in
include/atalk/define the public API surface - Implementation files live under
libatalk/subdirectories - Cache APIs in
etc/afpd/extend libatalk structures with caching layers (not part of libatalk itself, but tightly coupled)
Module Reference
| Subdirectory | Purpose | Key Header |
|---|---|---|
libatalk/adouble/ |
AppleDouble file format handling (metadata, resource forks) | adouble.h |
libatalk/cnid/ |
CNID (Catalog Node ID) database interface with pluggable backends | cnid.h |
libatalk/dsi/ |
DSI (Data Stream Interface) protocol implementation | dsi.h |
libatalk/unicode/ |
Character encoding conversion (UCS-2, UTF-8, Mac charsets) | unicode.h |
libatalk/util/ |
General utilities: IPC, child management, logging, sockets, locking | util.h |
libatalk/vfs/ |
Virtual filesystem layer (permissions, EAs, ACLs) | vfs.h |
libatalk/acl/ |
ACL and UUID mapping (LDAP, local cache) | acl.h |
libatalk/compat/ |
Platform compatibility shims | compat.h |
libatalk/asp/ |
AppleTalk Session Protocol (optional, AppleTalk builds) | — |
libatalk/atp/ |
AppleTalk Transaction Protocol (optional) | — |
libatalk/nbp/ |
Name Binding Protocol (optional) | — |
libatalk/netddp/ |
DDP networking (optional) | — |
libatalk/dalloc/ |
Dynamic allocation for Spotlight (optional) | — |
AppleDouble API
Header: include/atalk/adouble.h
Implementation: libatalk/adouble/
The AppleDouble API handles Mac metadata storage on Unix filesystems. Two storage backends exist:
| Backend | Constant | Storage |
|---|---|---|
| AppleDouble v2 | AD_VERSION2 (0x00020000) |
._ sidecar files in .AppleDouble/ directories |
| EA-based | AD_VERSION_EA (0x00020002) |
Native extended attributes (org.netatalk.Metadata, org.netatalk.ResourceFork) |
Core Structures
/* adouble.h — struct ad_entry */
struct ad_entry {
off_t ade_off;
ssize_t ade_len;
};
struct ad_fd {
int adf_fd; /* -1: invalid, AD_SYMLINK: symlink */
char *adf_syml;
int adf_flags;
adf_lock_t *adf_lock;
int adf_refcount, adf_lockcount, adf_lockmax;
};
struct adouble {
uint32_t ad_magic; /* AD_MAGIC (0x00051607) */
uint32_t ad_version;
char ad_filler[16];
struct ad_entry ad_eid[ADEID_MAX]; /* entry offset/length table */
struct ad_fd ad_data_fork; /* data fork fd */
struct ad_fd ad_resource_fork; /* resource fork fd (EA or ._ file) */
struct ad_fd *ad_rfp; /* pointer to resource fork fd */
struct ad_fd *ad_mdp; /* pointer to metadata fd */
int ad_vers; /* AD_VERSION2 or AD_VERSION_EA */
int ad_adflags; /* ADFLAGS_* open flags */
int ad_refcount; /* multiple forks may share one adouble */
int ad_data_refcount;
int ad_meta_refcount;
int ad_reso_refcount;
off_t ad_rlen; /* resource fork length */
size_t valid_data_len; /* bytes read into ad_data */
char ad_data[AD_DATASZ_MAX]; /* 1024-byte in-memory buffer */
};
Core Functions
| Function | File | Signature | Purpose |
|---|---|---|---|
ad_init() |
ad_open.c |
void ad_init(struct adouble *ad, const struct vol *vol) |
Initialize adouble with volume defaults and entry offsets |
ad_open() |
ad_open.c |
int ad_open(struct adouble *ad, const char *path, int adflags, ...) |
Open data/resource/metadata forks (variadic: mode_t for create) |
ad_close() |
ad_open.c |
int ad_close(struct adouble *ad, int adflags) |
Close specified forks, decrement refcounts |
ad_flush() |
ad_open.c |
int ad_flush(struct adouble *ad) |
Write modified metadata back to disk/EA |
ad_read() |
ad_read.c |
ssize_t ad_read(struct adouble *ad, uint32_t eid, off_t off, char *buf, size_t buflen) |
Read from data or resource fork |
ad_write() |
ad_write.c |
ssize_t ad_write(struct adouble *ad, uint32_t eid, off_t off, int end, const char *buf, size_t buflen) |
Write to data or resource fork |
ad_lock() |
ad_lock.c |
int ad_lock(struct adouble *ad, uint32_t eid, int type, off_t off, off_t len, int fork) |
Set fcntl byte-range or share-mode lock |
ad_unlock() |
ad_lock.c |
void ad_unlock(struct adouble *ad, int fork, int unlckbrl) |
Release locks for a fork |
ad_tmplock() |
ad_lock.c |
int ad_tmplock(struct adouble *ad, uint32_t eid, int type, off_t off, off_t len, int fork) |
Temporary lock (not tracked in lock list) |
ad_metadata() |
ad_open.c |
int ad_metadata(const char *path, int flags, struct adouble *ad) |
Read-only open + header parse (leaves fd open) |
ad_entry() |
ad_open.c |
void *ad_entry(const struct adouble *ad, int eid) |
Get pointer to entry data within ad_data[] buffer |
ad_refresh() |
ad_open.c |
int ad_refresh(const char *path, struct adouble *ad) |
Re-read header from disk |
Entry IDs
/* adouble.h — AppleDouble entry IDs */
#define ADEID_DFORK 1 /* Data fork */
#define ADEID_RFORK 2 /* Resource fork */
#define ADEID_NAME 3 /* Real name */
#define ADEID_COMMENT 4 /* Comment */
#define ADEID_ICONBW 5 /* B&W icon */
#define ADEID_ICONCOL 6 /* Color icon */
#define ADEID_FILEI 7 /* v1 file info (replaced by FILEDATESI) */
#define ADEID_FILEDATESI 8 /* File dates: create/modify/backup/access */
#define ADEID_FINDERI 9 /* Finder info (32 bytes) */
#define ADEID_MACFILEI 10 /* Unused */
#define ADEID_PRODOSFILEI 11 /* ProDOS info */
#define ADEID_MSDOSFILEI 12 /* Unused */
#define ADEID_SHORTNAME 13 /* 8.3 short name */
#define ADEID_AFPFILEI 14 /* AFP file attributes (4 bytes) */
#define ADEID_DID 15 /* Directory ID */
#define ADEID_PRIVDEV 16 /* Netatalk private: device number */
#define ADEID_PRIVINO 17 /* Netatalk private: inode number */
#define ADEID_PRIVSYN 18 /* Netatalk private: DB sync stamp */
#define ADEID_PRIVID 19 /* Netatalk private: CNID */
Open Flags
/* adouble.h — ADFLAGS_* */
#define ADFLAGS_DF (1<<0) /* Data fork */
#define ADFLAGS_RF (1<<1) /* Resource fork */
#define ADFLAGS_HF (1<<2) /* Header (metadata) fork */
#define ADFLAGS_DIR (1<<3) /* Entry is a directory */
#define ADFLAGS_NOHF (1<<4) /* Not an error if no metadata fork */
#define ADFLAGS_NORF (1<<5) /* Not an error if no resource fork */
#define ADFLAGS_CHECK_OF (1<<6) /* Check for open forks from us and others */
#define ADFLAGS_SETSHRMD (1<<7) /* Set share mode with exclusive fcntl lock */
#define ADFLAGS_RDWR (1<<8) /* Open read/write */
#define ADFLAGS_RDONLY (1<<9) /* Open read-only */
#define ADFLAGS_CREATE (1<<10) /* Create if not existing */
#define ADFLAGS_EXCL (1<<11) /* Exclusive create */
#define ADFLAGS_TRUNC (1<<12) /* Truncate on open */
Refcounting
Multiple AFP forks can share one struct adouble. The macros ad_ref() and ad_unref() in adouble.h manage ad_refcount, while per-fork refcounts (ad_data_refcount, ad_meta_refcount, ad_reso_refcount) track individual fork opens.
/* adouble.h — ad_ref / ad_unref macros */
#define ad_ref(ad) (ad)->ad_refcount++
#define ad_unref(ad) --((ad)->ad_refcount)
Lock Model
Locks are placed on the data fork file using fcntl() at high offsets starting at AD_FILELOCK_BASE:
/* adouble.h — share-mode lock offsets */
#define AD_FILELOCK_OPEN_WR (AD_FILELOCK_BASE + 0)
#define AD_FILELOCK_OPEN_RD (AD_FILELOCK_BASE + 1)
#define AD_FILELOCK_RSRC_OPEN_WR (AD_FILELOCK_BASE + 2)
#define AD_FILELOCK_RSRC_OPEN_RD (AD_FILELOCK_BASE + 3)
#define AD_FILELOCK_DENY_WR (AD_FILELOCK_BASE + 4)
#define AD_FILELOCK_DENY_RD (AD_FILELOCK_BASE + 5)
#define AD_FILELOCK_RSRC_DENY_WR (AD_FILELOCK_BASE + 6)
#define AD_FILELOCK_RSRC_DENY_RD (AD_FILELOCK_BASE + 7)
#define AD_FILELOCK_OPEN_NONE (AD_FILELOCK_BASE + 8)
#define AD_FILELOCK_RSRC_OPEN_NONE (AD_FILELOCK_BASE + 9)
CNID API
Header: include/atalk/cnid.h
Implementation: libatalk/cnid/cnid.c, libatalk/cnid/cnid_init.c
The CNID API provides a backend-agnostic interface for assigning and resolving persistent file/directory IDs. Function pointers in cnid_db dispatch to the registered backend.
Backend Registration
cnid_init() in cnid_init.c registers compiled backends via cnid_register() in cnid.c:
| Backend | Module | Subdirectory |
|---|---|---|
dbd |
cnid_dbd_module |
libatalk/cnid/dbd/ |
mysql |
cnid_mysql_module |
libatalk/cnid/mysql/ |
sqlite |
cnid_sqlite_module |
libatalk/cnid/sqlite/ |
Core Structure
/* cnid.h — cnid_db */
typedef struct _cnid_db {
uint32_t cnid_db_flags;
struct vol *cnid_db_vol;
void *cnid_db_private; /* backend-specific data */
/* Function pointer table — set by backend's cnid_open() */
cnid_t (*cnid_add)(...);
int (*cnid_delete)(...);
cnid_t (*cnid_get)(...);
cnid_t (*cnid_lookup)(...);
cnid_t (*cnid_nextid)(...);
char *(*cnid_resolve)(...);
int (*cnid_update)(...);
void (*cnid_close)(...);
int (*cnid_getstamp)(...);
cnid_t (*cnid_rebuild_add)(...);
int (*cnid_find)(...);
int (*cnid_wipe)(...);
} cnid_db;
Core Functions
All dispatch functions in cnid.c wrap the function pointer call with signal blocking (when CNID_FLAG_BLOCK is set) and CNID validation:
| Function | Signature | Purpose |
|---|---|---|
cnid_open() |
struct _cnid_db *cnid_open(struct vol *vol, char *type, int flags) |
Open CNID database for a volume |
cnid_close() |
void cnid_close(struct _cnid_db *db) |
Close and free CNID database |
cnid_add() |
cnid_t cnid_add(cnid_db *cdb, const struct stat *st, cnid_t did, const char *name, size_t len, cnid_t hint) |
Add new entry, return assigned CNID |
cnid_get() |
cnid_t cnid_get(cnid_db *cdb, cnid_t did, char *name, size_t len) |
Look up CNID by parent DID + name |
cnid_lookup() |
cnid_t cnid_lookup(cnid_db *cdb, const struct stat *st, cnid_t did, char *name, size_t len) |
Look up by stat + DID + name |
cnid_resolve() |
char *cnid_resolve(cnid_db *cdb, cnid_t *id, void *buffer, size_t len) |
Resolve CNID → parent DID + name |
cnid_delete() |
int cnid_delete(cnid_db *cdb, cnid_t id) |
Remove entry |
cnid_update() |
int cnid_update(cnid_db *cdb, cnid_t id, const struct stat *st, cnid_t did, char *name, size_t len) |
Update entry metadata |
cnid_find() |
int cnid_find(cnid_db *cdb, const char *name, size_t namelen, void *buffer, size_t buflen) |
Search by name substring |
cnid_wipe() |
int cnid_wipe(cnid_db *cdb) |
Delete all entries |
Constants
/* cnid.h — CNID constants */
#define CNID_INVALID 0
#define CNID_START 17 /* First assignable CNID */
/* directory.h — reserved directory IDs */
#define DIRDID_ROOT_PARENT htonl(1) /* Parent of root directory */
#define DIRDID_ROOT htonl(2) /* Root directory */
/* cnid.h — backend flags */
#define CNID_FLAG_PERSISTENT 0x01 /* Backend supports DID persistence */
#define CNID_FLAG_MANGLING 0x02 /* Backend supports name mangling */
#define CNID_FLAG_SETUID 0x04 /* Set DB owner to parent folder owner */
#define CNID_FLAG_BLOCK 0x08 /* Block signals during updates */
#define CNID_FLAG_NODEV 0x10 /* Ignore device numbers (inode-only) */
#define CNID_FLAG_LAZY_INIT 0x20 /* Lazy initialization */
#define CNID_FLAG_INODE 0x80 /* Inode is authoritative in cnid_add */
DSI Protocol API
Header: include/atalk/dsi.h
Implementation: libatalk/dsi/
The DSI (Data Stream Interface) API implements the AFP-over-TCP transport protocol. Every AFP command is carried inside a DSI frame with a 16-byte header.
DSI Structure
/* dsi.h — struct DSI */
typedef struct DSI {
struct DSI *next; /* multiple listening addresses */
AFPObj *AFPobj;
int statuslen;
char status[1400];
char *signature;
struct dsi_block header;
struct sockaddr_storage server, client;
struct itimerval timer;
int tickle; /* tickle count */
int in_write; /* mid-write flag for signal safety */
int msg_request; /* pending message to client */
int down_request; /* pending SIGUSR1 shutdown */
uint32_t attn_quantum, datasize, server_quantum;
uint16_t serverID, clientID;
uint8_t *commands; /* receive buffer */
uint8_t data[DSI_DATASIZ]; /* 64KB reply buffer */
size_t datalen, cmdlen;
off_t read_count, write_count;
uint32_t flags; /* DSI_SLEEPING, DSI_DISCONNECTED, etc. */
int socket; /* AFP session socket */
int serversock; /* listening socket */
size_t dsireadbuf; /* readahead buffer size */
char *buffer, *start, *eof, *end; /* readahead pointers */
pid_t (*proto_open)(struct DSI *);
void (*proto_close)(struct DSI *);
} DSI;
DSI Header Format (16 bytes)
/* dsi.h — struct dsi_block */
struct dsi_block {
uint8_t dsi_flags; /* DSIFL_REQUEST (0x00) or DSIFL_REPLY (0x01) */
uint8_t dsi_command; /* DSIFUNC_* command type */
uint16_t dsi_requestID; /* request ID */
union {
uint32_t dsi_code; /* error code (replies) */
uint32_t dsi_doff; /* data offset (requests) */
} dsi_data;
uint32_t dsi_len; /* total data length */
uint32_t dsi_reserved;
};
Core Functions
| Function | File | Purpose |
|---|---|---|
dsi_init() |
dsi_init.c |
Allocate and initialize DSI structure |
dsi_free() |
dsi_init.c |
Free DSI structure |
dsi_getsession() |
dsi_getsess.c |
Fork child, set up session |
dsi_stream_read() |
dsi_stream.c |
Read bytes from socket |
dsi_stream_write() |
dsi_stream.c |
Write bytes to socket |
dsi_stream_send() |
dsi_stream.c |
Send DSI header + payload |
dsi_stream_receive() |
dsi_stream.c |
Read DSI header + payload |
dsi_cmdreply() |
dsi_cmdreply.c |
Send command reply |
dsi_attention() |
dsi_attn.c |
Send attention packet |
dsi_tickle() |
dsi_stream.c |
Send keepalive tickle |
dsi_disconnect() |
dsi_stream.c |
Force disconnect |
DSI Commands
/* dsi.h — DSIFUNC_* */
#define DSIFUNC_CLOSE 1 /* DSICloseSession */
#define DSIFUNC_CMD 2 /* DSICommand (AFP command) */
#define DSIFUNC_STAT 3 /* DSIGetStatus */
#define DSIFUNC_OPEN 4 /* DSIOpenSession */
#define DSIFUNC_TICKLE 5 /* DSITickle (keepalive) */
#define DSIFUNC_WRITE 6 /* DSIWrite */
#define DSIFUNC_ATTN 8 /* DSIAttention */
DSI Session State Flags
/* dsi.h — DSI state flags */
#define DSI_DATA (1 << 0) /* DSI command received */
#define DSI_RUNNING (1 << 1) /* AFP command received */
#define DSI_SLEEPING (1 << 2) /* Sleeping after FPZzz */
#define DSI_EXTSLEEP (1 << 3) /* Extended sleep */
#define DSI_DISCONNECTED (1 << 4) /* Disconnected after socket error */
#define DSI_DIE (1 << 5) /* SIGUSR1 shutdown in 5 minutes */
#define DSI_NOREPLY (1 << 6) /* In dsi_write, self-generating replies */
#define DSI_RECONSOCKET (1 << 7) /* New socket from primary reconnect */
#define DSI_RECONINPROG (1 << 8) /* Reconnect in progress */
#define DSI_AFP_LOGGED_OUT (1 << 9) /* Client called FPLogout */
IPC API
Header: include/atalk/server_ipc.h
Implementation: libatalk/util/server_ipc.c
The IPC system uses Unix socketpairs between the afpd parent and child worker processes. Each message has a 14-byte header followed by a variable-length payload.
Wire Format
/* server_ipc.h — wire format constants */
#define IPC_HEADERLEN 14 /* cmd(2) + pid(4) + uid(4) + len(4) */
#define IPC_MAXMSGSIZE 90
/* server_ipc.c — ipc_header_t */
typedef struct ipc_header {
uint16_t command;
pid_t child_pid;
uid_t uid;
uint32_t len;
char *msg;
int afp_socket;
uint16_t DSI_requestID;
} ipc_header_t;
IPC Commands
/* server_ipc.h — IPC commands */
#define IPC_DISCOLDSESSION 0 /* Disconnect old session (reconnect) */
#define IPC_GETSESSION 1 /* Register client ID */
#define IPC_STATE 2 /* Pass AFP session state */
#define IPC_VOLUMES 3 /* Pass list of open volumes */
#define IPC_LOGINDONE 4 /* Login completed notification */
#define IPC_CACHE_HINT 5 /* Cross-process dircache invalidation hint */
Core Functions
| Function | Direction | Signature | Purpose |
|---|---|---|---|
ipc_child_write() |
Child → Parent | int ipc_child_write(int fd, uint16_t command, size_t len, void *msg) |
Send IPC message from child |
ipc_server_read() |
Parent reads | int ipc_server_read(server_child_t *children, int fd) |
Read and dispatch IPC message |
ipc_child_state() |
Child → Parent | int ipc_child_state(AFPObj *obj, uint16_t state) |
Report session state change |
ipc_send_cache_hint() |
Child → Parent | int ipc_send_cache_hint(const AFPObj *obj, uint16_t vid, cnid_t cnid, uint8_t event) |
Send dircache invalidation hint |
hint_flush_pending() |
Parent → Children | void hint_flush_pending(server_child_t *children) |
Flush buffered hints to siblings |
hint_buf_count() |
Parent only | int hint_buf_count(void) |
Return number of buffered hints |
Cache Hint Types
/* server_ipc.h — cache hint event types */
#define CACHE_HINT_REFRESH 0 /* Refresh stat + AD metadata */
#define CACHE_HINT_DELETE 1 /* Remove entry by CNID */
#define CACHE_HINT_DELETE_CHILDREN 2 /* Remove children + refresh parent */
Cache Hint Payload
/* server_ipc.h — ipc_cache_hint_payload (packed, 8 bytes) */
struct __attribute__((packed)) ipc_cache_hint_payload {
uint8_t event; /* CACHE_HINT_REFRESH/DELETE/DELETE_CHILDREN */
uint8_t reserved;
uint16_t vid; /* Volume ID — network byte order */
cnid_t cnid; /* CNID — network byte order */
};
Hint Batching
The parent buffers hints from children in a static array of HINT_BUF_SIZE (128) entries. Flushing is poll-driven: when hint_buf_count() > 0, the main loop uses HINT_FLUSH_INTERVAL_MS (50ms) as poll timeout, then calls hint_flush_pending() which performs priority sorting (REFRESH first, DELETE, DELETE_CHILDREN last) and PIPE_BUF-safe chunked writes to each sibling’s afpch_hint_fd.
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph LR
style A fill:#74b9ff,rx:10,ry:10
style B fill:#ffeaa7,rx:10,ry:10
style C fill:#a29bfe,rx:10,ry:10
style D fill:#55efc4,rx:10,ry:10
style E fill:#74b9ff,rx:10,ry:10
A["Child afpd<br/>ipc_send_cache_hint()"] -->|IPC socket| B["Parent afpd<br/>hint_buf[128]"]
B -->|50ms flush| C["hint_flush_pending()<br/>sort + serialize"]
C -->|hint pipe| D["Sibling A<br/>process_cache_hints()"]
C -->|hint pipe| E["Sibling B<br/>process_cache_hints()"]
Child Management API
Header: include/atalk/server_child.h
Implementation: libatalk/util/server_child.c
Manages AFP session child processes using a hash table (PID-indexed, 32 buckets).
Core Structures
/* server_child.h — afp_child_t */
typedef struct afp_child {
pid_t afpch_pid; /* worker process PID */
uid_t afpch_uid; /* client user ID */
int afpch_valid; /* 1 if we have a client ID */
int afpch_killed; /* 1 if SIGTERM already sent */
uint32_t afpch_boottime; /* client boot time */
time_t afpch_logintime; /* time child was added */
uint32_t afpch_idlen; /* client ID length */
char *afpch_clientid; /* client ID (from Mac) */
int afpch_ipc_fd; /* IPC socketpair fd */
int afpch_hint_fd; /* cache hint pipe fd */
char *afpch_hostname; /* server hostname child connected to */
int16_t afpch_state; /* AFP session state */
char *afpch_volumes; /* mounted volume names */
struct afp_child **afpch_prevp;
struct afp_child *afpch_next;
} afp_child_t;
/* server_child.h — server_child_t */
typedef struct {
pthread_mutex_t servch_lock;
int servch_count; /* current active session count */
int servch_nsessions; /* max allowed sessions */
afp_child_t *servch_table[CHILD_HASHSIZE]; /* 32-bucket hash table */
} server_child_t;
Core Functions
| Function | Signature | Purpose |
|---|---|---|
server_child_alloc() |
server_child_t *server_child_alloc(int connections) |
Allocate child table with session limit |
server_child_add() |
afp_child_t *server_child_add(server_child_t *, pid_t, int ipc_fd, int hint_fd) |
Register a new child process |
server_child_remove() |
int server_child_remove(server_child_t *, pid_t) |
Remove and free child, returns IPC fd |
server_child_resolve() |
afp_child_t *server_child_resolve(server_child_t *, id_t pid) |
Find child by PID |
server_child_kill() |
void server_child_kill(server_child_t *, int sig) |
Send signal to all children |
server_child_free() |
void server_child_free(server_child_t *) |
Free entire table |
server_child_transfer_session() |
int server_child_transfer_session(...) |
Transfer socket to disconnected session (reconnect) |
server_child_kill_one_by_id() |
void server_child_kill_one_by_id(...) |
Kill child matching client ID (reboot detection) |
server_child_login_done() |
void server_child_login_done(...) |
Mark child as authenticated |
Hash Function
/* server_child.c — PID hash */
#define HASH(i) ((((i) >> 8) ^ (i)) & (CHILD_HASHSIZE - 1))
Character Conversion API
Header: include/atalk/unicode.h
Implementation: libatalk/unicode/charcnv.c, libatalk/unicode/
Handles charset conversion between Mac encodings, UTF-8, and Unix filesystem encodings.
Charset Types
/* unicode.h — charset_t */
typedef enum {
CH_UCS2 = 0, /* UCS-2 (internal pivot format) */
CH_UTF8 = 1, /* UTF-8 */
CH_MAC = 2, /* Volume's Mac charset (e.g., MacRoman) */
CH_UNIX = 3, /* System default charset */
CH_UTF8_MAC = 4 /* UTF-8 with Mac decomposition */
} charset_t;
Charset Registration
/* unicode.h — struct charset_functions */
struct charset_functions {
const char *name;
const long kTextEncoding;
size_t (*pull)(void *, char **inbuf, size_t *inbytesleft,
char **outbuf, size_t *outbytesleft);
size_t (*push)(void *, char **inbuf, size_t *inbytesleft,
char **outbuf, size_t *outbytesleft);
uint32_t flags;
const char *iname;
struct charset_functions *prev, *next;
};
Core Functions
| Function | File | Purpose |
|---|---|---|
init_iconv() |
charcnv.c |
Initialize conversion handles |
convert_charset() |
charcnv.c |
Convert between any two charsets with flags |
convert_string() |
charcnv.c |
Simple charset conversion |
convert_string_allocate() |
charcnv.c |
Convert with auto-allocated output buffer |
charset_precompose() |
charcnv.c |
Unicode precomposition |
charset_decompose() |
charcnv.c |
Unicode decomposition |
atalk_iconv_open() |
charcnv.c |
Open iconv conversion handle |
atalk_iconv() |
charcnv.c |
Perform iconv conversion |
atalk_register_charset() |
charcnv.c |
Register custom charset module |
Conversion Flags
/* unicode.h — CONV_* flags */
#define CONV_IGNORE (1<<0) /* Return first convertible characters */
#define CONV_ESCAPEHEX (1<<1) /* Escape unconvertible as :[UCS2HEX] */
#define CONV_ESCAPEDOTS (1<<2) /* Escape leading dots as :2600 */
#define CONV_UNESCAPEHEX (1<<3) /* Reverse hex escaping */
#define CONV_TOUPPER (1<<4) /* Convert to UPPER case */
#define CONV_TOLOWER (1<<5) /* Convert to lower case */
#define CONV_PRECOMPOSE (1<<6) /* Unicode precomposition */
#define CONV_DECOMPOSE (1<<7) /* Unicode decomposition */
#define CONV_FORCE (1<<8) /* Force conversion */
#define CONV__EILSEQ (1<<9) /* Replace illegal sequences with '_' */
Logging API
Header: include/atalk/logger.h
Implementation: libatalk/util/logger.c
LOG Macro
The primary logging interface is the LOG() macro in logger.h, which checks the configured log level before calling make_log_entry():
/* logger.h — LOG macro */
#define LOG(log_level, type, ...) \
do { \
if (log_level <= type_configs[type].level) \
make_log_entry((log_level), (type), __FILE__, \
type_configs[type].timestamp_us, __LINE__, \
__VA_ARGS__); \
} while(0)
Log Levels
/* logger.h — enum loglevels */
enum loglevels {
log_none, /* disabled */
log_severe, /* system errors */
log_error, /* serious errors */
log_warning, /* warnings */
log_note, /* important info */
log_info, /* general info */
log_debug, /* debug info */
log_debug6, /* verbose debug */
log_debug7, /* very verbose */
log_debug8, /* extremely verbose */
log_debug9, /* maximum verbosity */
log_maxdebug
};
Log Types
/* logger.h — enum logtypes */
enum logtypes {
logtype_default, /* default (used when no other specified) */
logtype_logger, /* general logger */
logtype_cnid, /* CNID backends */
logtype_afpd, /* AFP daemon */
logtype_dsi, /* DSI layer */
logtype_atalkd, /* AppleTalk daemon */
logtype_papd, /* PAP daemon */
logtype_uams, /* UAM modules */
logtype_fce, /* File Change Events */
logtype_ad, /* AppleDouble */
logtype_sl, /* Spotlight */
logtype_end_of_list_marker
};
Configuration
/* logger.h — setup functions */
void setuplog(const char *loglevel, const char *logfile, const bool log_us_timestamp);
void set_processname(const char *processname);
VFS API
Header: include/atalk/vfs.h
Implementation: libatalk/vfs/
The VFS layer provides an indirection table for filesystem operations, enabling different backends for permissions, file operations, and extended attributes.
VFS Operations Table
/* vfs.h — struct vfs_ops */
struct vfs_ops {
vfs_validupath_fn vfs_validupath; /* Validate Unix path */
vfs_chown_fn vfs_chown; /* Change ownership */
vfs_renamedir_fn vfs_renamedir; /* Rename directory */
vfs_deletecurdir_fn vfs_deletecurdir; /* Delete current directory */
vfs_setfilmode_fn vfs_setfilmode; /* Set file permissions */
vfs_setdirmode_fn vfs_setdirmode; /* Set directory permissions */
vfs_setdirunixmode_fn vfs_setdirunixmode; /* Set Unix dir permissions */
vfs_setdirowner_fn vfs_setdirowner; /* Set directory owner */
vfs_deletefile_fn vfs_deletefile; /* Delete file */
vfs_renamefile_fn vfs_renamefile; /* Rename file */
vfs_copyfile_fn vfs_copyfile; /* Copy file */
/* ACL operations (conditional compilation) */
vfs_remove_acl_fn vfs_remove_acl; /* Remove ACL */
/* Extended Attribute operations */
vfs_ea_getsize_fn vfs_ea_getsize; /* Get EA size */
vfs_ea_getcontent_fn vfs_ea_getcontent; /* Get EA content */
vfs_ea_list_fn vfs_ea_list; /* List EAs */
vfs_ea_set_fn vfs_ea_set; /* Set EA */
vfs_ea_remove_fn vfs_ea_remove; /* Remove EA */
};
Volumes chain up to VFS_MODULES_MAX (3) modules via vol->vfs_modules[]. The function initvol_vfs() initializes VFS for a volume based on its configuration.
Extended Attributes API
Header: include/atalk/ea.h
Implementation: libatalk/vfs/
Two EA backends exist:
| Backend | Flag | Storage |
|---|---|---|
| Native (sys) | AFPVOL_EA_SYS |
Platform xattr syscalls |
| AppleDouble (ad) | AFPVOL_EA_AD |
.AppleDouble/ directory files |
EA Names
/* ea.h — Extended Attribute xattr names */
#define AD_EA_META "org.netatalk.Metadata" /* AppleDouble metadata xattr */
#define AD_EA_RESO "org.netatalk.ResourceFork" /* Resource fork xattr (Linux) */
/* On macOS: */
/* #define AD_EA_RESO "com.apple.ResourceFork" */
Native EA Wrappers
| Function | Purpose |
|---|---|
sys_getxattr() |
Get xattr value |
sys_setxattr() |
Set xattr value |
sys_listxattr() |
List xattr names |
sys_removexattr() |
Remove xattr |
sys_copyxattr() |
Copy all xattrs between files |
VFS-Indirected EA Functions
| Function | Purpose |
|---|---|
get_easize() |
Get EA size via VFS |
get_eacontent() |
Get EA content via VFS |
list_eas() |
List EAs via VFS |
set_ea() |
Set EA via VFS |
remove_ea() |
Remove EA via VFS |
File Change Event (FCE) API
Header: include/atalk/fce_api.h
FCE sends UDP notifications for filesystem events to external listeners.
Event Types
/* fce_api.h — FCE event types */
#define FCE_FILE_MODIFY 1
#define FCE_FILE_DELETE 2
#define FCE_DIR_DELETE 3
#define FCE_FILE_CREATE 4
#define FCE_DIR_CREATE 5
#define FCE_FILE_MOVE 6
#define FCE_DIR_MOVE 7
#define FCE_LOGIN 8
#define FCE_LOGOUT 9
Core Functions
/* fce_api.h */
int fce_register(const AFPObj *obj, fce_ev_t event, const char *path,
const char *oldpath);
int fce_add_udp_socket(const char *target); /* "IP" or "IP:Port" */
int fce_set_events(const char *events); /* "fmod,fdel,ddel,fcre,dcre" */
Default UDP port: FCE_DEFAULT_PORT (12250).
Error Handling
Header: include/atalk/errchk.h
A macro-based error handling framework using goto cleanup pattern. Functions following this pattern have a consistent structure:
int func(void)
{
EC_INIT; /* int ret = 0; */
EC_ZERO( some_call() ); /* goto cleanup if != 0 */
EC_NULL( some_ptr() ); /* goto cleanup if NULL */
EC_NEG1( some_fd() ); /* goto cleanup if == -1 */
EC_STATUS(0); /* ret = 0 */
EC_CLEANUP: /* cleanup: */
/* cleanup code here */
EC_EXIT; /* return ret; */
}
Macro Reference
| Macro | Condition | Logging |
|---|---|---|
EC_INIT |
— | Declares int ret = 0 |
EC_ZERO(a) |
a != 0 |
No |
EC_ZERO_LOG(a) |
a != 0 |
Logs errno |
EC_ZERO_LOGSTR(a, fmt, ...) |
a != 0 |
Logs custom message |
EC_ZERO_LOG_ERR(a, err) |
a != 0 |
Logs errno, sets ret = err |
EC_NEG1(a) |
a == -1 |
No |
EC_NEG1_LOG(a) |
a == -1 |
Logs errno |
EC_NULL(a) |
a == NULL |
No |
EC_NULL_LOG(a) |
a == NULL |
Logs errno |
EC_FAIL |
Always | ret = -1; goto cleanup |
EC_FAIL_LOG(fmt, ...) |
Always | Logs message, ret = -1 |
EC_STATUS(a) |
— | Sets ret = a |
EC_EXIT_STATUS(a) |
— | Sets ret = a; goto cleanup |
EC_CLEANUP |
— | Label alias: cleanup: |
EC_EXIT |
— | return ret |
Global Structures
Header: include/atalk/globals.h
AFPObj
The central per-session object, passed to all AFP command handlers:
/* globals.h — AFPObj */
typedef struct AFPObj {
char *cmdlineconfigfile;
int cmdlineflags;
int proto;
const void *signature;
struct DSI *dsi;
void *handle;
struct afp_options options;
char *Obj, *Type, *Zone;
dictionary *iniconfig;
char username[MAXUSERLEN]; /* 256 */
char oldtmp[AFPOBJ_TMPSIZ + 1]; /* MAXPATHLEN + 1 */
char newtmp[AFPOBJ_TMPSIZ + 1];
void *uam_cookie; /* UAM authentication state */
struct session_info sinfo;
uid_t uid; /* client login user ID */
uid_t euid; /* effective process user ID */
int ipc_fd; /* IPC socketpair with parent */
int hint_fd; /* cache hint pipe from parent */
gid_t *groups;
int ngroups;
int afp_version;
int cnx_cnt, cnx_max;
void (*logout)(void);
void (*exit)(int);
int (*reply)(void *, int);
int (*attention)(void *, AFPUserBytes);
int fce_version;
char *fce_ign_names;
char *fce_ign_directories;
char *fce_notify_script;
struct sl_ctx *sl_ctx;
} AFPObj;
afp_options
/* globals.h — struct afp_options (key fields) */
struct afp_options {
int connections; /* Max AFP connections */
int tickleval; /* Tickle interval */
int timeout; /* Session timeout */
int flags; /* OPTION_* flags */
int dircachesize; /* Directory cache size */
int dircache_validation_freq; /* Validate every Nth access */
int dircache_rfork_budget; /* Rfork cache budget (KB, 0=disabled) */
int dircache_rfork_maxentry; /* Max rfork per entry (KB) */
int sleep; /* Max sleep time (tickles) */
int disconnected; /* Max disconnected time (tickles) */
unsigned int tcp_sndbuf, tcp_rcvbuf;
uint32_t server_quantum; /* DSI server quantum */
char *hostname;
char *listen, *interfaces, *port;
char *Cnid_srv, *Cnid_port;
char *configfile;
char *uampath, *fqdn;
char *unixcodepage, *maccodepage;
charset_t maccharset, unixcharset;
/* ... more fields ... */
};
Directory and Path Structures
/* directory.h — struct dir (key fields) */
struct dir {
bstring d_fullpath; /* complete unix path */
bstring d_m_name; /* mac name */
bstring d_u_name; /* unix name */
void *dcache_rfork_buf; /* Tier 2: cached rfork data */
qnode_t *rfork_lru_node; /* rfork LRU position */
time_t dcache_ctime; /* cached inode ctime */
ino_t dcache_ino; /* cached inode number */
time_t dcache_mtime; /* cached st_mtime */
off_t dcache_size; /* cached file size */
off_t dcache_rlen; /* cached resource fork length */
int d_flags; /* DIRF_* flags */
cnid_t d_pdid; /* parent CNID */
cnid_t d_did; /* CNID */
uint32_t d_offcnt; /* offspring count */
uint8_t arc_list; /* ARC list: 0=NONE,1=T1,2=T2,3=B1,4=B2 */
uint8_t dcache_afpfilei[4]; /* Tier 1: AFP attributes */
uint8_t dcache_finderinfo[32]; /* Tier 1: Finder info */
uint8_t dcache_filedatesi[16]; /* Tier 1: file dates (served values) */
};
/* directory.h — struct path */
struct path {
int m_type; /* mac name type */
char *m_name; /* mac name */
char *u_name; /* unix name */
cnid_t id; /* file ID */
struct dir *d_dir; /* directory entry (NULL for files) */
int st_valid; /* stat validity flag */
int st_errno;
struct stat st;
};
Directory Flags
/* directory.h — DIRF_* flags */
#define DIRF_FSMASK (3<<0)
#define DIRF_NOFS (0<<0)
#define DIRF_UFS (1<<1)
#define DIRF_ISFILE (1<<3) /* Cached file, not a directory */
#define DIRF_OFFCNT (1<<4) /* Offspring count is valid */
#define DIRF_CNID (1<<5) /* Renumerate ID */
#define DIRF_ARC_GHOST (1<<6) /* ARC ghost entry (in B1/B2) */
Cache APIs
These APIs live in etc/afpd/ (not in libatalk), but operate on libatalk’s struct dir and struct adouble.
AD Cache (Tier 1: Metadata)
Header: etc/afpd/ad_cache.h
Implementation: etc/afpd/ad_cache.c
Caches AppleDouble metadata fields (FinderInfo, FileDatesI, AFPFileI, resource fork length) inside struct dir fields, avoiding repeated getxattr() / disk reads.
| Function | Signature | Purpose |
|---|---|---|
ad_store_to_cache() |
void ad_store_to_cache(struct adouble *adp, struct dir *cached) |
Copy AD fields to cache; pre-compute served mdate |
ad_rebuild_from_cache() |
void ad_rebuild_from_cache(struct adouble *adp, const struct dir *cached) |
Populate struct adouble from cached fields |
ad_metadata_cached() |
int ad_metadata_cached(const char *name, int flags, struct adouble *adp, const struct vol *vol, struct dir *dir, bool strict, struct stat *recent_st) |
Unified AD access: cache-first or strict validation |
Cache state in dcache_rlen:
| Value | Meaning |
|---|---|
>= 0 |
AD loaded; cached resource fork length |
-1 |
Not yet loaded (triggers ad_metadata() on first access) |
-2 |
Confirmed no AD exists (avoids repeated ENOENT) |
Statistics: ad_cache_hits, ad_cache_misses, ad_cache_no_ad.
Dircache (ARC Algorithm)
Header: etc/afpd/dircache.h
Implementation: etc/afpd/dircache.c
An ARC (Adaptive Replacement Cache) implementation for directory/file entries. Uses a hash table with DID/name index and queue index for LRU eviction.
| Function | Signature | Purpose |
|---|---|---|
dircache_init() |
int dircache_init(int reqsize) |
Initialize with requested size |
dircache_add() |
int dircache_add(const struct vol *, struct dir *) |
Add entry to cache |
dircache_remove() |
void dircache_remove(const struct vol *, struct dir *, int flag) |
Remove with flag control |
dircache_search_by_did() |
struct dir *dircache_search_by_did(const struct vol *, cnid_t) |
Look up by CNID |
dircache_search_by_name() |
struct dir *dircache_search_by_name(const struct vol *, const struct dir *, char *, size_t) |
Look up by parent + name |
dircache_promote() |
void dircache_promote(struct dir *dir) |
Promote entry in ARC (on access) |
dircache_remove_children() |
int dircache_remove_children(const struct vol *, struct dir *) |
Remove all children of a directory |
process_cache_hints() |
void process_cache_hints(AFPObj *obj) |
Process incoming IPC cache hints |
dircache_dump() |
void dircache_dump(void) |
Dump cache to /tmp/dircache.PID (SIGINT) |
log_dircache_stat() |
void log_dircache_stat(void) |
Log cache statistics |
Size constants:
/* dircache.h — size bounds */
#define MIN_DIRCACHE_SIZE 1024 /* 1K minimum */
#define DEFAULT_DIRCACHE_SIZE 65536 /* 64K default */
#define MAX_DIRCACHE_SIZE 1048576 /* 1M maximum */
#define DIRCACHE_FREE_QUANTUM 256 /* eviction batch size */
Resource Fork Cache (Tier 2: Data)
Header: etc/afpd/ad_cache.h (rfork functions)
Implementation: etc/afpd/ad_cache.c (rfork functions), etc/afpd/dircache.c (budget globals, LRU list)
Caches small resource fork data in memory to avoid repeated disk/xattr reads. Budget-managed with a dedicated LRU list separate from the main dircache.
| Function | Signature | Purpose |
|---|---|---|
rfork_cache_store_from_fd() |
int rfork_cache_store_from_fd(struct dir *entry, struct adouble *adp, int eid) |
Read rfork data via ad_read() and cache in dcache_rfork_buf |
rfork_cache_free() |
void rfork_cache_free(struct dir *entry) |
Free buffer, remove from LRU, update budget |
rfork_cache_evict_to_budget() |
void rfork_cache_evict_to_budget(size_t needed) |
Evict LRU entries until budget accommodates needed bytes |
Budget management globals (defined in dircache.c):
| Variable | Type | Purpose |
|---|---|---|
rfork_cache_used |
size_t |
Current total bytes in rfork cache |
rfork_cache_budget |
size_t |
Configurable limit (bytes), 0 = disabled |
rfork_max_entry_size |
size_t |
Per-entry limit (bytes) |
rfork_lru_count |
unsigned int |
Entries currently in rfork LRU list |
rfork_lru |
q_t * |
Dedicated LRU list for rfork cache entries |
Configuration (in globals.h):
/* globals.h — rfork cache hard caps */
#define RFORK_BUDGET_MAX_KB (10 * 1024 * 1024) /* 10 GB in KB */
#define RFORK_ENTRY_MAX_KB (10 * 1024) /* 10 MB in KB */
Configured via afp_options.dircache_rfork_budget and afp_options.dircache_rfork_maxentry (both in KB).
Statistics:
| Counter | Purpose |
|---|---|
rfork_stat_lookups |
Total rfork cache lookups |
rfork_stat_hits |
Cache hits (data served from memory) |
rfork_stat_misses |
Cache misses (fell through to disk) |
rfork_stat_added |
Entries successfully cached |
rfork_stat_evicted |
Entries evicted by LRU |
rfork_stat_invalidated |
Entries invalidated by stat change |
rfork_stat_used_max |
High-water mark of bytes used |
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph TD
style T1 fill:#74b9ff,rx:10,ry:10
style T2 fill:#a29bfe,rx:10,ry:10
style DC fill:#55efc4,rx:10,ry:10
style DISK fill:#dfe6e9,rx:10,ry:10
style DIR fill:#ffeaa7,rx:10,ry:10
DIR["struct dir<br/>dcache_finderinfo[32]<br/>dcache_filedatesi[16]<br/>dcache_afpfilei[4]<br/>dcache_rlen"] --> T1
DIR --> T2
T1["Tier 1: AD Metadata Cache<br/>ad_metadata_cached()<br/>ad_store_to_cache()<br/>ad_rebuild_from_cache()"]
T2["Tier 2: Rfork Data Cache<br/>rfork_cache_store_from_fd()<br/>dcache_rfork_buf<br/>Budget-managed LRU"]
DC["Dircache (ARC)<br/>dircache_search_by_did()<br/>dircache_search_by_name()"] --> DIR
T1 -->|miss| DISK["Disk / xattr<br/>ad_metadata()<br/>ad_read()"]
T2 -->|miss| DISK
Footnotes
This is a mirror of the Netatalk GitHub Wiki
Last updated 2026-04-06