package update
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"tera-clone/services/tera-api/internal/storage/atomicfile"
"tera-clone/services/tera-api/internal/storage/capacity"
)
var safeFilename = regexp . MustCompile ( ` ^[A-Za-z0-9][A-Za-z0-9._-] { 0,127}$ ` )
type ManagerOptions struct {
UpdateRoot string
PolicyPath string
TrustDirectory string
Product string
Profile string
CurrentVersion string
Clock func ( ) time . Time
RecoveryApplyEnabled bool
RequestRecovery func ( string ) error
Reboot func ( ) error
}
type Manager struct {
mu sync . Mutex
active bool
status Status
root string
incoming string
validated string
journalPath string
trustDirectory string
product string
profile string
currentVersion string
clock func ( ) time . Time
governor * capacity . Governor
recoveryApplyEnabled bool
requestRecovery func ( string ) error
reboot func ( ) error
}
func NewManager ( options ManagerOptions ) ( * Manager , error ) {
if options . Clock == nil {
options . Clock = time . Now
}
if options . Product == "" || options . Profile == "" || options . CurrentVersion == "" || options . TrustDirectory == "" {
return nil , errors . New ( "update product, profile, current version, and trust directory are required" )
}
if options . RecoveryApplyEnabled && ( options . RequestRecovery == nil || options . Reboot == nil ) {
return nil , errors . New ( "enabled recovery apply requires recovery-request and reboot adapters" )
}
root := filepath . Clean ( options . UpdateRoot )
directories := [ ] string { root , filepath . Join ( root , "incoming" ) , filepath . Join ( root , "validated" ) , filepath . Join ( root , "journal" ) }
for _ , directory := range directories {
if err := requireDirectory ( directory ) ; err != nil {
return nil , err
}
}
if err := requireDirectory ( options . TrustDirectory ) ; err != nil {
return nil , err
}
policy , err := capacity . LoadPolicy ( options . PolicyPath )
if err != nil {
return nil , err
}
governor , err := capacity . New ( root , policy )
if err != nil {
return nil , err
}
manager := & Manager {
root : root , incoming : directories [ 1 ] , validated : directories [ 2 ] , journalPath : filepath . Join ( directories [ 3 ] , "status.json" ) ,
trustDirectory : options . TrustDirectory , product : options . Product , profile : options . Profile , currentVersion : options . CurrentVersion ,
clock : options . Clock , governor : governor , recoveryApplyEnabled : options . RecoveryApplyEnabled ,
requestRecovery : options . RequestRecovery , reboot : options . Reboot ,
}
if err := manager . recover ( ) ; err != nil {
return nil , err
}
return manager , nil
}
func ( m * Manager ) Status ( context . Context ) ( Status , error ) {
m . mu . Lock ( )
defer m . mu . Unlock ( )
status := cloneStatus ( m . status )
snapshot , err := m . governor . Snapshot ( )
if err != nil {
return Status { } , err
}
status . Capacity = snapshot
return status , nil
}
func ( m * Manager ) Upload ( ctx context . Context , request UploadRequest ) ( Status , error ) {
filename := strings . TrimSpace ( request . Filename )
if request . Body == nil || request . ContentLength <= 0 || ! safeFilename . MatchString ( filename ) || filepath . Base ( filename ) != filename {
return Status { } , & Error { Code : "update_validation_failed" , Message : "a bounded local package filename and content length are required" }
}
m . mu . Lock ( )
if m . active {
m . mu . Unlock ( )
return Status { } , & Error { Code : "update_conflict" , Message : "another firmware upload is already active" }
}
m . active = true
m . mu . Unlock ( )
defer func ( ) {
m . mu . Lock ( )
m . active = false
m . mu . Unlock ( )
} ( )
reservation , snapshot , err := m . governor . Reserve ( request . ContentLength )
if err != nil {
code := "update_capacity_failed"
if ! errors . Is ( err , capacity . ErrInsufficientSpace ) {
code = "update_unavailable"
}
return Status { } , & Error { Code : code , Message : err . Error ( ) }
}
defer reservation . Release ( )
operationID , err := randomID ( )
if err != nil {
return Status { } , & Error { Code : "update_unavailable" , Message : "cannot allocate update operation id" }
}
failUpload := func ( code , reason string , cleanupPaths ... string ) ( Status , error ) {
reservation . Release ( )
return m . fail ( operationID , code , reason , cleanupPaths ... )
}
now := m . clock ( ) . UTC ( )
status := Status {
FormatVersion : 1 , OperationID : operationID , State : StateReceiving , Message : "Receiving local firmware package" ,
Filename : filename , DeviceVersion : m . currentVersion , BytesTotal : request . ContentLength , ApplyEnabled : false ,
ApplyDisabledReason : "Package validation has not completed" , RecoveryInstructions : "If validation fails, correct the package and upload again. No running firmware is changed." ,
Capacity : snapshot , CreatedAt : now , UpdatedAt : now ,
}
if err := m . setStatus ( status , true ) ; err != nil {
return Status { } , & Error { Code : "update_unavailable" , Message : err . Error ( ) }
}
partPath := filepath . Join ( m . incoming , operationID + ".part" )
file , err := os . OpenFile ( partPath , os . O_WRONLY | os . O_CREATE | os . O_EXCL , 0 o600 )
if err != nil {
return failUpload ( "update_staging_failed" , "Cannot create the protected staging file" , partPath )
}
buffer := make ( [ ] byte , 1024 * 1024 )
reader := & contextReader { ctx : ctx , reader : io . LimitReader ( request . Body , request . ContentLength + 1 ) }
progress := & progressWriter { writer : file , onProgress : func ( received int64 ) { m . updateProgress ( received ) } }
written , copyErr := io . CopyBuffer ( progress , reader , buffer )
syncErr := file . Sync ( )
closeErr := file . Close ( )
if copyErr != nil || syncErr != nil || closeErr != nil || written != request . ContentLength {
return failUpload ( "update_receive_failed" , "Firmware upload was interrupted or its length did not match" , partPath )
}
stagedPath := filepath . Join ( m . incoming , operationID + ".pkg" )
if err := os . Rename ( partPath , stagedPath ) ; err != nil || syncDirectory ( m . incoming ) != nil {
return failUpload ( "update_staging_failed" , "Cannot durably stage the firmware package" , partPath , stagedPath )
}
status = m . transition ( StateStaged , "Firmware package staged" , 80 )
if err := m . setStatus ( status , true ) ; err != nil {
return failUpload ( "update_journal_failed" , "Cannot persist the staged state" , stagedPath )
}
status = m . transition ( StateValidating , "Validating signature, compatibility, version, size, and payload digests" , 85 )
if err := m . setStatus ( status , true ) ; err != nil {
return failUpload ( "update_journal_failed" , "Cannot persist the validation state" , stagedPath )
}
manifest , signerKeyID , err := VerifyPackage ( stagedPath , m . trustDirectory , m . product , m . profile , m . currentVersion )
if err != nil {
return failUpload ( "update_validation_failed" , err . Error ( ) , stagedPath )
}
validatedPath := filepath . Join ( m . validated , operationID + ".pkg" )
if err := os . Rename ( stagedPath , validatedPath ) ; err != nil || syncDirectory ( m . incoming ) != nil || syncDirectory ( m . validated ) != nil {
return failUpload ( "update_staging_failed" , "Cannot commit the validated package" , stagedPath , validatedPath )
}
status = m . transition ( StateValidated , "Package is authentic and compatible; running firmware was not changed" , 100 )
status . Manifest = & manifest
status . SignerKeyID = signerKeyID
status . PackagePath = validatedPath
status . ApplyEnabled , status . ApplyDisabledReason = m . applyPolicy ( manifest )
status . RecoveryInstructions = manifest . Recovery . Instructions
reservation . Release ( )
if snapshot , snapshotErr := m . governor . Snapshot ( ) ; snapshotErr == nil {
status . Capacity = snapshot
}
if err := m . setStatus ( status , true ) ; err != nil {
return failUpload ( "update_journal_failed" , "Validated package exists but its journal could not be committed" , validatedPath )
}
// Keep the previously validated package until the new validated state is
// durable. Cleanup after the journal commit preserves an atomic fallback if
// the final status write fails.
m . removeOlderValidated ( validatedPath )
if snapshot , snapshotErr := m . governor . Snapshot ( ) ; snapshotErr == nil {
status . Capacity = snapshot
_ = m . setStatus ( status , false )
}
return cloneStatus ( status ) , nil
}
func ( m * Manager ) Apply ( ctx context . Context , request ApplyRequest ) ( Status , error ) {
if strings . TrimSpace ( request . Actor ) == "" {
return Status { } , & Error { Code : "update_validation_failed" , Message : "an authenticated update actor is required" }
}
m . mu . Lock ( )
if m . active {
m . mu . Unlock ( )
return Status { } , & Error { Code : "update_conflict" , Message : "another firmware operation is already active" }
}
status := cloneStatus ( m . status )
m . active = true
m . mu . Unlock ( )
defer func ( ) {
m . mu . Lock ( )
m . active = false
m . mu . Unlock ( )
} ( )
if status . State != StateValidated || status . Manifest == nil {
return status , & Error { Code : "update_apply_unsupported" , Message : "a signed recovery package must be validated before apply" }
}
if request . OperationID == "" || request . OperationID != status . OperationID {
return status , & Error { Code : "update_validation_failed" , Message : "apply operation id does not match the validated package" }
}
enabled , reason := m . applyPolicy ( * status . Manifest )
if ! enabled {
status . ApplyEnabled , status . ApplyDisabledReason = false , reason
return status , & Error { Code : "update_apply_unsupported" , Message : reason }
}
select {
case <- ctx . Done ( ) :
return status , & Error { Code : "update_interrupted" , Message : "apply request was cancelled before recovery was armed" }
default :
}
packagePath := filepath . Join ( m . validated , status . OperationID + ".pkg" )
manifest , signer , err := VerifyPackage ( packagePath , m . trustDirectory , m . product , m . profile , m . currentVersion )
if err != nil || manifest . PackageID != status . Manifest . PackageID || signer != status . SignerKeyID {
reason := "validated package identity changed before apply"
if err != nil {
reason = err . Error ( )
}
return m . fail ( status . OperationID , "update_revalidation_failed" , reason )
}
status . State = StateApplyPending
status . Message = "Recovery apply accepted; synchronizing durable request state"
status . ProgressPercent = 100
status . ApplyEnabled = false
status . ApplyDisabledReason = "Recovery apply is already pending"
status . RecoveryStep = "request-pending"
status . UpdatedAt = m . clock ( ) . UTC ( )
if err := m . setStatus ( status , true ) ; err != nil {
return status , & Error { Code : "update_journal_failed" , Message : err . Error ( ) }
}
if err := m . requestRecovery ( status . OperationID ) ; err != nil {
return m . fail ( status . OperationID , "update_recovery_request_failed" , err . Error ( ) )
}
status . State = StateRebootPending
status . Message = "Recovery request is durable; rebooting into the offline updater"
status . RecoveryStep = "reboot-pending"
status . UpdatedAt = m . clock ( ) . UTC ( )
if err := m . setStatus ( status , true ) ; err != nil {
return status , & Error { Code : "update_journal_failed" , Message : err . Error ( ) }
}
if err := m . reboot ( ) ; err != nil {
status . RecoveryInstructions = "The recovery request is already set. Use the approved service procedure to reset the device; do not clear misc or remove the validated package."
_ = m . setStatus ( status , true )
return cloneStatus ( status ) , & Error { Code : "update_reboot_failed" , Message : err . Error ( ) }
}
return cloneStatus ( status ) , nil
}
func ( m * Manager ) applyPolicy ( manifest Manifest ) ( bool , string ) {
if manifest . Apply . Capability != ApplyCapabilityRecoveryV1 {
return false , "This package is validation-only and cannot change running firmware"
}
if ! m . recoveryApplyEnabled {
return false , "Recovery Apply remains locked until the target recovery boot, readback, fault-injection, and service-path gates are approved"
}
return true , ""
}
func ( m * Manager ) recover ( ) error {
entries , err := os . ReadDir ( m . incoming )
if err != nil {
return fmt . Errorf ( "scan update incoming directory: %w" , err )
}
for _ , entry := range entries {
if entry . Type ( ) . IsRegular ( ) && ( strings . HasSuffix ( entry . Name ( ) , ".part" ) || strings . HasSuffix ( entry . Name ( ) , ".pkg" ) ) {
if err := os . Remove ( filepath . Join ( m . incoming , entry . Name ( ) ) ) ; err != nil {
return fmt . Errorf ( "remove interrupted update staging: %w" , err )
}
}
}
if data , err := os . ReadFile ( m . journalPath ) ; err == nil {
if err := decodeSingleJSON ( data , & m . status ) ; err != nil {
return fmt . Errorf ( "decode update journal: %w" , err )
}
if m . status . State == StateReceiving || m . status . State == StateStaged || m . status . State == StateValidating {
m . status . State = StateFailed
m . status . Message = "Interrupted update operation recovered"
m . status . FailureCode = "update_interrupted"
m . status . FailureReason = "The updater restarted before validation completed; the partial package was removed"
m . status . RecoveryInstructions = "Upload the signed local package again. The running firmware was not changed."
m . status . UpdatedAt = m . clock ( ) . UTC ( )
return m . persistLocked ( )
}
if m . status . State == StateValidated && m . status . Manifest != nil {
if m . status . DeviceVersion == "" {
m . status . DeviceVersion = m . currentVersion
}
m . status . ApplyEnabled , m . status . ApplyDisabledReason = m . applyPolicy ( * m . status . Manifest )
return m . persistLocked ( )
}
return nil
} else if ! os . IsNotExist ( err ) {
return fmt . Errorf ( "read update journal: %w" , err )
}
now := m . clock ( ) . UTC ( )
m . status = Status { FormatVersion : 1 , State : StateIdle , Message : "No firmware package has been uploaded" , DeviceVersion : m . currentVersion , ApplyEnabled : false , ApplyDisabledReason : "Upload and validate a signed local package first" , RecoveryInstructions : "No recovery action is required." , UpdatedAt : now }
return m . persistLocked ( )
}
func ( m * Manager ) fail ( operationID , code , reason string , cleanupPaths ... string ) ( Status , error ) {
for _ , cleanupPath := range cleanupPaths {
if cleanupPath != "" {
_ = os . Remove ( cleanupPath )
}
}
m . mu . Lock ( )
status := m . status
m . mu . Unlock ( )
status . OperationID = operationID
status . State = StateFailed
status . Message = "Firmware package validation did not complete"
status . ProgressPercent = 0
status . ApplyEnabled = false
status . ApplyDisabledReason = "Only a successfully validated package can proceed"
status . FailureCode = code
status . FailureReason = reason
status . RecoveryInstructions = "Correct or replace the signed package and upload it again. The running firmware was not changed."
status . UpdatedAt = m . clock ( ) . UTC ( )
if snapshot , snapshotErr := m . governor . Snapshot ( ) ; snapshotErr == nil {
status . Capacity = snapshot
}
_ = m . setStatus ( status , true )
return cloneStatus ( status ) , & Error { Code : code , Message : reason }
}
func ( m * Manager ) transition ( state , message string , progress int ) Status {
m . mu . Lock ( )
defer m . mu . Unlock ( )
status := cloneStatus ( m . status )
status . State , status . Message , status . ProgressPercent , status . UpdatedAt = state , message , progress , m . clock ( ) . UTC ( )
return status
}
func ( m * Manager ) updateProgress ( received int64 ) {
m . mu . Lock ( )
defer m . mu . Unlock ( )
m . status . BytesReceived = received
if m . status . BytesTotal > 0 {
m . status . ProgressPercent = int ( received * 75 / m . status . BytesTotal )
}
m . status . UpdatedAt = m . clock ( ) . UTC ( )
}
func ( m * Manager ) setStatus ( status Status , persist bool ) error {
m . mu . Lock ( )
defer m . mu . Unlock ( )
m . status = cloneStatus ( status )
if persist {
return m . persistLocked ( )
}
return nil
}
func ( m * Manager ) persistLocked ( ) error {
data , err := json . MarshalIndent ( m . status , "" , " " )
if err != nil {
return fmt . Errorf ( "encode update journal: %w" , err )
}
if err := atomicfile . Write ( m . journalPath , append ( data , '\n' ) , 0 o600 ) ; err != nil {
return fmt . Errorf ( "persist update journal: %w" , err )
}
return nil
}
func ( m * Manager ) removeOlderValidated ( keep string ) {
entries , err := os . ReadDir ( m . validated )
if err != nil {
return
}
for _ , entry := range entries {
candidate := filepath . Join ( m . validated , entry . Name ( ) )
if candidate != keep && entry . Type ( ) . IsRegular ( ) && strings . HasSuffix ( entry . Name ( ) , ".pkg" ) {
_ = os . Remove ( candidate )
}
}
}
func requireDirectory ( path string ) error {
info , err := os . Lstat ( path )
if err != nil {
return fmt . Errorf ( "inspect required directory %s: %w" , path , err )
}
if ! info . IsDir ( ) || info . Mode ( ) & os . ModeSymlink != 0 {
return fmt . Errorf ( "required path is not a safe directory: %s" , path )
}
return nil
}
func randomID ( ) ( string , error ) {
value := make ( [ ] byte , 16 )
if _ , err := rand . Read ( value ) ; err != nil {
return "" , err
}
return "fw-" + hex . EncodeToString ( value ) , nil
}
func cloneStatus ( value Status ) Status {
if value . Manifest != nil {
manifest := * value . Manifest
manifest . Payloads = append ( [ ] Payload ( nil ) , value . Manifest . Payloads ... )
value . Manifest = & manifest
}
return value
}
type contextReader struct {
ctx context . Context
reader io . Reader
}
func ( r * contextReader ) Read ( buffer [ ] byte ) ( int , error ) {
select {
case <- r . ctx . Done ( ) :
return 0 , r . ctx . Err ( )
default :
return r . reader . Read ( buffer )
}
}
type progressWriter struct {
writer io . Writer
received int64
onProgress func ( int64 )
}
func ( w * progressWriter ) Write ( buffer [ ] byte ) ( int , error ) {
n , err := w . writer . Write ( buffer )
w . received += int64 ( n )
if w . onProgress != nil {
w . onProgress ( w . received )
}
return n , err
}
// FactoryReset removes uploaded packages and their journal, not installed
// firmware or the firmware-signing trust anchors.
func ( m * Manager ) FactoryReset ( ctx context . Context ) error {
m . mu . Lock ( )
defer m . mu . Unlock ( )
if err := ctx . Err ( ) ; err != nil {
return err
}
if m . active || m . status . State == StateRebootPending || m . status . State == StateApplying {
return errors . New ( "cannot factory reset during firmware update" )
}
for _ , dir := range [ ] string { m . incoming , m . validated , filepath . Dir ( m . journalPath ) } {
if err := requireDirectory ( dir ) ; err != nil {
return err
}
entries , err := os . ReadDir ( dir )
if err != nil {
return err
}
for _ , entry := range entries {
path := filepath . Join ( dir , entry . Name ( ) )
if filepath . Dir ( path ) != dir || entry . IsDir ( ) {
return errors . New ( "unexpected firmware staging entry" )
}
if err := os . Remove ( path ) ; err != nil {
return err
}
}
if err := syncDirectory ( dir ) ; err != nil {
return err
}
}
m . status = Status { FormatVersion : 1 , State : StateIdle , Message : "No firmware package has been uploaded" , DeviceVersion : m . currentVersion , UpdatedAt : m . clock ( ) . UTC ( ) }
return m . persistLocked ( )
}