Skip to content

Instantly share code, notes, and snippets.

@davidmurdoch
Created March 5, 2024 21:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidmurdoch/fe0eb0d3ee6e3e398f7c47bf07590d95 to your computer and use it in GitHub Desktop.
Save davidmurdoch/fe0eb0d3ee6e3e398f7c47bf07590d95 to your computer and use it in GitHub Desktop.
A TypeScript generator function that parses the given ini file (as a buffer) into sections, keys, and values (not tested, just for fun)
/**
* Enum representing the possible states of the parser.
*/
enum State {
Start,
Key,
Value,
Comment,
}
// Define constants for ASCII values of special characters
const openBracket = 0x5b; // [
const closeBracket = 0x5d; // ]
const equalSign = 0x3d; // =
const newline = 0x0a; // \n
const space = 0x20; // ' '
const carriageReturn = 0x0d; // \r
const tab = 0x09; // \t
const backslash = 0x5c; // \
const semicolon = 0x3b; // ;
const hash = 0x23; // #
/**
* A set of whitespace ASCII values for easy reference in trimming operations.
*/
const whitespace = new Set([newline, space, tab, carriageReturn]);
/**
* Trims whitespace characters from both ends of a buffer.
* @param buf - The buffer to trim.
* @returns A new buffer trimmed of leading and trailing whitespace characters.
*/
function trimBuffer(buf: Buffer): Buffer {
let start = 0,
end = buf.length - 1;
// Find the first non-whitespace character from the start
while (start <= end && whitespace.has(buf[start])) start++;
// Find the first non-whitespace character from the end
while (end >= start && whitespace.has(buf[end])) end--;
// Create a new buffer from the determined start and end
return buf.subarray(start, end + 1);
}
/**
* Generator function that parses the given buffer into sections, keys, and values.
* @param data - The buffer containing the data to parse.
* @yields A tuple containing the section name (if any), key, and value.
*/
export function* parse(data: Buffer) {
// initialize parsing state
let state: State = State.Start;
let sectionName: Buffer | null = null;
let partStart = -1;
const current = { key: null as Buffer | null, value: [] as Buffer[] };
/**
* Resets the current state and prepares the key-value data for yielding.
* @returns {[Buffer | null, Buffer, Buffer | null]} The section name, key, and value to yield.
*/
function emit(): [Buffer | null, Buffer, Buffer | null] {
const key = current.key!;
let value: Buffer | null = null;
if (current.value.length === 1) {
value = trimBuffer(current.value[0]);
} else if (current.value.length > 1) {
value = trimBuffer(Buffer.concat(current.value));
}
current.key = null;
current.value.length = 0;
partStart = -1;
return [sectionName, key, value];
}
// Parse the buffer
for (let pos = 0; pos < data.length; pos++) {
const char = data[pos];
switch (state) {
case State.Start:
if (char === openBracket) {
const endIndex = data.indexOf(closeBracket, pos);
if (endIndex !== -1) {
sectionName = data.subarray(pos + 1, endIndex);
pos = endIndex;
break;
}
}
if (char === semicolon || char === hash) {
state = State.Comment;
} else if (!whitespace.has(char)) {
partStart = pos;
state = State.Key;
}
break;
case State.Key:
if (char === equalSign) {
current.key = data.subarray(partStart, pos);
partStart = pos + 1;
state = State.Value;
} else if (char === newline || pos === data.length - 1) {
current.key = data.subarray(
partStart,
pos + (pos === data.length - 1 ? 1 : 0),
);
yield emit();
state = State.Start;
}
break;
case State.Value:
if (
char === backslash &&
pos + 1 < data.length &&
data[pos + 1] === newline
) {
current.value.push(data.subarray(partStart, pos));
pos++; // Skip the backslash and newline
partStart = pos + 1;
} else if (char === newline) {
current.value.push(data.subarray(partStart, pos));
yield emit();
state = State.Start;
}
break;
case State.Comment:
if (char === newline) {
state = State.Start;
}
break;
}
if (
state !== State.Comment &&
char !== newline &&
pos !== data.length - 1
) {
if (partStart === -1) partStart = pos;
} else if (state === State.Value && pos === data.length - 1) {
// Edge case for values ending at the end of the data
current.value.push(data.subarray(partStart, data.length));
yield emit();
}
}
// Handle any final data not yet emitted
if (state === State.Value && partStart !== -1) {
current.value.push(data.subarray(partStart));
yield emit();
}
}
@davidmurdoch
Copy link
Author

could be used like:

function maybeLoadIni(iniPath: string) {
  let data: Buffer;
  const sections = new Map<string, { declarations: Set<string>; definitions: Map<string, Buffer> }>();
  const declarations = new Set<string>();
  const definitions = new Map<string, Buffer>();
  try {
    data = readFileSync(iniPath);
  } catch (e) {
    return { sections, declarations, definitions };
  }

  for (const [sectionName, key, value] of parse(data)) {
    if (sectionName !== null) {
      let section = sections.get(sectionName.toString("utf-8"));
      if (!section){
        section = { declarations: new Set<string>(), definitions: new Map<string, Buffer>() };
        sections.set(sectionName.toString("utf-8"), section);
      }
      if (value !== null) {
        section.definitions.set(key.toString("utf-8"), value);
      } else {
        section.declarations.add(key.toString("utf-8"));
      }
    } else {
      if (value !== null) {
        definitions.set(key.toString("utf-8"), value);
      } else {
        declarations.add(key.toString("utf-8"));
      }
    }
  }
  return { sections, declarations, definitions };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment