Functional Weave
Code in TypeScript

net.mac-address

Validate a MAC address in colon, hyphen, Cisco dot or bare form and normalise it, with multicast and local bits.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 22 tests, run in TypeScript, Python and Rust.

What it does

Checks that a text is an EUI-48 (MAC) address and normalises it to lower-case, colon-separated form, `00:1b:63:84:45:e6`. It never throws: an invalid address comes back with `valid: false` and a short `reason`, so an inventory can list every bad entry instead of stopping at the first.

## Forms accepted

For example

  • validateMacAddress(00:1b:63:84:45:e6) → valid true, canonical 00:1b:63:84:45:e6, reason —, multicast false, locally administered false, oui 00-1B-63 colon form, lower case, a universally administered unicast address
  • validateMacAddress(00-1B-63-84-45-E6) → valid true, canonical 00:1b:63:84:45:e6, reason —, multicast false, locally administered false, oui 00-1B-63 hyphen form, upper case, as Windows ipconfig prints it, is lower-cased
  • validateMacAddress(001b.6384.45e6) → valid true, canonical 00:1b:63:84:45:e6, reason —, multicast false, locally administered false, oui 00-1B-63 Cisco dotted form aabb.ccdd.eeff

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function validateMacAddress(text: string): MacValidation
textstringaa:bb:cc:dd:ee:ff, AA-BB-CC-DD-EE-FF, aabb.ccdd.eeff or aabbccddeeff
returnsMacValidation

The type it declares, generated into your project

/** Whether the text is an EUI-48 address, and what it says about itself. */
export interface MacValidation {
  readonly valid: boolean;
  /** lower-case, colon-separated */
  readonly canonical: string | null;
  /** why it is not valid */
  readonly reason: string | null;
  /** the I/G bit: a group address rather than one interface */
  readonly multicast: boolean | null;
  /** the U/L bit: set by software, not burned in by the maker */
  readonly locallyAdministered: boolean | null;
  /** the first three octets, AA-BB-CC as the IEEE registry writes them */
  readonly oui: string | null;
}

Your code names it in one line, in the file that uses it

import { validateMacAddress } from "#fune/net.mac-address@^1";
impl/typescript.ts · 54 lines · open · raw
import { type MacValidation } from "./net_mac_address_types.ts";

const HEX = "0123456789abcdefABCDEF";

function invalid(reason: string): MacValidation {
  return { valid: false, canonical: null, reason, multicast: null, locallyAdministered: null, oui: null };
}

/**
 * Check a MAC (EUI-48) address written in any of the four common forms and
 * normalise it to lower-case colon form.
 *
 * Exactly four shapes are accepted: aa:bb:cc:dd:ee:ff, aa-bb-cc-dd-ee-ff,
 * aabb.ccdd.eeff (Cisco) and aabbccddeeff. Nothing is trimmed and single-digit
 * octets are refused, because "a:b:c:d:e:f" is more often a typo than a MAC.
 */
