Skip to content

Commit 0785c0c

Browse files
committed
refactor: Optionally support murmur3 + implement capabilities negotiation
Signed-off-by: Marcel Klehr <mklehr@gmx.net>
1 parent 0367b02 commit 0785c0c

17 files changed

Lines changed: 220 additions & 57 deletions

src/lib/CachingTreeWrapper.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CachingResource, OrderFolderResource } from './interfaces/Resource'
1+
import { CachingResource, ICapabilities, IHashSettings, OrderFolderResource } from './interfaces/Resource'
22
import { Bookmark, Folder, ItemLocation } from './Tree'
33
import CacheTree from './CacheTree'
44
import Ordering from './interfaces/Ordering'
@@ -74,4 +74,12 @@ export default class CachingTreeWrapper implements OrderFolderResource<typeof It
7474
getCacheTree(): Promise<Folder<typeof ItemLocation.LOCAL>> {
7575
return this.cacheTree.getBookmarksTree()
7676
}
77+
78+
getCapabilities(): Promise<ICapabilities> {
79+
return this.innerTree.getCapabilities()
80+
}
81+
82+
setHashSettings(hashSettings: IHashSettings): void {
83+
this.innerTree.setHashSettings(hashSettings)
84+
}
7785
}

src/lib/Crypto.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { fromUint8Array, toUint8Array } from 'js-base64'
2+
import { murmurhash3_32_gc } from './murmurhash3'
23

