Skip to content

Commit

Permalink
Allow load as Map for Hashes + Omit encoding for Strings while dumping
Browse files Browse the repository at this point in the history
  • Loading branch information
Nuri Yuri committed Dec 23, 2023
1 parent 9613fc5 commit 3d530a7
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 14 deletions.
51 changes: 51 additions & 0 deletions lib/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export type MarshalLoadConfig = {
hashToJS: 'legacy' | 'map';
};

export type MarshalDumpConfig = {
/* Omit encoding for RMXP compatibility */
omitStringEncoding: boolean;
};

export type MarshalConfig = Readonly<{
load: Readonly<MarshalLoadConfig>;
dump: Readonly<MarshalDumpConfig>;
}>;

let config: MarshalConfig = {
load: {
hashToJS: 'legacy',
},
dump: {
omitStringEncoding: false,
},
};

export const getMarshalConfig = () => config;
export const getMarshalDumpConfig = () => config.dump;
export const getMarshalLoadConfig = () => config.load;

export const setMarshalConfig = (newConfig: { load: MarshalLoadConfig; dump: MarshalDumpConfig }) => {
config = JSON.parse(JSON.stringify(newConfig));
};

export const setMarshalDumpConfig = (newConfig: MarshalDumpConfig) => {
config = {
...config,
dump: JSON.parse(JSON.stringify(newConfig)),
};
};

export const setMarshalLoadConfig = (newConfig: MarshalLoadConfig) => {
config = {
...config,
load: JSON.parse(JSON.stringify(newConfig)),
};
};

export const mergeConfig = (config: Partial<MarshalConfig>): MarshalConfig => {
return {
...getMarshalConfig(),
...JSON.parse(JSON.stringify(config)),
};
};
10 changes: 10 additions & 0 deletions lib/dump/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { dump } from '.';
import { load } from '../load';
import { MarshalObject, MarshalStandardObject } from '../types';

// str.each_char.each_slice(2).map { |s| s.join.to_i(16).chr }.join
Expand All @@ -12,6 +13,15 @@ describe('Marshal', () => {
expect(dump(['test', 'tést'])).toEqual(Buffer.from('04085b0749220974657374063a06455449220a74c3a97374063b0054', 'hex'));
});

it('dumps string and symbol without encoding (RMXP) with no issue', () => {
// It's a RMXP shit issue, I don't have time to convince Ruby to make me an example buffer without encoding tag
expect(load(dump(['tést', 'test', Symbol.for('tést')], { omitStringEncoding: true }))).toEqual(['tést', 'test', Symbol.for('tést')]);
expect(dump(['tést', 'test', Symbol.for('tést')], { omitStringEncoding: true })).toEqual(
dump(['tést', 'test', Symbol.for('tést')], { omitStringEncoding: true }),
);
expect(dump(['tést', 'test', Symbol.for('tést')], { omitStringEncoding: true })).not.toEqual(dump(['tést', 'test', Symbol.for('tést')]));
});