export function validateMacAddress(text: string): MacValidation {
  if (typeof text !== "string") return invalid("not text");
  if (text.length === 0) return invalid("empty");

  const seps = [":", "-", "."].filter((s) => text.includes(s));
  if (seps.length > 1) return invalid("mixed separators");

  // Count characters, not UTF-16 units or bytes, so every language agrees.
  const chars = (s: string) => Array.from(s).length;
  const sep = seps[0];
  const parts = sep === undefined ? [text] : text.split(sep);
  const shapeOk =
    sep === undefined
      ? chars(text) === 12
      : sep === "."
        ? parts.length === 3 && parts.every((p) => chars(p) === 4)
        : parts.length === 6 && parts.every((p) => chars(p) === 2);
  if (!shapeOk) return invalid("not a recognised MAC address format");

  const hex = parts.join("");
  for (const ch of hex) {
    if (!HEX.includes(ch)) return invalid("not a hex digit");
  }

  const lower = hex.toLowerCase();
  const octets: string[] = [];
  for (let i = 0; i < 12; i += 2) octets.push(lower.slice(i, i + 2));
  const first = parseInt(octets[0], 16);
  return {
    valid: true,
    canonical: octets.join(":"),
    reason: null,
    // The I/G bit is the least significant bit of the first octet, the U/L bit the next.
    multicast: (first & 1) === 1,
    locallyAdministered: (first & 2) === 2,
    oui: octets.slice(0, 3).join("-").toUpperCase(),
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add net.mac-address
Download for TypeScript net.mac-address-1.0.0-typescript.fune · 12,134 bytes sha256 800dbac36f9d706add93717ed708bf69091d238a0fea5eb97f32205f77ab9343

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./net.mac-address-1.0.0-typescript.fune, or fetch it from a terminal with fune pull net.mac-address@1.0.0:typescript.

The whole function, every language, is one file too: net.mac-address-1.0.0.fune, 17,508 bytes, sha256 8645bd19c80f220684b55b51b8ce0a839c37ee15c4a4b860ad78c9fae11e03af. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before net.mac-address

after — your function gets the result and the arguments, and returns the final result.

// fune: after net.mac-address

replace — it requires no other capability, so there is no dependency to replace.

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show net.mac-address --steps.

// fune: step net.mac-address after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
colon form, lower case, a universally administered unicast address 00:1b:63:84:45:e6 → valid true, canonical 00:1b:63:84:45:e6, reason —, multicast false, locally administered false, oui 00-1B-63
hyphen form, upper case, as Windows ipconfig prints it, is lower-cased 00-1B-63-84-45-E6 → valid true, canonical 00:1b:63:84:45:e6, reason —, multicast false, locally administered false, oui 00-1B-63
Cisco dotted form aabb.ccdd.eeff 001b.6384.45e6 → valid true, canonical 00:1b:63:84:45:e6, reason —, multicast false, locally administered false, oui 00-1B-63
bare twelve hex digits 001B638445E6 → valid true, canonical 00:1b:63:84:45:e6, reason —, multicast false, locally administered false, oui 00-1B-63
broadcast ff:ff:ff:ff:ff:ff has the I/G and U/L bits set FF:FF:FF:FF:FF:FF → valid true, canonical ff:ff:ff:ff:ff:ff, reason —, multicast true, locally administered true, oui FF-FF-FF
IPv4 multicast MAC 01:00:5e is a group address but universally administered 01:00:5e:00:00:fb → valid true, canonical 01:00:5e:00:00:fb, reason —, multicast true, locally administered false, oui 01-00-5E
0x02 in the first octet is a locally administered unicast (Docker, VMs) 02:42:ac:11:00:02 → valid true, canonical 02:42:ac:11:00:02, reason —, multicast false, locally administered true, oui 02-42-AC
first octet 0x03 sets both bits 03-00-00-00-00-01 → valid true, canonical 03:00:00:00:00:01, reason —, multicast true, locally administered true, oui 03-00-00
mixed case hex digits in Cisco form AaBb.CcDd.EeFf → valid true, canonical aa:bb:cc:dd:ee:ff, reason —, multicast false, locally administered true, oui AA-BB-CC
all zeros is well formed 000000000000 → valid true, canonical 00:00:00:00:00:00, reason —, multicast false, locally administered false, oui 00-00-00
Show the other 12 tests
CaseArgumentsExpected
empty text → valid false, canonical —, reason empty, multicast —, locally administered —, oui —
mixed separators are refused 00:1b-63:84:45:e6 → valid false, canonical —, reason mixed separators, multicast —, locally administered —, oui —
single-digit octets are refused, not zero-padded 0:1b:63:84:45:e6 → valid false, canonical —, reason not a recognised MAC address format, multicast —, locally administered —, oui —
five octets is too short 00:1b:63:84:45 → valid false, canonical —, reason not a recognised MAC address format, multicast —, locally administered —, oui —
seven octets is too long (EUI-64 is not accepted) 00:1b:63:84:45:e6:01 → valid false, canonical —, reason not a recognised MAC address format, multicast —, locally administered —, oui —
Cisco form with groups of the wrong size 001b6.38445.e6 → valid false, canonical —, reason not a recognised MAC address format, multicast —, locally administered —, oui —
a trailing newline is not trimmed 00:1b:63:84:45:e6 → valid false, canonical —, reason not a recognised MAC address format, multicast —, locally administered —, oui —
leading space is not trimmed 001b638445e6 → valid false, canonical —, reason not a recognised MAC address format, multicast —, locally administered —, oui —
g is not a hex digit 00:1b:63:84:45:g6 → valid false, canonical —, reason not a hex digit, multicast —, locally administered —, oui —
Arabic-Indic digits are not hex digits (length counted in characters) 00:1b:63:84:45:٦٠ → valid false, canonical —, reason not a hex digit, multicast —, locally administered —, oui —
fullwidth letters are not hex digits AA:bb:cc:dd:ee:ff → valid false, canonical —, reason not a hex digit, multicast —, locally administered —, oui —
bare form of eleven digits 001b638445e → valid false, canonical —, reason not a recognised MAC address format, multicast —, locally administered —, oui —

More from the author

Exactly four, in either case:

| form | example | where you see it | |------|---------|------------------| | colon | `00:1b:63:84:45:e6` | Linux, macOS, IEEE 802 | | hyphen | `00-1B-63-84-45-E6` | Windows | | Cisco dotted | `001b.6384.45e6` | Cisco IOS | | bare | `001b638445e6` | databases, DHCP exports |

Refused, deliberately:

- mixed separators (`00:1b-63:...`): `mixed separators`; - single-digit octets (`0:1b:63:84:45:e6`), which some tools print but which are more often a typo than an address; EUI-64 and short forms: `not a recognised MAC address format`; - surrounding whitespace, including a trailing newline: nothing is trimmed, so trim input yourself if you mean to; - anything that is not an ASCII hex digit, including non-ASCII digits and fullwidth letters: `not a hex digit`. Lengths are counted in characters in every language, so they fail the same way everywhere.

## What the bits say

- `multicast` is the I/G bit, the least significant bit of the first octet: a group address (`01:00:5e:...` for IPv4 multicast, `ff:ff:ff:ff:ff:ff` broadcast) rather than one interface. - `locallyAdministered` is the U/L bit, the next bit up: the address was set by software (Docker's `02:42:...`, VMs, randomised Wi-Fi MACs) rather than assigned by the maker. - `oui` is the first three octets as the IEEE registry writes them, `00-1B-63`. For a locally administered address it is not a real maker's OUI.

Source: IEEE 802-2014 section 8.2 (MAC address format, I/G and U/L bits), and the IEEE "Guidelines for Use of Extended Unique Identifier (EUI)".

Files

PathBytes
README.md1,906
impl/python.py1,906
impl/rust.rs3,235
impl/typescript.ts2,043
vectors.json5,120