IdACB1ValidationGeneral.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */


package com.io7m.idstore.protocol.admin.cb.internal;

import com.io7m.cedarbridge.runtime.api.CBIntegerUnsigned32;
import com.io7m.cedarbridge.runtime.api.CBIntegerUnsigned64;
import com.io7m.cedarbridge.runtime.api.CBIntegerUnsigned8;
import com.io7m.cedarbridge.runtime.api.CBList;
import com.io7m.cedarbridge.runtime.api.CBOptionType;
import com.io7m.cedarbridge.runtime.api.CBSerializableType;
import com.io7m.cedarbridge.runtime.api.CBSome;
import com.io7m.cedarbridge.runtime.api.CBString;
import com.io7m.cedarbridge.runtime.api.CBUUID;
import com.io7m.cedarbridge.runtime.time.CBOffsetDateTime;
import com.io7m.idstore.model.IdAuditEvent;
import com.io7m.idstore.model.IdBan;
import com.io7m.idstore.model.IdEmail;
import com.io7m.idstore.model.IdLogin;
import com.io7m.idstore.model.IdNonEmptyList;
import com.io7m.idstore.model.IdPage;
import com.io7m.idstore.model.IdPassword;
import com.io7m.idstore.model.IdPasswordAlgorithms;
import com.io7m.idstore.model.IdPasswordException;
import com.io7m.idstore.model.IdTimeRange;
import com.io7m.idstore.protocol.admin.cb.IdA1AuditEvent;
import com.io7m.idstore.protocol.admin.cb.IdA1Ban;
import com.io7m.idstore.protocol.admin.cb.IdA1Login;
import com.io7m.idstore.protocol.admin.cb.IdA1Page;
import com.io7m.idstore.protocol.admin.cb.IdA1Password;
import com.io7m.idstore.protocol.admin.cb.IdA1TimeRange;
import com.io7m.idstore.protocol.admin.cb.IdA1TimestampUTC;
import com.io7m.idstore.protocol.api.IdProtocolException;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import static com.io7m.cedarbridge.runtime.api.CBOptionType.fromOptional;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.PROTOCOL_ERROR;
import static com.io7m.idstore.model.IdOptional.mapPartial;

/**
 * Functions to translate between the core command set and the Admin v1
 * Cedarbridge encoding command set.
 */

public final class IdACB1ValidationGeneral
{
  private IdACB1ValidationGeneral()
  {

  }

  public static <A, B extends CBSerializableType> IdA1Page<B> toWirePage(
    final IdPage<A> page,
    final Function<A, B> f)
  {
    return new IdA1Page<>(
      new CBList<>(page.items().stream().map(f).toList()),
      new CBIntegerUnsigned32(Integer.toUnsignedLong(page.pageIndex())),
      new CBIntegerUnsigned32(Integer.toUnsignedLong(page.pageCount())),
      new CBIntegerUnsigned64(page.pageFirstOffset())
    );
  }

  public static IdA1Ban toWireBan(
    final IdBan ban)
  {
    return new IdA1Ban(
      new CBUUID(ban.user()),
      new CBString(ban.reason()),
      fromOptional(
        ban.expires().map(IdACB1ValidationGeneral::toWireTimestamp)
      )
    );
  }

  public static IdA1TimestampUTC toWireTimestamp(
    final OffsetDateTime t)
  {
    return new IdA1TimestampUTC(
      new CBIntegerUnsigned32(Integer.toUnsignedLong(t.getYear())),
      new CBIntegerUnsigned8(t.getMonthValue()),
      new CBIntegerUnsigned8(t.getDayOfMonth()),
      new CBIntegerUnsigned8(t.getHour()),
      new CBIntegerUnsigned8(t.getMinute()),
      new CBIntegerUnsigned8(t.getSecond()),
      new CBIntegerUnsigned32(Integer.toUnsignedLong(t.getNano() / 1000))
    );
  }

  public static IdA1TimeRange toWireTimeRange(
    final IdTimeRange timeRange)
  {
    return new IdA1TimeRange(
      toWireTimestamp(timeRange.timeLower()),
      toWireTimestamp(timeRange.timeUpper())
    );
  }

