Skip to content

feat: Add MAC Address validation (#3970) #3972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
- [Dates](#dates)
- [Times](#times)
- [IP addresses](#ip-addresses)
- [IP ranges](#ip-ranges-cidr)
- [IP ranges (CIDR)](#ip-ranges-cidr)
- [MAC addresses](#mac-addresses)
- [Numbers](#numbers)
- [BigInts](#bigints)
- [NaNs](#nans)
Expand Down Expand Up @@ -106,7 +107,6 @@
- [Unions](#unions)
- [Discriminated unions](#discriminated-unions)
- [Records](#records)
- [Record key type](#record-key-type)
- [Maps](#maps)
- [Sets](#sets)
- [Intersections](#intersections)
Expand Down Expand Up @@ -154,6 +154,7 @@
- [Guides and concepts](#guides-and-concepts)
- [Type inference](#type-inference)
- [Writing generic functions](#writing-generic-functions)
- [Inferring the inferred type](#inferring-the-inferred-type)
- [Constraining allowable inputs](#constraining-allowable-inputs)
- [Error handling](#error-handling)
- [Error formatting](#error-formatting)
Expand Down Expand Up @@ -809,6 +810,7 @@ z.string().date(); // ISO date format (YYYY-MM-DD)
z.string().time(); // ISO time format (HH:mm:ss[.SSSSSS])
z.string().duration(); // ISO 8601 duration
z.string().base64();
z.string().mac(); //Validate 48-bit MAC
```

> Check out [validator.js](https://github.com/validatorjs/validator.js) for a bunch of other useful string validation functions that can be used in conjunction with [Refinements](#refine).
Expand Down Expand Up @@ -840,6 +842,7 @@ z.string().date({ message: "Invalid date string!" });
z.string().time({ message: "Invalid time string!" });
z.string().ip({ message: "Invalid IP address" });
z.string().cidr({ message: "Invalid CIDR" });
z.string().mac({ message: "Invalid MAC address" });
```

### Datetimes
Expand Down Expand Up @@ -970,6 +973,18 @@ ipv4Cidr.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // fail

const ipv6Cidr = z.string().cidr({ version: "v6" });
ipv6Cidr.parse("192.168.1.1"); // fail
```
### MAC addresses

Validate standard 48-bit MAC address [IEEE 802](https://en.wikipedia.org/wiki/MAC_address).

```ts
const mac = z.string().mac();
mac.parse("00:1A:2B:3C:4D:5E"); // Pass
mac.parse("98-76-54-32-10-FF"); // Pass
mac.parse("00:1A:2B:3C:4D:5E:FF"); // Fail
//Fails if mixed sperator used
mac.parse("00:1A:2B-3C:4D:5E"); // Fail
```

## Numbers
Expand Down
1 change: 1 addition & 0 deletions src/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export type StringValidation =
| "base64"
| "jwt"
| "base64url"
| "mac"
| { includes: string; position?: number }
| { startsWith: string }
| { endsWith: string };
Expand Down
56 changes: 56 additions & 0 deletions src/__tests__/string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ test("checks getters", () => {
expect(z.string().email().isIP).toEqual(false);
expect(z.string().email().isCIDR).toEqual(false);
expect(z.string().email().isULID).toEqual(false);
expect(z.string().email().isMac).toEqual(false);

expect(z.string().url().isEmail).toEqual(false);
expect(z.string().url().isURL).toEqual(true);
Expand All @@ -474,6 +475,7 @@ test("checks getters", () => {
expect(z.string().url().isIP).toEqual(false);
expect(z.string().url().isCIDR).toEqual(false);
expect(z.string().url().isULID).toEqual(false);
expect(z.string().url().isMac).toEqual(false);

expect(z.string().cuid().isEmail).toEqual(false);
expect(z.string().cuid().isURL).toEqual(false);
Expand All @@ -484,6 +486,7 @@ test("checks getters", () => {
expect(z.string().cuid().isIP).toEqual(false);
expect(z.string().cuid().isCIDR).toEqual(false);
expect(z.string().cuid().isULID).toEqual(false);
expect(z.string().cuid().isMac).toEqual(false);

expect(z.string().cuid2().isEmail).toEqual(false);
expect(z.string().cuid2().isURL).toEqual(false);
Expand All @@ -494,6 +497,7 @@ test("checks getters", () => {
expect(z.string().cuid2().isIP).toEqual(false);
expect(z.string().cuid2().isCIDR).toEqual(false);
expect(z.string().cuid2().isULID).toEqual(false);
expect(z.string().cuid2().isMac).toEqual(false);

expect(z.string().uuid().isEmail).toEqual(false);
expect(z.string().uuid().isURL).toEqual(false);
Expand All @@ -504,6 +508,7 @@ test("checks getters", () => {
expect(z.string().uuid().isIP).toEqual(false);
expect(z.string().uuid().isCIDR).toEqual(false);
expect(z.string().uuid().isULID).toEqual(false);
expect(z.string().uuid().isMac).toEqual(false);

expect(z.string().nanoid().isEmail).toEqual(false);
expect(z.string().nanoid().isURL).toEqual(false);
Expand All @@ -514,6 +519,7 @@ test("checks getters", () => {
expect(z.string().nanoid().isIP).toEqual(false);
expect(z.string().nanoid().isCIDR).toEqual(false);
expect(z.string().nanoid().isULID).toEqual(false);
expect(z.string().nanoid().isMac).toEqual(false);

expect(z.string().ip().isEmail).toEqual(false);
expect(z.string().ip().isURL).toEqual(false);
Expand All @@ -524,6 +530,7 @@ test("checks getters", () => {
expect(z.string().ip().isIP).toEqual(true);
expect(z.string().ip().isCIDR).toEqual(false);
expect(z.string().ip().isULID).toEqual(false);
expect(z.string().ip().isMac).toEqual(false);

expect(z.string().cidr().isEmail).toEqual(false);
expect(z.string().cidr().isURL).toEqual(false);
Expand All @@ -534,6 +541,7 @@ test("checks getters", () => {
expect(z.string().cidr().isIP).toEqual(false);
expect(z.string().cidr().isCIDR).toEqual(true);
expect(z.string().cidr().isULID).toEqual(false);
expect(z.string().cidr().isMac).toEqual(false);

expect(z.string().ulid().isEmail).toEqual(false);
expect(z.string().ulid().isURL).toEqual(false);
Expand All @@ -543,7 +551,19 @@ test("checks getters", () => {
expect(z.string().ulid().isNANOID).toEqual(false);
expect(z.string().ulid().isIP).toEqual(false);
expect(z.string().ulid().isCIDR).toEqual(false);
expect(z.string().ulid().isMac).toEqual(false);
expect(z.string().ulid().isULID).toEqual(true);

expect(z.string().mac().isEmail).toEqual(false);
expect(z.string().mac().isURL).toEqual(false);
expect(z.string().mac().isCUID).toEqual(false);
expect(z.string().mac().isCUID2).toEqual(false);
expect(z.string().mac().isUUID).toEqual(false);
expect(z.string().mac().isNANOID).toEqual(false);
expect(z.string().mac().isIP).toEqual(false);
expect(z.string().mac().isCIDR).toEqual(false);
expect(z.string().mac().isULID).toEqual(false);
expect(z.string().mac().isMac).toEqual(true);
});

test("min max getters", () => {
Expand Down Expand Up @@ -918,3 +938,39 @@ test("CIDR validation", () => {
invalidCidrs.every((ip) => cidrSchema.safeParse(ip).success === false)
).toBe(true);
});

test("MAC validation", () => {
const mac = z.string().mac();
expect(mac.safeParse("00:1A:2B:3C:4D:5E").success).toBe(true);
expect(() => mac.parse("00:1A-2B:3C:4D:5E")).toThrow();

const validMacs = [
"00:1A:2B:3C:4D:5E",
"FF:FF:FF:FF:FF:FF",
"01-23-45-67-89-AB",
"A1:B2:C3:D4:E5:F6",
"10:20:30:40:50:60",
"AA-BB-CC-DD-EE-FF",
"0a:1b:2c:3d:4e:5f",
"DE-AD-BE-EF-00-01",
"12:34:56:78:9A:BC",
"98-76-54-32-10-FF"
];

const invalidMacs = [
"00:1A-2B:3C-4D:5E",
"001A2B3C4D5E",
"00:1A:2B:3C:4D",
"00:1A:2B:3C:4D:5E:FF",
"00-1A-2B-3C-4D",
"00:1A:2B:3C:4D:GZ",
"00:1A:2B:3C:4D:5E:GG",
"123:45:67:89:AB:CD",
"00--1A:2B:3C:4D:5E",
"00:1A::2B:3C:4D:5E"
];

const macSchema = z.string().mac();
expect(validMacs.every((mac) => macSchema.safeParse(mac).success)).toBe(true);
expect(invalidMacs.every((mac) => macSchema.safeParse(mac).success)).toBe(false);
});
36 changes: 34 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,8 @@ export type ZodStringCheck =
| { kind: "ip"; version?: IpVersion; message?: string }
| { kind: "cidr"; version?: IpVersion; message?: string }
| { kind: "base64"; message?: string }
| { kind: "base64url"; message?: string };
| { kind: "base64url"; message?: string }
| { kind: "mac"; message?: string };

export interface ZodStringDef extends ZodTypeDef {
checks: ZodStringCheck[];
Expand Down Expand Up @@ -695,6 +696,11 @@ const base64urlRegex =
const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`;
const dateRegex = new RegExp(`^${dateRegexSource}$`);

// https://stackoverflow.com/questions/4260467/what-is-a-regular-expression-for-a-mac-address
// no support for mac without seperators
// enforces all seperators should be either ':' or '-'
const macRegex=/^([0-9A-Fa-f]{2}(:)){5}[0-9A-Fa-f]{2}$|^([0-9A-Fa-f]{2}(-)){5}[0-9A-Fa-f]{2}$/;

function timeRegexSource(args: { precision?: number | null }) {
// let regex = `\\d{2}:\\d{2}:\\d{2}`;
let regex = `([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d`;
Expand Down Expand Up @@ -770,6 +776,14 @@ function isValidCidr(ip: string, version?: IpVersion) {
return false;
}

function isValidMac(mac:string) {
if (macRegex.test(mac)){
return true;
}

return false;
}

export class ZodString extends ZodType<string, ZodStringDef, string> {
_parse(input: ParseInput): ParseReturnType<string> {
if (this._def.coerce) {
Expand Down Expand Up @@ -1072,7 +1086,18 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
});
status.dirty();
}
} else {
} else if (check.kind === "mac") {
if(!isValidMac(input.data)) {
ctx = this._getOrReturnCtx(input,ctx);
addIssueToContext(ctx, {
validation: "mac",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
}
else {
util.assertNever(check);
}
}
Expand Down Expand Up @@ -1150,6 +1175,10 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) });
}

mac(message?: errorUtil.ErrMessage) {
return this._addCheck({ kind: "mac", ...errorUtil.errToObj(message) });
}

datetime(
options?:
| string
Expand Down Expand Up @@ -1352,6 +1381,9 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
// base64url encoding is a modification of base64 that can safely be used in URLs and filenames
return !!this._def.checks.find((ch) => ch.kind === "base64url");
}
get isMac() {
return !!this._def.checks.find((ch)=> ch.kind === "mac")
}

get minLength() {
let min: number | null = null;
Expand Down