mcard-js
Version:
A JavaScript implementation of MCard - A data model for persistently storing content with cryptographic hashing and timestamping
301 lines (250 loc) • 10.5 kB
JavaScript
import { SafeBuffer } from '../utils/bufferPolyfill.js';
import { MCard } from './mcard.js';
import {
generateDuplicationEvent,
generateCollisionEvent
} from './event-producer.js';
import logger from '../services/logger.js';
import { DEFAULT_PAGE_SIZE, HASH_ALGORITHM_HIERARCHY, HashAlgorithm } from '../config/config_constants.js';
import HashValidator from './hash/validator.js';
console.log('Card Collection Module Loading...');
/**
* Dataclass-like Page class
*/
class Page {
constructor(pageData = {}) {
const {
items = [],
total_items = 0,
page_number = 1,
page_size = DEFAULT_PAGE_SIZE,
has_next = false,
has_previous = false,
total_pages = 0
} = pageData;
this.items = items;
this.total_items = total_items;
this.page_number = page_number;
this.page_size = page_size;
this.has_next = has_next;
this.has_previous = has_previous;
this.total_pages = total_pages;
// Add previous_page calculation
this.previous_page = has_previous ? page_number - 1 : null;
// Add next_page calculation
this.next_page = has_next ? page_number + 1 : null;
}
}
console.log('Page Class Defined:', Page);
class CardCollection {
constructor(engine) {
this.engine = engine;
this.hashValidator = new HashValidator(SafeBuffer.from(''), 'sha256');
}
add(card) {
if (card === null) {
throw new Error("Card cannot be None");
}
logger.debug(`Attempting to add card with content: ${card.content}`);
// Get the hash of the incoming card
const hash_value = card.hash;
// Check if a card with this hash already exists
const existing_card = this.get(hash_value);
if (existing_card) {
logger.debug(`Card with hash ${hash_value} already exists`);
// Compare content to determine if it's a duplicate or collision
const content1 = SafeBuffer.isBuffer(existing_card.content) ? existing_card.content : SafeBuffer.from(JSON.stringify(existing_card.content));
const content2 = SafeBuffer.isBuffer(card.content) ? card.content : SafeBuffer.from(JSON.stringify(card.content));
if (SafeBuffer.compare(content1, content2) === 0) {
logger.debug(`Duplicate card found with content: ${card.content}`);
// Same content = duplicate, create event and return original hash
const duplicate_event_content_str = generateDuplicationEvent(existing_card);
const duplicate_event_card = new MCard(SafeBuffer.from(duplicate_event_content_str));
this.engine.add(duplicate_event_card);
logger.debug(`Added duplicate event card with hash: ${duplicate_event_card.hash}`);
return duplicate_event_card.hash;
} else {
logger.debug(`Collision detected for card with content: ${card.content}`);
// Create collision event card and store the new card with new hash function
const collision_event_content_str = generateCollisionEvent(card, existing_card);
const contentDict = JSON.parse(collision_event_content_str);
// Determine the upgraded hash function
const currentHashFunction = card.hash_algorithm;
// Get the next hash function from the hierarchy
const upgradedFunction = HASH_ALGORITHM_HIERARCHY[currentHashFunction];
if (!upgradedFunction) {
throw new Error(`No stronger hash algorithm available for ${currentHashFunction}`);
}
// Create the collision card with the upgraded hash function
const collision_content_card = new MCard(
card.content,
upgradedFunction
);
// Verify the hash algorithm is different and stronger
if (collision_content_card.hash_algorithm === card.hash_algorithm) {
throw new Error(`Hash algorithm did not upgrade: ${card.hash_algorithm} to ${upgradedFunction}`);
}
// Verbose logging for the new card
console.log('Collision Card Details:');
console.log(`Original Hash: ${card.hash}`);
console.log(`Collision Card Hash: ${collision_content_card.hash}`);
console.log(`Original Hash Algorithm: ${card.hash_algorithm}`);
console.log(`Collision Card Hash Algorithm: ${collision_content_card.hash_algorithm}`);
// Define the expected hash lengths in bytes for each algorithm
const HASH_LENGTHS = {
'md5': 16, // 128 bits = 16 bytes
'sha1': 20, // 160 bits = 20 bytes
'sha224': 28, // 224 bits = 28 bytes
'sha256': 32, // 256 bits = 32 bytes
'sha384': 48, // 384 bits = 48 bytes
'sha512': 64 // 512 bits = 64 bytes
};
// Get the expected hash lengths for the current and upgraded algorithms
const currentHashLength = HASH_LENGTHS[currentHashFunction.toLowerCase()] || 0;
const upgradedHashLength = HASH_LENGTHS[upgradedFunction.toLowerCase()] || 0;
console.log('Hash Length Comparison (in bytes):');
console.log(`Current Algorithm (${currentHashFunction}): ${currentHashLength} bytes`);
console.log(`Upgraded Algorithm (${upgradedFunction}): ${upgradedHashLength} bytes`);
if (upgradedHashLength <= currentHashLength) {
throw new Error(`Hash algorithm upgrade did not increase hash length: ` +
`Current (${currentHashFunction}): ${currentHashLength} bytes, ` +
`Upgraded (${upgradedFunction}): ${upgradedHashLength} bytes`);
}
// Add the collision card with the upgraded hash function
this.engine.add(collision_content_card);
// Add the collision event card
const collision_event_card = new MCard(SafeBuffer.from(collision_event_content_str));
logger.debug(`Collision event: ${collision_event_content_str}`);
this.engine.add(collision_event_card);
logger.debug(`Added collision event card with hash: ${collision_event_card.hash}`);
return collision_event_card.hash;
}
}
// No existing card with this hash or content, add the new card
this.engine.add(card);
logger.debug(`Successfully added card with hash ${hash_value}`);
return hash_value;
}
get(hash_value) {
return this.engine.get(hash_value);
}
delete(hash_value) {
return this.engine.delete(hash_value);
}
get_page(page_number = 1, page_size = DEFAULT_PAGE_SIZE) {
// Validate input parameters
if (page_number < 1) {
throw new Error(`Invalid page number: ${page_number}. Page number must be >= 1.`);
}
if (page_size < 1) {
throw new Error(`Invalid page size: ${page_size}. Page size must be >= 1.`);
}
const result = this.engine.get_page(page_number, page_size);
// Explicitly calculate total pages
const total_pages = result.total_items > 0
? Math.ceil(result.total_items / page_size)
: 0;
// Throw an error if page_number is beyond total pages (but only if there are items)
if (result.total_items > 0 && page_number > total_pages) {
throw new Error(`Page number ${page_number} is beyond total pages ${total_pages}`);
}
return new Page({
...result,
total_pages: total_pages
});
}
search_by_string(search_string, page_number = 1, page_size = DEFAULT_PAGE_SIZE) {
return this.engine.search_by_string(search_string, page_number, page_size);
}
search_by_hash(hash_value, page_number = 1, page_size = DEFAULT_PAGE_SIZE) {
if (!hash_value) {
throw new Error("Hash value cannot be empty");
}
if (page_number < 1) {
throw new Error("Page number must be greater than 0");
}
if (page_size < 1) {
throw new Error("Page size must be greater than 0");
}
// Get all matching cards
const matching_cards = [];
for (const card of this.engine.get_all().items) {
if (String(card.hash) === hash_value) {
matching_cards.push(card);
}
}
// Calculate pagination
const total_items = matching_cards.length;
const start_idx = (page_number - 1) * page_size;
const end_idx = start_idx + page_size;
const items = matching_cards.slice(start_idx, end_idx);
// Create page object
return new Page({
items: items,
total_items: total_items,
page_number: page_number,
page_size: page_size,
has_next: end_idx < total_items,
has_previous: page_number > 1
});
}
search_by_content(search_string, page_number = 1, page_size = DEFAULT_PAGE_SIZE) {
if (!search_string) {
throw new Error("Search string cannot be empty");
}
if (page_number < 1) {
throw new Error("Page number must be greater than 0");
}
if (page_size < 1) {
throw new Error("Page size must be greater than 0");
}
// Delegate to engine's search method
return this.engine.search_by_content(search_string, page_number, page_size);
}
/**
* Update the content of an existing card
* @param {string} hash - Hash of the card to update
* @param {any} newContent - New content for the card
* @returns {boolean} Whether the update was successful
*/
update(hash, newContent) {
if (!hash) {
throw new Error("Hash cannot be empty");
}
// Check if the card exists
const existingCard = this.get(hash);
if (!existingCard) {
return false;
}
try {
// Update the card in the engine
return this.engine.update(hash, newContent);
} catch (error) {
console.error(`Error updating card ${hash}:`, error);
return false;
}
}
clear() {
this.engine.clear();
}
count() {
return this.engine.count();
}
get_all(page_number = 1, page_size = DEFAULT_PAGE_SIZE) {
const result = this.engine.get_all(page_number, page_size);
return new Page({
items: result.items,
total_items: result.total_items,
page_number: result.page_number || page_number,
page_size: result.page_size || page_size,
has_next: result.has_next || false,
has_previous: result.has_previous || false
});
}
// Alias for get_all for backward compatibility
get_all_cards(page_number = 1, page_size = DEFAULT_PAGE_SIZE) {
return this.get_all(page_number, page_size);
}
}
console.log('Card Collection Module Loaded.');
export { CardCollection, Page };