  public static IdA1Password toWirePassword(
    final IdPassword password)
  {
    return new IdA1Password(
      new CBString(password.algorithm().identifier()),
      new CBString(password.hash()),
      new CBString(password.salt()),
      fromOptional(password.expires().map(CBOffsetDateTime::new))
    );
  }

  public static <A extends CBSerializableType, B> IdPage<B> fromWirePage(
    final IdA1Page<A> page,
    final Function<A, B> f)
  {
    return new IdPage<>(
      page.fieldItems().values().stream().map(f).toList(),
      (int) page.fieldPageIndex().value(),
      (int) page.fieldPageCount().value(),
      page.fieldPageFirstOffset().value()
    );
  }

  public static IdNonEmptyList<IdEmail> fromWireEmails(
    final CBList<CBString> fieldEmails)
    throws IdProtocolException
  {
    final var es = fieldEmails.values();
    if (es.isEmpty()) {
      throw new IdProtocolException(
        "Admin emails list is empty!",
        PROTOCOL_ERROR,
        Map.of(),
        Optional.of("Provide at least one admin email address.")
      );
    }

    final var emails = new ArrayList<>(fieldEmails.values());
    final var email0 = new IdEmail(emails.remove(0).value());
    return new IdNonEmptyList<>(
      email0,
      emails.stream().map(CBString::value).map(IdEmail::new).toList()
    );
  }

  public static Optional<IdPassword> fromWirePasswordOptional(
    final CBOptionType<IdA1Password> fieldPassword)
    throws IdPasswordException
  {
    if (fieldPassword instanceof CBSome<IdA1Password> some) {
      return Optional.of(
        fromWirePassword(some.value())
      );
    }
    return Optional.empty();
  }

  public static IdTimeRange fromWireTimeRange(
    final IdA1TimeRange t)
  {
    return new IdTimeRange(
      fromWireTimestamp(t.fieldLower()),
      fromWireTimestamp(t.fieldUpper())
    );
  }

  public static IdPassword fromWirePassword(
    final IdA1Password fieldPassword)
    throws IdPasswordException
  {
    return new IdPassword(
      IdPasswordAlgorithms.parse(fieldPassword.fieldAlgorithm().value()),
      fieldPassword.fieldHash().value(),
      fieldPassword.fieldSalt().value(),
      fieldPassword.fieldExpires().asOptional().map(CBOffsetDateTime::value)
    );
  }

  public static IdBan fromWireBan(
    final IdA1Ban fieldBan)
  {
    return new IdBan(
      fieldBan.fieldUser().value(),
      fieldBan.fieldReason().value(),
      mapPartial(
        fieldBan.fieldExpires().asOptional(),
        IdACB1ValidationGeneral::fromWireTimestamp)
    );
  }

  public static OffsetDateTime fromWireTimestamp(
    final IdA1TimestampUTC t)
  {
    return OffsetDateTime.of(
      (int) (t.fieldYear().value() & 0xffffffffL),
      t.fieldMonth().value(),
      t.fieldDay().value(),
      t.fieldHour().value(),
      t.fieldMinute().value(),
      t.fieldSecond().value(),
      (int) (t.fieldMillisecond().value() * 1000L),
      ZoneOffset.UTC
    );
  }

  public static IdA1AuditEvent toWireAuditEvent(
    final IdAuditEvent e)
  {
    return new IdA1AuditEvent(
      new CBIntegerUnsigned64(e.id()),
      new CBUUID(e.owner()),
      toWireTimestamp(e.time()),
      new CBString(e.type()),
      new CBString(e.message())
    );
  }

  public static IdLogin fromWireLogin(
    final IdA1Login i)
  {
    return new IdLogin(
      i.fieldUser().value(),
      fromWireTimestamp(i.fieldTime()),
      i.fieldHost().value(),
      i.fieldAgent().value()
    );
  }

  public static IdAuditEvent fromWireAuditEvent(
    final IdA1AuditEvent i)
  {
    return new IdAuditEvent(
      i.fieldId().value(),
      i.fieldOwner().value(),
      fromWireTimestamp(i.fieldTime()),
      i.fieldType().value(),
      i.fieldMessage().value()
    );
  }

  public static IdA1Login toWireLogin(
    final IdLogin i)
  {
    return new IdA1Login(
      new CBUUID(i.userId()),
      toWireTimestamp(i.time()),
      new CBString(i.host()),
      new CBString(i.userAgent())
    );
  }
}