- Table of Contents
- 0. Overview
- 1. Context Integration
- 2. Go API V1 Package Organization
- 2.1 Module Path
- 2.1.1 API Package Structure
- 2.1.2 Root Package Purpose
- 2.1.3 Root Package Example Import
- 2.1.4. File Format Package: Fileformat
- 2.1.5 File Format Package Key Types
- 2.1.6 File Format Package Example Import
- 2.1.7 Metadata Package Purpose
- 2.1.8 Metadata Package Imports
- 2.1.9. Generics Package: Generics
- 2.1.10 Generics Package Key Types
- 2.1.11 Generics Package Example Import
- 2.1.12 Error Handling Package Purpose
- 2.1.13 Error Handling Package Imports
- 2.1.14. Signatures Package: Signatures
- 2.1.15 Signatures Package Key Types
- 2.1.16 Signatures Package Example Import
- 2.1.17 Internal Package Purpose
- 2.2 Import Patterns
- 2.3 Package Aliases
- 2.1 Module Path
- 3. Package Structure and Loading
- 4. Package Format Constants
- 5. Package Lifecycle Operations
- 6. NewPackage Function
- 7. NewPackageWithOptions Function
- 8. Package.SetTargetPath Method
- 9. Package Configuration
- 10. OpenPackage Function
- 11. Opening Packages As Read-Only
- 12. OpenBrokenPackage Function
- 13. Package.Close Method
- 14. Package.CloseWithCleanup Method
- 15. Package.Validate Method
- 16. Package.Defragment Method
- 17. Package.GetInfo Method
- 18. Header Inspection
- 19. Package Session Base Management
- 20. Structured Error System
- 21. Error Handling Best Practices
- 22. Resource Management
This document defines the basic package operations for the NovusPack system, covering the fundamental lifecycle operations of creating, opening, and closing packages.
- Go API Definitions Index - Complete index of all Go API functions, types, and structures
- Core Package Interface - Package operations and compression
- Package Writing Operations - SafeWrite, FastWrite, and write strategy selection
- Package Compression API - Package compression and decompression operations
- Package Metadata API - Comment management, AppID/VendorID, and metadata operations
- File Format Specifications - .nvpk format structure and signature implementation
See Core API - Context Integration for the complete context integration specification.
The NovusPack Basic Operations API follows the same context integration patterns as the core API.
These methods assume the following imports:
import (
"context"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// FileEntry, PathMetadataEntry, PackageInfo, PackageHeader, PackageIndex from other API modulesThe NovusPack Go API is organized into multiple subpackages within api/go to provide clear separation of concerns and avoid circular dependencies.
The module path for all packages is:
github.com/novus-engine/novuspack/api/go
The API is organized into the following subpackages:
Import Path: github.com/novus-engine/novuspack/api/go
The root package provides the main public API and re-exports types from domain-specific subpackages for convenience.
- Re-exports all public types from subpackages
- Provides unified import path for most use cases
- Implements the main
Packageinterface and lifecycle operations
import "github.com/novus-engine/novuspack/api/go"- From
fileformat:PackageHeader,FileIndex,IndexEntry - From
generics:PathEntry,Option,Result,Tag,Strategy,Validator - From
metadata:PackageComment,PackageInfo,FileEntry,HashEntry,OptionalDataEntry,ProcessingState - From
signatures:Signature,SignatureInfo - From
pkgerrors:ErrorType,PackageError
Import Path: github.com/novus-engine/novuspack/api/go/fileformat
Provides structures and operations for the NovusPack binary file format, including headers, file entries, and data management.
PackageHeader- Package header structureFileIndex- File index structureIndexEntry- File index entry structure
github.com/novus-engine/novuspack/api/go/generics- ForPathEntryand generic typesgithub.com/novus-engine/novuspack/api/go/metadata- ForPathMetadataEntryassociationsgithub.com/novus-engine/novuspack/api/go/pkgerrors- For error handling
import "github.com/novus-engine/novuspack/api/go/fileformat"Import Path: github.com/novus-engine/novuspack/api/go/metadata
Provides metadata structures including package information, path metadata, directory entries, and security levels.
PackageInfo- Package metadata informationPathMetadataEntry- Path metadata with inheritance and filesystem propertiesFileEntry- FileEntry with paths, hashes, and optional dataHashEntry- Hash entry structureOptionalDataEntry- Optional data entry structureProcessingState- File processing state enumerationPackageComment- Package comment structure
github.com/novus-engine/novuspack/api/go/generics- ForPathEntry,Tag, and interface typesgithub.com/novus-engine/novuspack/api/go/pkgerrors- For error handling
Note: FileEntry, HashEntry, OptionalDataEntry, and ProcessingState are defined in the metadata package, not the fileformat package.
import "github.com/novus-engine/novuspack/api/go/metadata"Import Path: github.com/novus-engine/novuspack/api/go/generics
Provides generic types, patterns, and shared interfaces to avoid circular dependencies between domain packages.
PathEntry- Minimal path structure used by bothFileEntryandPathMetadataEntryOption[T]- Type-safe optional valuesResult[T, E]- Type-safe result typeTag[T]- Generic tag structureStrategy[T, U]- Strategy pattern interfaceValidator[T]- Validation interface
- No internal NovusPack imports (standalone package)
import "github.com/novus-engine/novuspack/api/go/generics"Import Path: github.com/novus-engine/novuspack/api/go/pkgerrors
Provides structured error handling with typed errors and validation context.
ErrorType- Error type enumerationPackageError- Structured error typeValidationErrorContext- Validation error context structure
- Standard library only (no internal NovusPack imports)
import "github.com/novus-engine/novuspack/api/go/pkgerrors"Import Path: github.com/novus-engine/novuspack/api/go/signatures
Provides digital signature structures and operations for package integrity verification. Signature management and signature validation are deferred to v2. V1 only enforces signed package immutability based on signature presence.
Signature- Digital signature structureSignatureInfo- Signature information structure
github.com/novus-engine/novuspack/api/go/pkgerrors- For error handling
import "github.com/novus-engine/novuspack/api/go/signatures"Import Path: github.com/novus-engine/novuspack/api/go/internal
Provides internal helper functions used by the main package. This package is not part of the public API and should not be imported by external code.
- Internal helper functions for file operations
- Not exported for external use
Note: This package is not part of the public API. Do not import it in external code.
For most use cases, import the root package:
import "github.com/novus-engine/novuspack/api/go"This provides access to all re-exported types and the main Package interface.
For advanced use cases or when you need access to package-specific functionality not re-exported, import subpackages directly:
import (
"github.com/novus-engine/novuspack/api/go/fileformat"
"github.com/novus-engine/novuspack/api/go/metadata"
"github.com/novus-engine/novuspack/api/go/pkgerrors"
)The package structure is designed to avoid circular dependencies:
genericspackage has no internal NovusPack imports and provides shared interfacespkgerrorspackage has no internal NovusPack importsmetadatapackage contains bothFileEntryandPathMetadataEntry, which can directly reference each other since they're in the same packagesignaturesonly importspkgerrors
The following diagram shows the dependency relationships between packages:
novuspack (root)
├── fileformat
│ ├── generics (shared types and interfaces)
│ └── pkgerrors
├── metadata
│ ├── generics (shared types and interfaces)
│ │ └── Contains: FileEntry, HashEntry, OptionalDataEntry, ProcessingState
│ └── pkgerrors
├── signatures
│ └── pkgerrors
├── generics (standalone, no internal imports)
└── pkgerrors (standalone, no internal imports)
When importing multiple packages, use aliases to avoid naming conflicts:
import (
novuspack "github.com/novus-engine/novuspack/api/go"
fileformat "github.com/novus-engine/novuspack/api/go/fileformat"
metadata "github.com/novus-engine/novuspack/api/go/metadata"
)This section describes the internal structure of packages and how they are loaded from disk.
The NovusPack API uses an interface-based design where the Package interface is implemented by the concrete filePackage struct.
The Package interface provides the public API for package operations, while filePackage contains the actual implementation.
The canonical Package interface definition is specified in Core Package Interface API - Package Interface.
The Package interface provides a unified API that combines read operations, write operations, lifecycle operations, file management, metadata operations, compression operations, and session base management.
For session base management (used for both file addition and extraction operations), see Package Session Base Management.
When a package is opened, the following initialization sequence occurs:
- Load package header - Validates magic number and version
- Load package info - Retrieves metadata (comment, VendorID, AppID)
- Load file entries - Reads file index and entry structures
- Load special metadata files - Processes special file types (65000-65535)
- Load path metadata - Parses path structure from YAML
- Update file-path associations - Links files to their path metadata
The package opening functions are:
- OpenPackage - Opens an existing package from disk and validates the package structure during open
- OpenPackageReadOnly - Opens a package in read-only mode, enforcing read-only behavior for both in-memory modifications and writes to disk
- OpenBrokenPackage - Opens a package that may be invalid or partially corrupted, intended for repair workflows
The package loading process uses internal methods to complete initialization:
- loadSpecialMetadataFiles - Loads all special metadata files (file types 65000-65535)
- loadPathMetadata - Loads path metadata from special files and parses YAML structure
- updateFilePathAssociations - Links file entries to their corresponding path metadata entries
// loadSpecialMetadataFiles loads all special metadata files
// Returns *PackageError on failure
func (p *Package) loadSpecialMetadataFiles(ctx context.Context) errorThis method loads all special metadata files (file types 65000-65535) from the package.
// loadPathMetadata loads path metadata from special files
// Returns *PackageError on failure
func (p *Package) loadPathMetadata(ctx context.Context) errorThis method loads path metadata from special files and parses the YAML structure.
// updateFilePathAssociations links files to their path metadata
// Returns *PackageError on failure
func (p *Package) updateFilePathAssociations(ctx context.Context) errorThis method links file entries to their corresponding path metadata entries.
This section documents the internal implementation details of the NovusPack package system, including data loading strategies, state management, memory management, and resource lifecycle.
Note: The canonical filePackage struct definition is specified in Core Package Interface API - filePackage Implementation.
The NovusPack API uses an interface-based design where the Package interface is implemented by the concrete filePackage struct.
See Core Package Interface API - filePackage Implementation for the complete struct definition and field descriptions.
The NovusPack implementation uses a hybrid loading strategy that balances memory efficiency with performance.
The following data is loaded immediately when a package is opened:
- Package header: Validated and loaded from file start.
- File index: Loaded from header.IndexStart offset.
- Package info: Populated from header and index data.
The following data is loaded only when needed:
-
File entries: FileEntries slice is initially empty.
- Individual entries are loaded when file operations require them.
- Entries are located using the index (FileID => Offset mapping).
- This minimizes memory usage for packages with many files.
-
Path metadata: PathMetadataEntries slice is initially empty.
- Loaded when path metadata operations are performed.
- Loaded from special metadata files (type 65001).
- Parsed from YAML format.
File entries are loaded when:
- File read operations are performed.
- File metadata queries require entry data.
- File-path associations are updated.
- Package validation requires entry inspection.
Path metadata is loaded when:
- Path metadata queries are performed.
- Tag inheritance operations require path hierarchy.
- File-path associations are updated.
- Path metadata management operations are called.
- Large packages can be opened without loading all file entries into memory.
- Memory usage scales with actual usage, not package size.
- File handles remain open for efficient on-demand reading.
- Resources are released when Close() is called.
The implementation does not track a separate "Created" state as a dedicated field. Instead, state is derived from a small set of fields and invariants.
The only explicit lifecycle flag is isOpen.
Additional state is inferred from whether the package has an associated FilePath.
Additional state is inferred from whether a file handle exists.
Additional state is inferred from whether metadata caches are still present in memory.
Lifecycle state definitions are expressed as predicates over fields.
-
New (unconfigured): Package returned by NewPackage(), not yet associated with a file.
- Predicate:
FilePath == "". - Predicate:
isOpen == false. - Notes:
Infois initialized with default values but is not considered loaded from a package file.
- Predicate:
-
Open (configured, in-memory): Package has been configured via NewPackageWithOptions().
- Predicate:
FilePath != "". - Predicate:
isOpen == true. - Predicate:
fileHandle == nil. - Notes: No on-disk package is opened for reading.
- Predicate:
-
Open (file-backed): Package has been opened via OpenPackage().
- Predicate:
FilePath != "". - Predicate:
isOpen == true. - Predicate:
fileHandle != nil. - Notes: Header/index and required metadata caches are loaded as specified by OpenPackage eager loading requirements.
- Predicate:
-
Closed (cached): Package has been closed via Close(), but cached metadata may remain.
- Predicate:
isOpen == false. - Predicate:
fileHandle == nil. - Notes: Pure in-memory read operations MAY be allowed if required metadata caches remain in memory.
- The
isOpenflag being false is sufficient to indicate the package is closed.
- Predicate:
-
Closed (cleaned): Package has been closed via CloseWithCleanup(), and caches are cleared.
- Predicate:
isOpen == false. - Predicate:
fileHandle == nil. - Predicate: metadata caches required by pure in-memory reads have been cleared.
- Notes: Pure in-memory read operations MUST fail in this state.
- The
isOpenflag being false is sufficient to indicate the package is closed.
- Predicate:
Valid state transitions:
- NewPackage() => New state.
- New state => NewPackageWithOptions() => Open (configured, in-memory) state.
- Open (configured, in-memory) state => Close() => Closed (cached) state.
- Open (configured, in-memory) state => CloseWithCleanup() => Closed (cleaned) state.
- OpenPackage() => Open (file-backed) state.
- Open (file-backed) state => Close() => Closed (cached) state.
- Open (file-backed) state => CloseWithCleanup() => Closed (cleaned) state.
- Any state => Close() => Closed (cached) state (idempotent).
- GetInfo(): Available in Open states, and in Closed (cached) state if metadata remains in memory.
- GetMetadata(): Available in Open states, and in Closed (cached) state if metadata remains in memory.
- ListFiles(): Available in Open states, and in Closed (cached) state if metadata remains in memory.
- Validate(): Available only in Open (file-backed) state.
- ReadFile(): Available only in Open (file-backed) state.
- AddFile(), AddFileFromMemory(), RemoveFile(): Available in Open states.
- Close(): Available in any state (idempotent).
- CloseWithCleanup(): Available in any state.
The implementation validates state before operations:
- Closed (cleaned) state: Pure in-memory read operations MUST return validation errors.
- Closed (cached) state: I/O operations MUST return validation errors.
- Closed (cached) state: Pure in-memory read operations MAY succeed if metadata remains cached after Close().
- Open (file-backed) state: Required for I/O read operations and validation operations.
- Open states: Required for in-memory write operations (for example AddFile(), RemoveFile()).
The implementation uses several strategies to manage memory efficiently.
File entries are not loaded into memory until needed. This allows opening very large packages without excessive memory usage.
- File handles are closed when Close() is called.
- Memory allocated for cached metadata MAY remain after Close() to support in-memory read operations.
- CloseWithCleanup() clears in-memory caches (slices and maps) as part of cleanup.
For packages that don't fit in memory:
- File entries are read from disk as needed.
- File data is streamed rather than loaded entirely.
- Index provides efficient random access to entries.
- File handles remain open for efficient I/O.
- Slices are pre-allocated with appropriate capacity when size is known.
- Maps are initialized with make() when first needed.
- FileEntry data is allocated only when LoadData() is called.
- Temporary buffers are reused where possible.
The package implementation maintains several relationships between data structures.
- Association: FileEntry.PathMetadataEntries map links paths to PathMetadataEntry instances (direct references since both types are in the metadata package).
- Bidirectional: PathMetadataEntry.AssociatedFileEntries links back to FileEntry instances.
- Matching: Associations are established by matching path strings.
- Purpose: Enables per-path tag inheritance and filesystem properties.
- Organization: Special files are organized by file type ID (65000-65535).
- Access: Direct lookup via file type ID.
- Types: Metadata files, signature files, manifest files, etc.
- Loading: Loaded during package opening.
- Navigation: Header contains index location (IndexStart, IndexSize).
- Index: Contains entry count and FileID => Offset mappings.
- Entries: Located using index offsets, read on demand.
- Flow: Header => Index => IndexEntry => FileEntry.
- Source: Aggregated from header, index, and entry data.
- Updates: Updated as package state changes.
- Caching: Cached to avoid repeated calculations.
- Access: Available via GetInfo() method.
The current implementation has specific concurrency characteristics.
- Package instances should not be shared across goroutines.
- No internal locking is performed.
- Concurrent operations on the same package instance are not safe.
- Multiple packages can be opened concurrently in different goroutines.
- Read operations on different packages are safe.
- Write operations on different packages are safe.
- No mutexes or locks are used in the current implementation.
- File I/O operations use standard Go file handles (not thread-safe).
- Package state fields are not protected by locks.
- Thread-safe operations would require adding mutexes.
- Read-write locks could enable concurrent reads.
- Per-operation locking could enable limited concurrency.
The package implementation manages several types of resources throughout its lifecycle.
- Opened: During OpenPackage() operation, file handle is opened for reading.
- Active: File handle remains open while package is in Open state.
- Closed: During Close() operation, file handle is closed.
- Cleanup: File handle is set to nil after closing.
- Initialization: Slices and maps are allocated during package creation or opening.
- Growth: Slices grow as entries are loaded on demand.
- Cleanup: Memory is released when package is closed.
- Garbage Collection: Go runtime handles final cleanup.
- isOpen: Set to true during OpenPackage(), false during Close().
- closed: Set to true during Close(), prevents further operations.
- Validation: Flags are checked before operations to ensure valid state.
- File handle errors: File handle is closed, package transitions to Closed state.
- Memory errors: Allocated resources are released, error is returned.
- State errors: Operations return validation errors, package state is preserved.
const (
// NVPKMagic is the magic number for .nvpk files
NVPKMagic = 0x4E56504B // "NVPK" in hex
// NVPKVersion is the current version of the .nvpk format
NVPKVersion = 1
// HeaderSize is the fixed size of the package header
// See: Package File Format - Package Header for authoritative definition
HeaderSize int64 = 112
)These constants define fundamental values for the NovusPack format.
NVPKMagic: Package identifier (0x4E56504B "NVPK")NVPKVersion: Current format version (1)HeaderSize: Fixed header size in bytes (see Package File Format - Package Header)
Usage: Validate package header magic number and version before processing.
The NovusPack system follows a simple lifecycle pattern:
- Create - Create a new package (in-memory)
- Open - Open an existing package
- Operations - Perform various operations (add files, metadata, etc.)
- Write - Write the package to disk
- Close - Close the package and release resources
Always use defer statements to ensure resources are properly cleaned up, even when errors occur. This prevents resource leaks and ensures consistent cleanup behavior.
Always verify that a package is in the correct state before performing operations. Check if the package is open, not read-only, and in a valid state for the intended operation.
Use appropriate context timeouts for long-running operations to prevent indefinite blocking. Set timeouts based on the expected operation duration and handle timeout errors gracefully.
// NewPackage creates a new empty package
func NewPackage() (Package, error)This function creates a new, empty NovusPack package in memory with default values.
The package exists only in memory until written to disk using one of the Write functions (Write, SafeWrite, or FastWrite).
Returns a new Package instance with:
- Default header values (magic number, version, timestamps)
- Empty file index
- Empty package comment
- Closed state set to false
- Creates package structure in memory only (no file I/O operations performed)
- Initializes package with standard NovusPack header
- Sets creation timestamp to current time
- Prepares package for file operations
- Package must be written to disk using one of the Write functions (
Write,SafeWrite, orFastWrite) before it can be persisted
package, err := NewPackage()
if err != nil {
return err
}
defer package.Close()// NewPackageWithOptions creates a new package with specified configuration options
// Returns *PackageError on failure
func NewPackageWithOptions(ctx context.Context, options CreateOptions) (Package, error)This function creates a new NovusPack package in memory with the specified configuration options.
This function does not write to disk - it only creates and configures the package structure in memory.
The package file is only written to disk when one of the Write functions (Write, SafeWrite, or FastWrite) is called.
Path Validation: If Path is provided in options, this function validates that the provided path is valid and the target directory is writable, even though it doesn't write to disk. This ensures early detection of path-related issues.****
ctx: Context for cancellation and timeout handlingoptions: Package configuration options
- Creates package structure in memory (same as
NewPackage) - Initializes package with standard NovusPack header
- Sets creation timestamp to current time
- If
Pathis provided:- Validates that the provided path is valid and well-formed
- Validates that the target directory exists and is writable (fails if directory does not exist)
- Stores the target path for later writing operations
- Applies provided options:
- Sets package comment if
Commentis provided - Sets VendorID if provided
- Sets AppID if provided
- Stores file permissions for later use during Write operations
- Enables package-level compression if
CompressPackageis true - Sets session base path if
SessionBaseis provided (usesSetSessionBaseinternally)
- Sets package comment if
- The package remains in an unsigned state until written (compressed if
CompressPackageis true) - No file I/O operations are performed on the target file - package remains in memory
Note: While the target file is not created during NewPackageWithOptions, if a path is provided, the path and directory are validated to ensure they exist and are writable. This enables early error detection before file operations begin. The parent directory must already exist - NewPackageWithOptions will not create missing parent directories.
- Validation Errors (see
ErrTypeValidation):- Invalid or malformed file path (if Path is provided)
- Target directory does not exist (parent directories are not created)
- Target directory is not writable
- Insufficient permissions to create file in target directory
- Security Errors: Insufficient permissions to access target directory (see
ErrTypeSecurity) - Context Errors: Context cancellation or timeout exceeded
- Additional Validation Errors: Invalid option values (e.g., invalid VendorID format)
// Create package with options (still in memory)
options := CreateOptions{
Path: Option.Some("/path/to/game-package.nvpk"),
Comment: Option.Some("My Game Package"),
VendorID: Option.Some(uint32(0x00000001)), // Steam
AppID: Option.Some(uint64(0x00000000000002DA)), // CS:GO
Permissions: Option.Some(os.FileMode(0644)),
CompressPackage: true,
SessionBase: Option.Some("/base/path"),
}
package, err := NewPackageWithOptions(ctx, options)
if err != nil {
return err
}
defer package.Close()
// Add files, metadata, etc...
// ...
// Write to disk
err = package.Write(ctx)
if err != nil {
return err
}If Path is not provided in options, the package is created without a target path.
The path can be set later using SetTargetPath:
// Example: Create package without path
options := CreateOptions{
Comment: Option.Some("My Package"),
CompressPackage: true,
}
package, err := NewPackageWithOptions(ctx, options)
if err != nil {
return err
}
// Set path later
err = package.SetTargetPath(ctx, "/path/to/package.nvpk")
if err != nil {
return err
}// CreateOptions represents options for creating a package.
// CreateOptions allows configuring package creation with metadata,
// comments, and identifiers.
type CreateOptions struct {
Path Option[string] // Target file system path where package will be written
Comment Option[string] // Initial package comment
VendorID Option[uint32] // Vendor identifier
AppID Option[uint64] // Application identifier
Permissions Option[os.FileMode] // File permissions (default: 0644)
CompressPackage bool // Enable package-level compression (default: false)
SessionBase Option[string] // Session base path for file operations
}// SetTargetPath changes the package's target write path
// Returns *PackageError on failure
func (p *Package) SetTargetPath(ctx context.Context, path string) errorThis function changes the target path for an existing package that will be used when calling Write, SafeWrite, or FastWrite.
This is useful when you want to write an existing package (either newly created or opened from disk) to a different location.
Early Validation: This function validates the path and target directory immediately (requiring minimal I/O), enabling early error detection before write operations begin.
Consistent with Create, this ensures path-related issues are caught early.
Path Validation: This function validates that the provided path is valid and the target directory is writable, even though it doesn't write to disk. This validation requires minimal filesystem I/O to check directory existence and permissions.
Signature Clearing: If the package is signed and the new path differs from the current path, this function MUST clear all signature information from the in-memory package. This is required because signed packages are immutable and writing to a new location creates a new, unsigned package.
Important: Signature clearing only occurs when the new path differs from the current path.
If SetTargetPath is called with the same path as the current path, signatures are NOT cleared.
See Package Writing API - Writing Signed Package Content to New Path for complete signature clearing behavior.
ctx: Context for cancellation and timeout handlingpath: New file system path where the package will be written
- Validates that the provided path is valid and well-formed
- Validates that the target directory exists and is writable (requires minimal filesystem I/O)
- If the new path differs from the current path and the package is signed, clears signature information (see Package Writing API - Writing Signed Package Content to New Path)
- Updates the package's internal target path
- Does not create or modify files (validation only)
- Validation Errors (see
ErrTypeValidation):- Invalid or malformed file path
- Target directory does not exist
- Target directory is not writable
- Insufficient permissions to create file in target directory
- Security Errors: Insufficient permissions to access target directory (see
ErrTypeSecurity) - Context Errors: Context cancellation or timeout exceeded
// Open existing package
pkg, err := OpenPackage(ctx, "/path/to/existing.nvpk")
if err != nil {
return err
}
// Make some modifications
err = pkg.AddFile(ctx, "/path/to/newfile.txt", nil)
if err != nil {
return err
}
// Change target path to write to a new location (validates directory early)
err = pkg.SetTargetPath(ctx, "/path/to/modified-package.nvpk")
if err != nil {
return err
}
// Write to the new location
err = pkg.SafeWrite(ctx, false)
if err != nil {
return err
}NewPackageWithOptions: Used for initial package creation with configuration options, including optional pathSetTargetPath: Used to change the write path on an existing package (created or opened)- Both validate the target path and directory
- Both clear signatures when the path changes on a signed package
NewPackageWithOptionscreates and configures a new package;SetTargetPathonly changes the path on an existing package
This section describes package configuration options and settings.
// Package configuration
type PackageConfig struct {
// DefaultPathHandling specifies default behavior for multiple paths
// Default: PathHandlingHardLinks (backward compatible)
DefaultPathHandling PathHandling
// AutoConvertToSymlinks enables automatic conversion of duplicate paths to symlinks
// Default: false (backward compatible)
AutoConvertToSymlinks bool
}Provides package-level configuration for path handling behavior during file addition operations.
-
DefaultPathHandling: Specifies default behavior for multiple paths pointing to the same contentPathHandlingHardLinks(1): Store multiple paths as hard links (backward compatible default)PathHandlingSymlinks(2): Convert additional paths to symlinksPathHandlingPreserve(3): Preserve original filesystem behavior- Default:
PathHandlingHardLinks(maintains backward compatibility)
-
AutoConvertToSymlinks: Enables automatic conversion of duplicate paths to symlinks during deduplication- When
true, deduplication creates symlinks instead of adding paths to existing FileEntry - Default:
false(backward compatible)
- When
Default values ensure backward compatibility:
DefaultPathHandling = PathHandlingHardLinks: Maintains existing behaviorAutoConvertToSymlinks = false: No automatic conversion unless explicitly enabled- Existing packages continue to work without changes
// PathHandling specifies how to handle multiple paths pointing to the same content
type PathHandling uint8
const (
PathHandlingDefault PathHandling = 0 // Use package default
PathHandlingHardLinks PathHandling = 1 // Store multiple paths as hard links (current behavior)
PathHandlingSymlinks PathHandling = 2 // Convert additional paths to symlinks
PathHandlingPreserve PathHandling = 3 // Preserve original filesystem behavior (detect and respect symlinks/hardlinks)
)// OpenPackage opens an existing package from the specified path.
// It validates the on-disk package structure during open.
// Returns *PackageError on failure
func OpenPackage(ctx context.Context, path string) (Package, error)This function opens an existing NovusPack package file for reading. This function validates the package header, index, and required invariants before returning a Package instance.
ctx: Context for cancellation and timeout handlingpath: File system path to the existing package file
- Opens the package file for reading.
- Reads and validates the package header.
- Determines whether the package is compressed (header flags bits 15-8 != 0).
- If the package is compressed:
- Reads the metadata index located immediately after the header.
- Uses the metadata index to locate the compressed FileEntry metadata blocks.
- Decompresses and reads all FileEntry metadata.
- Locates, decompresses, and reads the compressed FileEntry index.
- Reads the package comment, if present.
- Detects signature presence for immutability enforcement, if present.
- If the package is not compressed:
- Locates and reads all FileEntry metadata.
- Locates and reads the FileEntry index.
- Reads the package comment, if present.
- Detects signature presence for immutability enforcement, if present.
- Validates the file index structure and offsets.
- Prepares the package for read operations.
- Validation Errors: Invalid format, invalid header, invalid index structure, or violated invariants (see
ErrTypeValidation). - Unsupported Errors: Package version not supported (see
ErrTypeUnsupported). - Compression Errors: Failed to decompress compressed FileEntry metadata or the compressed FileEntry index during open.
- Security Errors: Insufficient permissions to open file (see
ErrTypeSecurity). - I/O Errors: File system errors during opening.
- Context Errors: Context cancellation or timeout exceeded.
package, err := OpenPackage(ctx, "/path/to/existing-package.nvpk")
if err != nil {
return err
}
defer package.Close()This section outlines the handling of read only packages.
The read-only mode must be enforced without duplicating OpenPackage parsing or validation logic.
In the API, OpenPackageReadOnly should call OpenPackage and then return a wrapper type that implements the Package interface.
The wrapper must delegate read operations to the underlying package and must reject all mutating operations.
The wrapper must return structured errors for rejected operations.
The wrapper should prevent callers from type-asserting the returned Package to the writable implementation type.
This can be achieved by returning a distinct wrapper type as the dynamic type behind the Package interface.
The wrapper must reject all methods that mutate package state in memory or write to disk.
This includes all package write operations (Write, SafeWrite, FastWrite), all state-changing metadata setters, and lifecycle methods that change the target path or package configuration for writing.
At minimum, the wrapper must reject Create, SetTargetPath, Defragment, AddFile, AddFileFromMemory, AddFilePattern, AddDirectory, RemoveFile, RemoveFilePattern, Write, SafeWrite, FastWrite, SetComment, ClearComment, SetAppID, ClearAppID, SetVendorID, ClearVendorID, SetPackageIdentity, and ClearPackageIdentity.
// OpenPackageReadOnly opens a package in a read-only mode.
// It validates the on-disk package structure during open.
// The returned package must reject any attempt to mutate state or write to disk.
// Returns *PackageError on failure.
func OpenPackageReadOnly(ctx context.Context, path string) (Package, error)This function opens an existing NovusPack package file for reading. This function must enforce immutability for the returned package.
- Opens the package file for reading.
- Reads and validates the package header.
- Determines whether the package is compressed (header flags bits 15-8 != 0).
- If the package is compressed:
- Reads the metadata index located immediately after the header.
- Uses the metadata index to locate the compressed FileEntry metadata blocks.
- Decompresses and reads all FileEntry metadata.
- Locates, decompresses, and reads the compressed FileEntry index.
- Reads the package comment, if present.
- Detects signature presence for immutability enforcement, if present.
- If the package is not compressed:
- Locates and reads all FileEntry metadata.
- Locates and reads the FileEntry index.
- Reads the package comment, if present.
- Detects signature presence for immutability enforcement, if present.
- Validates the file index structure and offsets.
- Returns a Package wrapper that enforces read-only behavior.
- Rejects in-memory mutation operations with a structured error.
- Rejects write operations to disk with a structured error.
- All errors from
OpenPackage. - Security Errors: A write or mutation operation is attempted on a read-only package (see
ErrTypeSecurity).
// readOnlyPackage is a wrapper that enforces read-only behavior for a Package.
//
// This wrapper must be the dynamic type stored behind the Package interface returned by OpenPackageReadOnly.
//
// This prevents callers from type-asserting to the writable implementation type.
type readOnlyPackage struct {
inner Package
}
var _ Package = (*readOnlyPackage)(nil)The readOnlyPackage type wraps the inner Package and implements the Package interface:
- Read operations (ReadFile, ListFiles, GetMetadata, GetInfo, Validate, Close, IsOpen, GetComment, HasComment, GetAppID, HasAppID, GetVendorID, HasVendorID, GetPackageIdentity) delegate directly to the inner Package.
- Mutating operations (Create, Defragment, AddFile, AddFileFromMemory, RemoveFile, Write, SafeWrite, FastWrite, SetComment, ClearComment, SetAppID, ClearAppID, SetVendorID, ClearVendorID, SetPackageIdentity, ClearPackageIdentity) return a read-only error via the
readOnlyErrorhelper method.
// readOnlyError creates a structured security error for read-only enforcement.
// This helper method is used by all mutating operations to return consistent errors.
func (p *readOnlyPackage) readOnlyError(operation string) error {
return pkgerrors.NewPackageError(pkgerrors.ErrTypeSecurity, "package is read-only", nil, ReadOnlyErrorContext{
Operation: operation,
})
}// ReadOnlyErrorContext provides typed context for read-only enforcement errors.
type ReadOnlyErrorContext struct {
Operation string
}The following implementation shows how OpenPackageReadOnly reuses OpenPackage and wraps the returned Package to enforce read-only behavior.
See OpenPackageReadOnly for the function signature.
The implementation pattern:
// Implementation body for OpenPackageReadOnly
pkg, err := OpenPackage(ctx, path)
if err != nil {
return nil, err
}
return &readOnlyPackage{inner: pkg}, nil// OpenBrokenPackage opens a package that may be invalid or partially corrupted.
// This function is intended for repair workflows.
// TODO: Define repair APIs and the minimum guarantees of the returned package.
// Returns *PackageError on failure.
func OpenBrokenPackage(ctx context.Context, path string) (Package, error)This function opens a package that may be invalid or partially corrupted. This function is intended to support repair workflows and forensic inspection. This function is not required to enforce the same validation guarantees as OpenPackage. This function must return structured errors when I/O fails or when the file is unreadable. This function should expose enough internal state to enable repair operations.
TODO: Specify repair operations and the allowed state transitions.
// Close closes the package and releases resources
// Returns *PackageError on failure
func (p *Package) Close() errorThis function closes the package file and releases all associated resources.
- Closes the package file handle
- Releases memory buffers and caches
- Clears package state and metadata
- Performs any necessary cleanup operations
- Does not modify the package file (use Write methods to save changes)
- I/O Errors: File system errors during closing (see
ErrTypeIO) - Validation Errors: Package is not currently open (see
ErrTypeValidation)
// Example:
err := package.Close()
if err != nil {
return err
}// CloseWithCleanup closes the package and performs cleanup operations
// Returns *PackageError on failure
func (p *Package) CloseWithCleanup(ctx context.Context) errorThis function closes the package and performs additional cleanup operations.
- Closes the package file
- Performs cleanup operations (defragmentation, optimization)
- Releases all resources, including in-memory metadata caches
- May take longer than standard Close due to cleanup operations
// Validate validates package format, structure, and integrity
// Returns *PackageError on failure
func (p *Package) Validate(ctx context.Context) errorThis function performs comprehensive validation of the package format, structure, and integrity.
- Validates package header format and version
- Checks FileEntry structure and consistency
- Verifies data section integrity and checksums
- Detects signature presence for immutability enforcement, but does not validate signature contents in v1
- Ensures package follows NovusPack specifications
- Returns detailed error information for any issues found
- Validation Errors: Package not open, invalid format, validation failed (see
ErrTypeValidation) - Corruption Errors: Checksum mismatches (see
ErrTypeCorruption) - Context Errors: Context cancellation or timeout exceeded (see
ErrTypeContext)
// Example
err := package.Validate(ctx)
if err != nil {
return err
}// Defragment optimizes package structure and removes unused space
// Returns *PackageError on failure
func (p *Package) Defragment(ctx context.Context) errorThis function optimizes package structure by removing unused space and reorganizing data for better performance.
- Removes unused space from deleted files
- Reorganizes file entries for optimal access
- Compacts data sections to reduce file size
- Updates internal indexes and references
- Preserves all package metadata and signatures
- May take significant time for large packages
- Validation Errors: Package not open, read-only mode (see
ErrTypeValidation) - I/O Errors: File system errors during defragmentation (see
ErrTypeIO) - Context Errors: Context cancellation or timeout exceeded (see
ErrTypeContext)
// Example:
err := package.Defragment(ctx)
if err != nil {
return err
}Note: The canonical signature for GetInfo is defined in Core Package Interface API - Package.GetInfo.
This function retrieves comprehensive information about the current package.
Returns a PackageInfo structure containing:
- Basic package information (file count, sizes)
- Package identity (VendorID, AppID)
- Package comment and metadata
- Digital signature information
- Security and compression status
- Timestamps and feature flags
- Validation Errors: Package not currently open (see
ErrTypeValidation)
// Get comprehensive package information
info, err := package.GetInfo()
if err != nil {
return err
}
fmt.Printf("Package has %d files\n", info.FileCount)
fmt.Printf("Package version: %d\n", info.Version)These are low-level functions for header-only inspection without opening the full package.
- Validate .nvpk file format without loading package data
- Inspect package metadata before deciding to open
- Debugging corrupted or partially readable packages
- Stream processing where only header information is needed
- Quick validation of package files without full I/O overhead
ReadHeader: Use when you have an existingio.Readeror need fine-grained control over file operationsReadHeaderFromPath: Use when you want a simple, one-line header read from a file path with automatic file management
// ReadHeader reads the package header from a reader
func ReadHeader(ctx context.Context, reader io.Reader) (*Header, error)// ReadHeaderFromPath reads the package header from a file path
func ReadHeaderFromPath(ctx context.Context, path string) (*PackageHeader, error)Reads the package header from a file path. This is a convenience function that opens the file, reads the header, and closes the file automatically.
For more control over the file handle or to read from other sources, use ReadHeader with an io.Reader.
ctx: Context for cancellation and timeout handlingpath: File system path to the package file
Returns *PackageHeader and error.
- Validation Errors: Invalid package header format, invalid file path (see
ErrTypeValidation) - Unsupported Errors: Unsupported package version (see
ErrTypeUnsupported) - I/O Errors: File not found, permission denied, file system errors (see
ErrTypeIO) - Context Errors: Context cancellation or timeout exceeded (see
ErrTypeContext)
// Example:
header, err := ReadHeaderFromPath(ctx, "/path/to/package.nvpk")
if err != nil {
return err
}
fmt.Printf("Format Version: %d\n", header.FormatVersion)
fmt.Printf("Magic: 0x%08X\n", header.Magic)Reads the package header from an io.Reader.
This function is useful when you already have an open file handle or need to read from a stream.
ctx: Context for cancellation and timeout handlingreader: Input stream to read header from
Returns *PackageHeader and error.
- Validation Errors: Invalid package header format (see
ErrTypeValidation) - Unsupported Errors: Unsupported package version (see
ErrTypeUnsupported) - Context Errors: Context cancellation or timeout exceeded (see
ErrTypeContext)
// Example:
file, err := os.Open("/path/to/package.nvpk")
if err != nil {
return err
}
defer file.Close()
header, err := ReadHeader(ctx, file)
if err != nil {
return err
}
fmt.Printf("Format Version: %d\n", header.FormatVersion)// IsOpen checks if the package is currently open
func (p *Package) IsOpen() bool// IsReadOnly checks if the package is in read-only mode
func (p *Package) IsReadOnly() bool// GetPath returns the current package file path
func (p *Package) GetPath() stringThese functions provide information about the current package state.
The Package.sessionBase property controls what the package considers to be the "base" path for converting between absolute filesystem paths and stored package paths.
The sessionBase is a runtime-only property that is used for bidirectional path conversion:
- File Addition (Construction): Converts absolute filesystem paths to stored package paths
- File Extraction: Converts stored package paths to absolute filesystem paths
The session base is a runtime-only property that persists during package operations and is used to automatically derive paths in both directions.
When an absolute filesystem path is provided to file addition operations (AddFile, AddFilePattern, or AddDirectory) without an explicit BasePath in AddFileOptions, the session base is automatically established from that first absolute path.
Once established, the session base persists for all subsequent file operations within the same package construction session, ensuring consistent path derivation across multiple file additions.
For complete details on how session base affects path derivation during file addition, see File Addition API - Session Base and AddFileOptions: Path Determination.
When extracting files using ExtractPath, the session base is used as the default extraction root directory.
The session base can be set explicitly using SetSessionBase before extraction operations, or via the SessionBase option in ExtractPathOptions.
If the session base is not set and is required for default-relative extraction, extraction operations will fail with ErrTypeValidation.
For complete details on how session base affects extraction destination resolution, see File Extraction API - Destination Resolution.
The session base can be set explicitly using SetSessionBase before any file operations, or via the SessionBase option in NewPackageWithOptions.
The session base is runtime-only and is not persisted to disk.
// SetSessionBase explicitly sets the package-level session base path
// This method allows setting the session base before any file operations
// Returns *PackageError on failure (e.g., invalid path format)
func (p *Package) SetSessionBase(basePath string) errorSets the package-level session base path explicitly.
This is useful when you want to control the base path before adding files, rather than letting it be established automatically from the first absolute path.
basePath: The filesystem base directory to use for path derivation (must be an absolute path)
error:*PackageErrorwithErrTypeValidationif the path format is invalid
// Example: Setting session base for file addition
pkg := NewPackage()
err := pkg.SetSessionBase("/home/user/project")
if err != nil {
return err
}
// Now all absolute paths will be relative to /home/user/project
entry, err := pkg.AddFile(ctx, "/home/user/project/src/main.go", nil)
// Stored as: /src/main.go
// Example: Setting session base for file extraction
pkg2, err := OpenPackage(ctx, "/path/to/package.nvp")
if err != nil {
return err
}
err = pkg2.SetSessionBase("/tmp/extract")
if err != nil {
return err
}
// Extract files will be written to /tmp/extract/...
err = pkg2.ExtractPath(ctx, "/src/main.go", false, nil)
// Extracted to: /tmp/extract/src/main.go// GetSessionBase returns the current session base path
// Returns empty string if no session base has been established
func (p *Package) GetSessionBase() stringReturns the current session base path for inspection or logging:
string: Current session base path, or empty string if no session base is set
base := pkg.GetSessionBase()
if base != "" {
fmt.Printf("Session base: %s\n", base)
}// ClearSessionBase clears the package-level session base
// Subsequent absolute paths will establish a new session base
func (p *Package) ClearSessionBase()Clears the current session base, allowing a new base to be established from the next absolute path.
This is useful when you want to change the base path strategy mid-construction.
// Example: Add files with one base
pkg.SetSessionBase("/home/user/project1")
pkg.AddFile(ctx, "/home/user/project1/file1.txt", nil)
// Example: Switch to different base
pkg.ClearSessionBase()
pkg.SetSessionBase("/home/user/project2")
pkg.AddFile(ctx, "/home/user/project2/file2.txt", nil)// HasSessionBase returns true if a session base is currently set
func (p *Package) HasSessionBase() boolChecks whether a session base is currently active.
bool: true if a session base is set, false otherwise
if !pkg.HasSessionBase() {
// Set explicit base before adding files
pkg.SetSessionBase("/home/user/project")
}The NovusPack API uses a comprehensive structured error system that provides better error categorization, context, and debugging capabilities.
Usage: Create structured errors with rich context for different error scenarios.
The NovusPack Basic Operations API uses the following error types from the structured error system:
ErrTypeValidation: Input validation errors, invalid parameters, format errorsErrTypeIO: I/O errors, file system operations, network issuesErrTypeSecurity: Security-related errors, access denied, authentication failuresErrTypeUnsupported: Unsupported features, versions, or operationsErrTypeContext: Context cancellation, timeout, and lifecycle errorsErrTypeCorruption: Data corruption, checksum failures, integrity violations
// Define error context types
type PackageErrorContext struct {
Path string
Operation string
}// SecurityErrorContext provides typed context for security-related errors.
// This context structure is used with structured errors to provide additional
// diagnostic information for security operations.
type SecurityErrorContext struct {
Path string
User string
}// IOErrorContext provides typed context for I/O-related errors.
// This context structure is used with structured errors to provide additional
// diagnostic information for file system and I/O operations.
type IOErrorContext struct {
File string
Offset int64
}// Example: Create a validation error with typed context
err := NewPackageError(ErrTypeValidation, "package file not found", nil, PackageErrorContext{
Path: "/path/to/package.nvpk",
Operation: "Open",
})
// Example: Wrap an existing error with typed context
err := WrapErrorWithContext(io.ErrUnexpectedEOF, ErrTypeIO, "unexpected end of file", IOErrorContext{
File: "package.nvpk",
Offset: 1024,
})
// Example: Create a security error with typed context
err := NewPackageError(ErrTypeSecurity, "permission denied", nil, SecurityErrorContext{
Path: "/path/to/package.nvpk",
User: "anonymous",
})
// Example: Create an I/O error with typed context
err := NewPackageError(ErrTypeIO, "failed to read package file", nil, PackageErrorContext{
Path: "/path/to/package.nvpk",
Operation: "Open",
})Usage: Check error types and handle them with appropriate logging and context extraction.
Best practices for error handling.
Always check for errors after calling package operations and handle them appropriately. Never ignore error return values as they indicate critical failures that must be addressed.
Use the structured error system to provide rich context for debugging. Wrap errors with additional context information to help identify the source of problems and provide better error messages to users.
Use context timeouts and cancellation to prevent operations from hanging indefinitely. Set appropriate timeouts for long-running operations and handle context cancellation gracefully.
Handle different error types with appropriate responses. Provide user-friendly messages for validation errors, log security errors, and implement retry logic for I/O errors. Use the structured error system to determine the appropriate handling strategy.
Always clean up resources properly using defer statements. Ensure packages are closed even when errors occur, and handle cleanup errors gracefully to prevent resource leaks.
Wrap errors with additional context information to provide better debugging information. Include relevant details such as file paths, operation names, and parameter values in error messages.
This section describes best practices for managing resources in package operations.
Use context for resource management and cancellation. Pass context to long-running operations and handle context cancellation to ensure proper resource cleanup and operation termination.
Handle cleanup errors gracefully by logging warnings rather than failing. Use defer functions to ensure cleanup occurs even when errors happen, and log cleanup failures as warnings rather than errors.