Skip to content
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

エラーハンドリングを実装 #46

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
78 changes: 61 additions & 17 deletions src/enfa.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { Pattern, Node } from 'rerejs';
import { State } from './state';
import { EpsilonNFA, NullableTransition, Char, Atom } from './types';
import {
EpsilonNFA,
NullableTransition,
Char,
Atom,
BuildChildResult,
ErrorType,
} from './types';
import { extendAlphabet, getChars } from './char';

/**
* Thompson's construction を用いて rerejs.Pattern から ε-NFA を構築する。
*/
export function buildEpsilonNFA(pattern: Pattern): EpsilonNFA {
export function buildEpsilonNFA(pattern: Pattern): EpsilonNFA | ErrorType {
return new EpsilonNFABuilder(pattern).build();
}

Expand All @@ -23,9 +30,14 @@ class EpsilonNFABuilder {

constructor(private pattern: Pattern) {}

build(): EpsilonNFA {
let { initialState, acceptingState } = this.buildChild(this.pattern.child);
build(): EpsilonNFA | ErrorType {
const resultOfBuildChild = this.buildChild(this.pattern.child);

if (resultOfBuildChild.type === 'Error') {
return resultOfBuildChild;
}

let { initialState, acceptingState } = resultOfBuildChild;
// submatch用の処理
if (
this.pattern.child.type === 'Capture' ||
Expand Down Expand Up @@ -77,17 +89,26 @@ class EpsilonNFABuilder {
* - 初期状態 q への遷移は持たない。
* - 受理状態 f からの遷移は持たない。
*/
private buildChild(
node: Node,
): Pick<EpsilonNFA, 'initialState' | 'acceptingState'> {
private buildChild(node: Node): BuildChildResult | ErrorType {
switch (node.type) {
case 'Disjunction': {
const q0 = this.createState();
const childNFAs = node.children
const maybeChildNFAs = node.children
.filter(
(child) => child.type !== 'LineBegin' && child.type !== 'LineEnd',
)
.map((child) => this.buildChild(child));

const childNFAs: BuildChildResult[] = [];

for (const childNFA of maybeChildNFAs) {
if (childNFA.type === 'BuildChildResult') {
childNFAs.push(childNFA);
} else if (childNFA.type === 'Error') {
return { type: 'Error', error: childNFA.error };
}
}

const f0 = this.createState();
for (const childNFA of childNFAs) {
const q1 = childNFA.initialState;
Expand All @@ -97,31 +118,47 @@ class EpsilonNFABuilder {
}

return {
type: 'BuildChildResult',
initialState: q0,
acceptingState: f0,
};
}
case 'Sequence': {
// ^または$が有効的な場所にない場合、エラーを返す
node.children.forEach((child, index) => {
if (index > 0 && child.type === 'LineBegin') {
throw new Error('Illigal use LineBegin');
for (let i = 0; i < node.children.length; i++) {
if (i > 0 && node.children[i].type === 'LineBegin') {
return { type: 'Error', error: new Error('Illigal use LineBegin') };
Copy link
Owner

@n4o847 n4o847 Dec 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このコミットのものではないですが、以下のほうが良い気がしたのでついでに修正してもらいたいです……。

Suggested change
return { type: 'Error', error: new Error('Illigal use LineBegin') };
return { type: 'Error', error: new Error('Illegal use of LineBegin') };

}
if (index < node.children.length - 1 && child.type === 'LineEnd') {
throw new Error('Illigal use LineEnd');
if (
i < node.children.length - 1 &&
node.children[i].type === 'LineEnd'
) {
return { type: 'Error', error: new Error('Illigal use LineEnd') };
Copy link
Owner

@n4o847 n4o847 Dec 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return { type: 'Error', error: new Error('Illigal use LineEnd') };
return { type: 'Error', error: new Error('Illegal use of LineEnd') };

}
});
const childNFAs = node.children
}

const maybeChildNFAs = node.children
.filter(
(child) => child.type !== 'LineBegin' && child.type !== 'LineEnd',
)
.map((child) => this.buildChild(child));

const childNFAs: BuildChildResult[] = [];

for (const childNFA of maybeChildNFAs) {
if (childNFA.type === 'BuildChildResult') {
childNFAs.push(childNFA);
} else if (childNFA.type === 'Error') {
return { type: 'Error', error: childNFA.error };
}
}

if (childNFAs.length === 0) {
const q0 = this.createState();
const f0 = this.createState();
this.addTransition(q0, null, f0);
return {
type: 'BuildChildResult',
initialState: q0,
acceptingState: f0,
};
Expand All @@ -135,6 +172,7 @@ class EpsilonNFABuilder {
const f0 = childNFAs[childNFAs.length - 1].acceptingState;

return {
type: 'BuildChildResult',
initialState: q0,
acceptingState: f0,
};
Expand All @@ -152,6 +190,9 @@ class EpsilonNFABuilder {
const optional = node.type === 'Optional';
const q0 = this.createState();
const childNFA = this.buildChild(node.child);
if (childNFA.type === 'Error') {
return { type: 'Error', error: childNFA.error };
}
const f0 = this.createState();
const q1 = childNFA.initialState;
const f1 = childNFA.acceptingState;
Expand All @@ -176,6 +217,7 @@ class EpsilonNFABuilder {
}

return {
type: 'BuildChildResult',
initialState: q0,
acceptingState: f0,
};
Expand All @@ -184,7 +226,7 @@ class EpsilonNFABuilder {
case 'WordBoundary':
case 'LookAhead':
case 'LookBehind': {
throw new Error('unimplemented');
return { type: 'Error', error: new Error('Unimplemented') };
}
case 'Char':
case 'EscapeClass':
Expand All @@ -195,18 +237,20 @@ class EpsilonNFABuilder {
this.extendAlphabet(node);
this.addTransition(q0, node, f0);
return {
type: 'BuildChildResult',
initialState: q0,
acceptingState: f0,
};
}
case 'BackRef':
case 'NamedBackRef': {
throw new Error('unimplemented');
return { type: 'Error', error: new Error('Unimplemented') };
}
case 'LineBegin':
case 'LineEnd': {
const q0 = this.throughState();
return {
type: 'BuildChildResult',
initialState: q0,
acceptingState: q0,
};
Expand Down
51 changes: 23 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,28 @@ import { showMessageIDA } from './ida';
import { Message } from './types';

export function detectReDoS(src: string, flags?: string): Message {
try {
const pat = new Parser(src, flags).parse();
const enfa = buildEpsilonNFA(pat);
const nfa = eliminateEpsilonTransitions(enfa);
const rnfa = reverseNFA(nfa);
const dfa = determinize(rnfa);
const pnfa = prune(nfa, dfa);
const sccs = buildStronglyConnectedComponents(pnfa);
const dps = buildDirectProductGraphs(sccs);
const messageEDA = showMessageEDA(pnfa, dfa, dps);
if (messageEDA.status === 'Vulnerable') {
return messageEDA;
}
const tdps = buildTripleDirectProductGraphs(sccs, pnfa);
const messageIDA = showMessageIDA(pnfa, dfa, tdps);
if (messageIDA.status === 'Vulnerable') {
return messageIDA;
}
return {
status: 'Safe',
message: "Don't have EDA nor IDA",
} as Message;
} catch (e) {
if (e instanceof Error) {
return { status: 'Error', message: e.message };
} else {
return { status: 'Error', message: 'Undefined Error.' };
}
const pat = new Parser(src, flags).parse();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse は構文エラーを throw する可能性があるので try で囲ったほうがいいと思います。

const enfa = buildEpsilonNFA(pat);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildEpsilonNFA も文字集合を構築するときに throw する可能性があった気がします……。

if (enfa.type === 'Error') {
return { status: 'Error', message: enfa.error.message };
}
const nfa = eliminateEpsilonTransitions(enfa);
const rnfa = reverseNFA(nfa);
const dfa = determinize(rnfa);
const pnfa = prune(nfa, dfa);
const sccs = buildStronglyConnectedComponents(pnfa);
const dps = buildDirectProductGraphs(sccs);
const messageEDA = showMessageEDA(pnfa, dfa, dps);
if (messageEDA.status === 'Vulnerable') {
return messageEDA;
}
const tdps = buildTripleDirectProductGraphs(sccs, pnfa);
const messageIDA = showMessageIDA(pnfa, dfa, tdps);
if (messageIDA.status === 'Vulnerable') {
return messageIDA;
}
return {
status: 'Safe',
message: "Don't have EDA nor IDA",
} as Message;
}
6 changes: 6 additions & 0 deletions src/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,19 @@ function main(): void {
[String.raw`(.*|(a|a)*)`], // 枝切り1
[String.raw`(a|a)*?.*`], // 枝切り2
[String.raw`^a|b$|aa`],
[String.raw`^ab^c`], // エラー回復用
[String.raw`^$`], // 空文字に一致
];

for (const [src, flags] of sources) {
console.log(`//`, src, flags);
const pat = new Parser(src, flags).parse();
const enfa = buildEpsilonNFA(pat);
if (enfa.type === 'Error') {
// detectReDosを用いる場合、Objectが返されるがここではmessageのみ
console.error('Error Message:', enfa.error.message);
continue;
}
console.log(toDOT(enfa));
console.log(`//`, src, `eliminated`);
const nfa = eliminateEpsilonTransitions(enfa);
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,10 @@ export type Message =
status: 'Error';
message: string;
};

export type ErrorType = { type: 'Error'; error: Error };

export type BuildChildResult = { type: 'BuildChildResult' } & Pick<
EpsilonNFA,
'initialState' | 'acceptingState'
>;
Comment on lines +136 to +139
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

たぶん buildChild 限定の内部的な型だと思うので enfa.ts にあった方がいい気がします。