-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathdata-processing.ts
More file actions
199 lines (191 loc) · 6.28 KB
/
Copy pathdata-processing.ts
File metadata and controls
199 lines (191 loc) · 6.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import {Interval, evaluateExpression} from 'sonic-weave';
import {version} from './package.json';
const serverSonicWeaveVersion = evaluateExpression('VERSION', false) as string;
/**
* Validates an input as a string and enforces a maximum length.
*
* @param str - Value to validate.
* @param maxLength - Maximum allowed length in characters.
* @returns The same string when validation succeeds.
* @throws {Error} If the value is not a string or exceeds `maxLength`.
*/
function validateString(str: string, maxLength = 255) {
if (typeof str !== 'string') {
throw new Error('Not a string');
}
if (str.length > maxLength) {
throw new Error('String too long');
}
return str;
}
/**
* Validates an input as a number.
*
* @param n - Value to validate.
* @returns The same number when validation succeeds.
* @throws {Error} If the value is not a number.
*/
function validateNumber(n: number, fieldName: string) {
if (typeof n !== 'number') {
throw new Error(`${fieldName} is not a number`);
}
return n;
}
/**
* Validates an input as a non-empty array of numbers.
*
* @param ns - Value to validate.
* @returns The same number array when validation succeeds.
* @throws {Error} If the value is not an array of numbers.
*/
function validateNumberArray(ns: number[], fieldName: string) {
if (!Array.isArray(ns)) {
throw new Error(`${fieldName} must be an array`);
}
if (!ns.length) {
throw new Error(`${fieldName} cannot be empty`);
}
for (let i = 0; i < ns.length; ++i) {
validateNumber(ns[i], `${fieldName}[${i}]`);
}
return ns;
}
/**
* Validates an input as a boolean.
*
* @param b - Value to validate.
* @returns The same boolean when validation succeeds.
* @throws {Error} If the value is not a boolean.
*/
function validateBoolean(b: boolean) {
if (typeof b !== 'boolean') {
throw new Error('Not a boolean');
}
return b;
}
/**
* Validates an incoming scale payload before it is persisted.
*
* @param data - Parsed request payload object.
* @returns `true` when validation succeeds.
* @throws {Error} If any required field is missing or malformed.
*/
export function validatePayload(data: any) {
// == Scale ==
const scaleStore = data.scale;
const scale = scaleStore.scale;
if (scale.type !== 'ScaleWorkshopScale') {
throw new Error('Invalid scale data');
}
for (const ratio of scale.intervalRatios) {
validateNumber(ratio, 'scale.intervalRatios[i]');
}
for (const color of scaleStore.colors) {
validateString(color);
}
for (const label of scaleStore.labels) {
validateString(label);
}
validateNumber(scale.baseFrequency, 'scale.baseFrequency');
validateNumber(scale.baseMidiNote, 'scale.baseMidiNote');
validateString(scale.title, 4095);
Interval.reviver('relativeIntervals', data.scale.relativeIntervals);
validateString(scaleStore.name, 4095);
validateString(scaleStore.sourceText, 65535);
validateString(scaleStore.error);
validateString(scaleStore.warning);
validateString(scaleStore.keyboardMode);
validateString(scaleStore.autoColors);
validateString(scaleStore.pianoMode);
validateString(scaleStore.accidentalColor);
validateString(scaleStore.lowAccidentalColor);
validateString(scaleStore.middleAccidentalColor);
validateString(scaleStore.highAccidentalColor);
validateNumber(scaleStore.userBaseFrequency, 'scaleStore.userBaseFrequency');
validateBoolean(scaleStore.autoFrequency);
validateNumberArray(
scaleStore.isomorphicVertical,
'scaleStore.isomorphicVertical',
);
validateNumberArray(
scaleStore.isomorphicHorizontal,
'scaleStore.isomorphicHorizontal',
);
validateNumber(scaleStore.equaveShift, 'scaleStore.equaveShift');
validateNumber(scaleStore.degreeShift, 'scaleStore.degreeShift');
if (scaleStore.latticeIntervals !== null) {
Interval.reviver('latticeIntervals', scaleStore.latticeIntervals);
}
// == Audio ==
const audio = data.audio;
validateNumber(audio.mainVolume);
if (audio.mainVolume < 0 || audio.mainVolume > 1) {
throw new Error('Invalid main volume');
}
validateNumber(audio.sustainLevel);
if (audio.sustainLevel < 0 || audio.sustainLevel > 1) {
throw new Error('Invalid sustain level');
}
validateNumber(audio.pingPongGain);
if (audio.pingPongGain < 0 || audio.pingPongGain > 1) {
throw new Error('Invalid ping pong gain');
}
validateNumber(audio.pingPongFeedback);
const fb = Math.abs(audio.pingPongFeedback);
if (fb > 1) {
throw new Error('Invalid ping pong feedback');
}
validateString(audio.waveform);
validateString(audio.aperiodicWaveform);
validateString(audio.synthType);
validateNumber(audio.attackTime, 'audio.attackTime');
validateNumber(audio.decayTime, 'audio.decayTime');
validateNumber(audio.releaseTime, 'audio.releaseTime');
validateNumber(audio.stackSize, 'audio.stackSize');
validateNumber(audio.spread, 'audio.spread');
if ('audioDelay' in audio) {
// Legacy/test support
validateNumber(audio.audioDelay, 'audio.audioDelay');
}
validateNumber(audio.pingPongDelayTime, 'audio.pingPongDelayTime');
validateNumber(audio.pingPongSeparation, 'audio.pingPongSeparation');
return true;
}
/**
* Sanitizes and validates client envelope metadata.
*
* @param data - Raw envelope metadata supplied by the client.
* @returns A server-augmented envelope object ready for storage.
* @throws {Error} If envelope fields fail validation.
*/
export function cleanAndValidateEnvelope(data: any) {
const envelope: any = {
serverVersion: version,
serverSonicWeaveVersion,
serverMsSince1970: new Date().valueOf(),
};
envelope.clientVersion = validateString(data.version);
envelope.clientSonicWeaveVersion = validateString(data.sonicWeaveVersion);
envelope.clientMsSince1970 = validateNumber(
data.msSince1970,
'data.msSince1970',
);
if (data.navigator) {
envelope.navigator = {};
envelope.navigator.userAgent = validateString(
data.navigator.userAgent,
1023,
);
envelope.navigator.language = validateString(data.navigator.language);
if (!Array.isArray(data.navigator.languages)) {
throw new Error('Not an array');
}
envelope.navigator.languages = data.navigator.languages.map((l: string) =>
validateString(l),
);
}
if (data.userUUID) {
envelope.userUUID = validateString(data.userUUID);
}
return envelope;
}