34
export default class Crypto {
45
static iterations = 250000
56
static ivLength = 16
67

8+
static async murmurHash3(message: string): Promise<string> {
9+
const buf32 = new Uint32Array([murmurhash3_32_gc(message, 0)])
10+
const buf8 = new Uint8Array(buf32.buffer)
11+
buf8.reverse()
12+
return this.bufferToHexstr(buf8)
13+
}
14+
715
static async sha256(message: string): Promise<string> {
816
const msgBuffer = new TextEncoder().encode(message) // encode as UTF-8
917
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer) // hash the message

src/lib/Scanner.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Parallel from 'async-parallel'
22
import Diff, { ActionType, CreateAction, MoveAction, RemoveAction, ReorderAction, UpdateAction } from './Diff'
33
import { Bookmark, Folder, ItemLocation, ItemType, TItem, TItemLocation } from './Tree'
44
import Logger from './Logger'
5+
import { IHashSettings } from './interfaces/Resource'
56

67
export interface ScanResult<L1 extends TItemLocation, L2 extends TItemLocation> {
78
CREATE: Diff<L1, L2, CreateAction<L1, L2>>
@@ -15,17 +16,17 @@ export default class Scanner<L1 extends TItemLocation, L2 extends TItemLocation>
1516
private oldTree: TItem<L1>
1617
private newTree: TItem<L2>
1718
private mergeable: (i1: TItem<TItemLocation>, i2: TItem<TItemLocation>) => boolean
18-
private preserveOrder: boolean
19+
private hashSettings: IHashSettings
1920
private checkHashes: boolean
2021
private hasCache: boolean
2122

2223
private result: ScanResult<L2, L1>
2324

24-
constructor(oldTree:TItem<L1>, newTree:TItem<L2>, mergeable:(i1:TItem<TItemLocation>, i2:TItem<TItemLocation>)=>boolean, preserveOrder:boolean, checkHashes = true, hasCache = true) {
25+
constructor(oldTree:TItem<L1>, newTree:TItem<L2>, mergeable:(i1:TItem<TItemLocation>, i2:TItem<TItemLocation>)=>boolean, hashSettings: IHashSettings, checkHashes = true, hasCache = true) {
2526
this.oldTree = oldTree
2627
this.newTree = newTree
2728
this.mergeable = mergeable
28-
this.preserveOrder = preserveOrder
29+
this.hashSettings = hashSettings
2930
this.checkHashes = typeof checkHashes === 'undefined' ? true : checkHashes
3031
this.hasCache = hasCache
3132
this.result = {
@@ -136,14 +137,14 @@ export default class Scanner<L1 extends TItemLocation, L2 extends TItemLocation>
136137
}
137138

138139
async bookmarkHasChanged(oldBookmark:Bookmark<L1>, newBookmark:Bookmark<L2>):Promise<boolean> {
139-
const oldHash = await oldBookmark.hash()
140-
const newHash = await newBookmark.hash()
140+
const oldHash = await oldBookmark.hash(this.hashSettings)
141+
const newHash = await newBookmark.hash(this.hashSettings)
141142
return oldHash !== newHash
142143
}
143144

144145
async folderHasChanged(oldFolder:Folder<L1>, newFolder:Folder<L2>):Promise<boolean> {
145-
const oldHash = await oldFolder.hash(this.preserveOrder)
146-
const newHash = await newFolder.hash(this.preserveOrder)
146+
const oldHash = await oldFolder.hash(this.hashSettings)
147+
const newHash = await newFolder.hash(this.hashSettings)
147148
return oldHash !== newHash
148149
}
149150

src/lib/Tree.ts

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Crypto from './Crypto'
22
import Logger from './Logger'
3-
import TResource from './interfaces/Resource'
3+
import TResource, { IHashSettings } from './interfaces/Resource'
44
import * as Parallel from 'async-parallel'
55

66
const STRANGE_PROTOCOLS = ['data:', 'javascript:', 'about:', 'chrome:', 'file:']
@@ -37,7 +37,7 @@ export class Bookmark<L extends TItemLocation> {
3737
public tags: string[]
3838
public location: L
3939
public isRoot = false
40-
private hashValue: string
40+
private hashValue: Record<string, string>
4141

4242
constructor({ id, parentId, url, title, tags, location }: { id:string|number, parentId:string|number, url:string, title:string, tags?: string[], location: L }) {
4343
this.id = id
@@ -78,13 +78,19 @@ export class Bookmark<L extends TItemLocation> {
7878
return 0
7979
}
8080

81-
async hash():Promise<string> {
81+
async hash({preserveOrder = false, hashFn = 'sha256'}):Promise<string> {
8282
if (!this.hashValue) {
83-
this.hashValue = await Crypto.sha256(
84-
JSON.stringify({ title: this.title, url: this.url })
85-
)
83+
this.hashValue = {}
84+
const json = JSON.stringify({ title: this.title, url: this.url })
85+
if (hashFn === 'sha256') {
86+
this.hashValue[hashFn] = await Crypto.sha256(json)
87+
} else if (hashFn === 'murmur3') {
88+
this.hashValue[hashFn] = await Crypto.murmurHash3(json)
89+
} else {
90+
throw new Error('Unsupported hash function specified')
91+
}
8692
}
87-
return this.hashValue
93+
return this.hashValue[hashFn]
8894
}
8995

9096
clone(withHash?: boolean):Bookmark<L> {
@@ -119,6 +125,7 @@ export class Bookmark<L extends TItemLocation> {
119125
toJSON() {
120126
// Flatten inherited properties for serialization
121127
const result = {}
128+
// eslint-disable-next-line @typescript-eslint/no-this-alias
122129
let obj = this
123130
while (obj) {
124131
Object.entries(obj).forEach(([key, value]) => {
@@ -306,9 +313,10 @@ export class Folder<L extends TItemLocation> {
306313
return 0
307314
}
308315

309-
async hash(preserveOrder = false): Promise<string> {
310-
if (this.hashValue && typeof this.hashValue[String(preserveOrder)] !== 'undefined') {
311-
return this.hashValue[String(preserveOrder)]
316+
async hash({preserveOrder = false, hashFn = 'sha256'}: IHashSettings): Promise<string> {
317+
const cacheKey = `${preserveOrder}-${hashFn}`
318+
if (this.hashValue && typeof this.hashValue[cacheKey] !== 'undefined') {
319+
return this.hashValue[cacheKey]
312320
}
313321

314322
if (!this.loaded) {
@@ -329,17 +337,22 @@ export class Folder<L extends TItemLocation> {
329337
})
330338
}
331339
if (!this.hashValue) this.hashValue = {}
332-
this.hashValue[String(preserveOrder)] = await Crypto.sha256(
333-
JSON.stringify({
334-
title: this.title,
335-
children: await Parallel.map(
336-
children,
337-
child => child.hash(preserveOrder),
338-
1
339-
)
340-
})
341-
)
342-
return this.hashValue[String(preserveOrder)]
340+
const json = JSON.stringify({
341+
title: this.title,
342+
children: await Parallel.map(
343+
children,
344+
child => child.hash({preserveOrder, hashFn}),
345+
1
346+
)
347+
})
348+
if (hashFn === 'sha256') {
349+
this.hashValue[cacheKey] = await Crypto.sha256(json)
350+
} else if (hashFn === 'murmur3') {
351+
this.hashValue[cacheKey] = await Crypto.murmurHash3(json)
352+
} else {
353+
throw new Error('Unsupported hash function specified')
354+
}
355+
return this.hashValue[cacheKey]
343356
}
344357

345358
copy(withHash?:boolean):Folder<L> {

src/lib/adapters/Caching.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import {
1212
UnknownMoveOriginError,
1313
UnknownMoveTargetError
1414
} from '../../errors/Error'
15-
import { BulkImportResource } from '../interfaces/Resource'
15+
import { BulkImportResource, ICapabilities, IHashSettings } from '../interfaces/Resource'
1616

1717
export default class CachingAdapter implements Adapter, BulkImportResource<TItemLocation> {
1818
protected highestId: number
1919
public bookmarksCache: Folder<TItemLocation>
2020
protected server: any
2121
protected location: TItemLocation = ItemLocation.SERVER
22+
protected hashSettings: IHashSettings
2223

2324
constructor(server: any) {
2425
this.resetCache()
@@ -245,4 +246,15 @@ export default class CachingAdapter implements Adapter, BulkImportResource<TItem
245246
isAvailable(): Promise<boolean> {
246247
return Promise.resolve(true)
247248
}
249+
250+
async getCapabilities(): Promise<ICapabilities> {
251+
return {
252+
preserveOrder: true,
253+
hashFn: ['murmur3', 'sha256'],
254+
}
255+
}
256+
257+
setHashSettings(hashSettings: IHashSettings): void {
258+
this.hashSettings = hashSettings
259+
}
248260
}

src/lib/adapters/Git.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export default class GitAdapter extends CachingAdapter {
170170

171171
const status = await this.pullFromServer()
172172

173-
this.initialTreeHash = await this.bookmarksCache.hash(true)
173+
this.initialTreeHash = await this.bookmarksCache.hash(this.hashSettings)
174174

175175
Logger.log('onSyncStart: completed')
176176

@@ -189,7 +189,7 @@ export default class GitAdapter extends CachingAdapter {
189189
clearInterval(this.lockingInterval)
190190

191191
this.bookmarksCache = this.bookmarksCache.clone(false)
192-
const newTreeHash = await this.bookmarksCache.hash(true)
192+
const newTreeHash = await this.bookmarksCache.hash(this.hashSettings)
193193
if (newTreeHash !== this.initialTreeHash) {
194194
const fileContents = this.server.bookmark_file_type === 'xbel' ? createXBEL(this.bookmarksCache, this.highestId) : createHTML(this.bookmarksCache, this.highestId)
195195
Logger.log('(git) writeFile ' + this.dir + '/' + this.server.bookmark_file)

src/lib/adapters/GoogleDrive.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ export default class GoogleDriveAdapter extends CachingAdapter {
298298
this.alwaysUpload = true
299299
}
300300

301-
this.initialTreeHash = await this.bookmarksCache.hash(true)
301+
this.initialTreeHash = await this.bookmarksCache.hash(this.hashSettings)
302302

303303
Logger.log('onSyncStart: completed')
304304

@@ -335,7 +335,7 @@ export default class GoogleDriveAdapter extends CachingAdapter {
335335
clearInterval(this.lockingInterval)
336336

337337
this.bookmarksCache = this.bookmarksCache.clone(false)
338-
const newTreeHash = await this.bookmarksCache.hash(true)
338+
const newTreeHash = await this.bookmarksCache.hash(this.hashSettings)
339339

340340
if (!this.fileId) {
341341
await this.createFile(await this.getXBELContent())

src/lib/adapters/Karakeep.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Adapter from '../interfaces/Adapter'
22
import { Bookmark, Folder, ItemLocation } from '../Tree'
33
import PQueue from 'p-queue'
4-
import { IResource } from '../interfaces/Resource'
4+
import { ICapabilities, IResource } from '../interfaces/Resource'
55
import Logger from '../Logger'
66
import {
77
AuthenticationError,
@@ -565,4 +565,11 @@ export default class KarakeepAdapter implements Adapter, IResource<typeof ItemLo
565565

566566
return json
567567
}
568+
569+
async getCapabilities(): Promise<ICapabilities> {
570+
return {
571+
preserveOrder: false,
572+
hashFn: ['murmur3', 'sha256'],
573+
}
574+
}
568575
}

src/lib/adapters/Linkwarden.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Adapter from '../interfaces/Adapter'
22
import { Bookmark, Folder, ItemLocation } from '../Tree'
33
import PQueue from 'p-queue'
4-
import { IResource } from '../interfaces/Resource'
4+
import { ICapabilities, IResource } from '../interfaces/Resource'
55
import Logger from '../Logger'
66
import {
77
AuthenticationError,
@@ -367,4 +367,11 @@ export default class LinkwardenAdapter implements Adapter, IResource<typeof Item
367367

368368
return json
369369
}
370+
371+
async getCapabilities(): Promise<ICapabilities> {
372+
return {
373+
preserveOrder: false,
374+
hashFn: ['murmur3', 'sha256'],
375+
}
376+
}
370377
}

src/lib/adapters/NextcloudBookmarks.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// Nextcloud ADAPTER
2-
// All owncloud specifc stuff goes in here
32
import { Capacitor, CapacitorHttp as Http } from '@capacitor/core'
43
import Adapter from '../interfaces/Adapter'
54
import HtmlSerializer from '../serializers/Html'
@@ -12,7 +11,7 @@ import PQueue from 'p-queue'
1211
import flatten from 'lodash/flatten'
1312
import {
1413
BulkImportResource,
15-
ClickCountResource,
14+
ClickCountResource, ICapabilities, IHashSettings,
1615
LoadFolderChildrenResource,
1716
OrderFolderResource
1817
} from '../interfaces/Resource'
@@ -78,6 +77,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
7877
private ended = false
7978
private locked = false
8079
private hasFeatureJavascriptLinks: boolean = null
80+
private hashSettings: IHashSettings
8181

8282
constructor(server: NextcloudBookmarksConfig) {
8383
this.server = server
@@ -316,6 +316,9 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
316316
}
317317

318318
async _getFolderHash(folderId:string|number):Promise<string> {
319+
if (this.hashSettings.hashFn !== 'sha256') {
320+
throw new Error('Unsupported hash function: ' + this.hashSettings.hashFn + ' - Nextcloud Bookmarks only supports sha256')
321+
}
319322
return this.sendRequest(
320323
'GET',
321324
`index.php/apps/bookmarks/public/rest/v2/folder/${folderId}/hash`
@@ -363,7 +366,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
363366
return recurseChildren(folderId, children)
364367
}
365368

366-
async loadFolderChildren(folderId:string|number, all?: boolean): Promise<TItem<typeof ItemLocation.SERVER>[]> {
369+
async loadFolderChildren(folderId:string|number, hashSettings: IHashSettings, all?: boolean): Promise<TItem<typeof ItemLocation.SERVER>[]> {
367370
const folder = this.tree.findFolder(folderId)
368371
if (!folder) {
369372
throw new Error('Could not find folder for loadFolderChildren')
@@ -977,6 +980,13 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
977980
return Promise.resolve(true)
978981
}
979982

983+
async getCapabilities(): Promise<ICapabilities> {
984+
return {
985+
preserveOrder: true,
986+
hashFn: ['sha256'],
987+
}
988+
}
989+
980990
async countClick(url: string): Promise<void> {
981991
try {
982992
await this.sendRequest(
@@ -991,4 +1001,8 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
9911001
console.warn(e)
9921002
}
9931003
}
1004+
1005+
setHashSettings(hashSettings: IHashSettings): void {
1006+
this.hashSettings = hashSettings
1007+
}
9941008
}

0 commit comments

Comments
 (0)