it('dumps floats', () => {
expect(dump([1.5, -500.3, NaN, Infinity, -Infinity])).toEqual(
Buffer.from('04085b0a6608312e35660b2d3530302e3366086e616e6608696e6666092d696e66', 'hex'),
Expand Down
4 changes: 3 additions & 1 deletion lib/dump/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { marshalDumpRegexp } from './regexp';
import { MarshalDumpContext, w_byte } from './r_helpers';
import { marshalDumpString } from './strings';
import { marshalDumpSymbol } from './symbol';
import { MarshalDumpConfig, mergeConfig } from '../config';

const marshalDump = (context: MarshalDumpContext, object: MarshalObject) => {
if (object === null) return w_byte(context, 48);
Expand All @@ -53,14 +54,15 @@ const marshalDump = (context: MarshalDumpContext, object: MarshalObject) => {
throw new MarshalError(`Cannot dump ${object}`);
};

export const dump = (object: MarshalObject): Buffer => {
export const dump = (object: MarshalObject, config?: MarshalDumpConfig): Buffer => {
const buffer = Buffer.allocUnsafe(64);
const context: MarshalDumpContext = {
buffer,
length: 0,
objects: [],
symbols: [],
marshalDump,
config: mergeConfig({ dump: config }),
};

w_byte(context, 4);
Expand Down
2 changes: 2 additions & 0 deletions lib/dump/r_helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MarshalConfig } from '../config';
import type { MarshalObject } from '../types';

export type MarshalDumpContext = {
Expand All @@ -6,6 +7,7 @@ export type MarshalDumpContext = {
objects: unknown[];
symbols: symbol[];
marshalDump: (context: MarshalDumpContext, object: MarshalObject) => void;
config: MarshalConfig;
};

export const expandBuffer = (context: MarshalDumpContext, needed: number) => {
Expand Down
4 changes: 4 additions & 0 deletions lib/dump/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const w_string = (context: MarshalDumpContext, object: string) => {

export const marshalDumpString = (context: MarshalDumpContext, object: string) => {
w_remember(context, object);
if (context.config.dump.omitStringEncoding) {
w_string(context, object);
return;
}
// Add IVAR to specify it contains UTF-8 chars
w_byte(context, 73);
w_string(context, object);
Expand Down
4 changes: 4 additions & 0 deletions lib/dump/symbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const marshalDumpSymbol = (context: MarshalDumpContext, object: symbol) =
const name = Symbol.keyFor(object);
if (!name) throw new MarshalError(`${object.toString()} is unknown to the JS realm.`);
if (name.match(/^[a-z0-9_@]+$/i) !== null) return w_symbol(context, name);
if (context.config.dump.omitStringEncoding) {
w_symbol(context, name);
return;
}
// Add IVAR to specify it contains UTF-8 chars
w_byte(context, 73);
w_symbol(context, name);
Expand Down
6 changes: 5 additions & 1 deletion lib/load/extended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ export const marshalLoadExtended = (context: MarshalContext, extMod: ReturnType<
// TODO: use guard clause for typed objects
// Note: in JS we can't extend strings, floats, bigInt etc... with modules
if (typeof object === 'object' && object != null) {
(object as Record<string, unknown>).__extendedModules = extMod;
if (object instanceof Map) {
object.set('__extendedModules', extMod);
} else {
(object as Record<string, unknown>).__extendedModules = extMod;
}
}
return object;
}
Expand Down
32 changes: 27 additions & 5 deletions lib/load/hash.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { MarshalError } from '../errors';
import type { MarshalHash } from '../types';
import type { MarshalHash, MarshalObject } from '../types';
import { MarshalContext, r_long, r_entry } from './r_helper';
import { r_object } from './withSubContext';

export const marshalLoadHash = (context: MarshalContext): MarshalHash => {
let length = r_long(context);
if (length < 0) throw new MarshalError(`Negative length are not allowed for hashes, given length: ${length}`);
const marshalLoadHashAsMap = (context: MarshalContext, length: number) => {
const hash: Map<MarshalObject, MarshalObject> = r_entry(context, new Map());
while (length--) {
const key = r_object(context);
const value = r_object(context);
hash.set(key, value);
}
return hash;
};

const marshalLoadHashAsObject = (context: MarshalContext, length: number) => {
const hash: MarshalHash = r_entry(context, { __class: 'Hash' });
while (length--) {
const key = r_object(context);
Expand All @@ -31,8 +39,22 @@ export const marshalLoadHash = (context: MarshalContext): MarshalHash => {
return hash;
};

export const marshalLoadHash = (context: MarshalContext): MarshalHash | Map<MarshalObject, MarshalObject> => {
let length = r_long(context);
if (length < 0) throw new MarshalError(`Negative length are not allowed for hashes, given length: ${length}`);
if (context.config.load.hashToJS === 'map') {
return marshalLoadHashAsMap(context, length);
} else {
return marshalLoadHashAsObject(context, length);
}
};

export const marshalLoadHashDef = (context: MarshalContext) => {
const hash = marshalLoadHash(context);
hash.__default = r_object(context);
if (hash instanceof Map) {
hash.set('__default', r_object(context));
} else {
hash.__default = r_object(context);
}
return hash;
};
47 changes: 47 additions & 0 deletions lib/load/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { load } from '.';
import { isMarshalExtendableObject } from '../typeGuards';
import { MarshalObject } from '../types';

// Marshal.dump().each_byte.map { |i| sprintf('%02x', i) }.join
describe('Marshal', () => {
Expand Down Expand Up @@ -92,6 +93,52 @@ describe('Marshal', () => {
});
});

it('loads hashes as Map', () => {
expect(load(Buffer.from('04087b073a0661690049220662063a0645546906', 'hex'), undefined, { hashToJS: 'map' })).toEqual(
new Map<MarshalObject, MarshalObject>([
[Symbol.for('a'), 0],
['b', 1],
]),
);
});

it('loads hashes as Map with default value as', () => {
expect(load(Buffer.from('04087d073a0661690049220662063a0645546906693c', 'hex'), undefined, { hashToJS: 'map' })).toEqual(
new Map<MarshalObject, MarshalObject>([
['__default', 55],
[Symbol.for('a'), 0],
['b', 1],
]),
);
});

it('loads hashes as Map with instance variables', () => {
expect(load(Buffer.from('0408497b073a0661690049220662063a0645546906063a074061690a', 'hex'), undefined, { hashToJS: 'map' })).toEqual(
new Map<MarshalObject, MarshalObject>([
[Symbol.for('a'), 0],
['b', 1],
['@a', 5],
]),
);
});

it('loads extended hashes as Map', () => {
expect(load(Buffer.from('0408653a0e457874656e73696f6e7b063a06616900', 'hex'), undefined, { hashToJS: 'map' })).toEqual(
new Map<MarshalObject, MarshalObject>([
[Symbol.for('a'), 0],
[
'__extendedModules',
[
{
__class: 'Module',
name: 'Extension',
},
],
],
]),
);
});

it('loads standard object', () => {
expect(load(Buffer.from('04086f3a10506f696e744f626a656374073a07407869093a074079690a', 'hex'))).toEqual({
__class: Symbol.for('PointObject'),
Expand Down
14 changes: 12 additions & 2 deletions lib/load/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MarshalLoadConfig, mergeConfig } from '../config';
import { MarshalError } from '../errors';
import type { MarshalObject } from '../types';
import { marshalLoadArray } from './array';
Expand Down Expand Up @@ -71,14 +72,23 @@ const marshalLoad = (context: MarshalContext): MarshalObject => {
}
};

export const load = (buffer: Buffer, map?: MarshalContext['map']): MarshalObject => {
export const load = (buffer: Buffer, map?: MarshalContext['map'], config?: MarshalLoadConfig): MarshalObject => {
if (!buffer || buffer.length < 3) throw new MarshalError('marshal data too short'); // Smallest Marshal buffer is of size 3

// Check version
const major = buffer.readUInt8(0);
const minor = buffer.readUInt8(1);
if (major !== 4 && minor !== 8) throw new MarshalError(`format version 4.8 required; ${major}.${minor} given`);

const context: MarshalContext = { buffer, index: 2, symbols: [], objects: [], ivar: false, marshalLoad, map };
const context: MarshalContext = {
buffer,
index: 2,
symbols: [],
objects: [],
ivar: false,
marshalLoad,
map,
config: mergeConfig({ load: config }),
};
return marshalLoad(context);
};
2 changes: 2 additions & 0 deletions lib/load/r_helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MarshalConfig } from '../config';
import { MarshalError } from '../errors';
import type { MarshalObject } from '../types';

Expand All @@ -9,6 +10,7 @@ export type MarshalContext = {
ivar: boolean;
marshalLoad: (context: MarshalContext) => MarshalObject;
map?: (object: MarshalObject) => MarshalObject;
config: MarshalConfig;
};

export const r_byte = (context: MarshalContext): number => {
Expand Down
13 changes: 9 additions & 4 deletions lib/load/r_ivar.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { MarshalError } from '../errors';
import { isMarshalExtendableObject } from '../typeGuards';
import type { MarshalExtendableObject } from '../types';
import type { MarshalExtendableObject, MarshalObject } from '../types';
import { MarshalContext, r_long } from './r_helper';
import { r_symbol } from './r_symbol';
import { r_object, withSubContext } from './withSubContext';

export const r_ivar = (context: MarshalContext, object: MarshalExtendableObject) => {
export const r_ivar = (context: MarshalContext, object: MarshalExtendableObject | Map<MarshalObject, MarshalObject>) => {
let length = r_long(context);
if (length < 0) throw new MarshalError(`Negative length are not allowed for IVAR, given length: ${length}`);
if (length === 0) return;
Expand All @@ -17,14 +17,19 @@ export const r_ivar = (context: MarshalContext, object: MarshalExtendableObject)
if (!field) throw new MarshalError(`${sym.toString()} is unknown to the JS realm.`);
const val = r_object(context);
// Note we're skipping the encoding stuff because it shouldn't be called with regexp or symbols
object[field] = val;
if (object instanceof Map) {
object.set(field, val);
} else {
object[field] = val;
}
} while (--length > 0);
};

export const marshalLoadIvar = (context: MarshalContext) => {
return withSubContext(context, true, (subContext) => {
const object = subContext.marshalLoad(subContext);
if (subContext.ivar && isMarshalExtendableObject(object)) withSubContext(subContext, false, (ivarContext) => r_ivar(ivarContext, object));
if (subContext.ivar && (isMarshalExtendableObject(object) || object instanceof Map))
withSubContext(subContext, false, (ivarContext) => r_ivar(ivarContext, object));

return object;
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"bin": "dist/index.js",
"main": "dist/index.js",
"name": "ts-marshal",
"version": "0.0.10",
"version": "0.0.11",
"description": "Typescript library to deserialize Ruby Marshal-i-zed objects",
"author": {
"email": "dont-email@communityscriptproject.com",
Expand Down

0 comments on commit 3d530a7

Please sign in to comment.