Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
713f66beaa build(deps): bump github.com/olekukonko/tablewriter from 1.1.2 to 1.1.3
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/olekukonko/tablewriter/releases)
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.2...v1.1.3)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 14:45:42 +00:00
248 changed files with 3781 additions and 35970 deletions

View File

@@ -201,6 +201,13 @@ config = {
],
"skip": False,
},
"accountsHashDifficulty": {
"skip": False,
"suites": [
"apiAccountsHashDifficulty",
],
"accounts_hash_difficulty": "default",
},
"notification": {
"suites": [
"apiNotification",
@@ -227,6 +234,7 @@ config = {
],
"skip": False,
"antivirusNeeded": True,
"generateVirusFiles": True,
"extraServerEnvironment": {
"ANTIVIRUS_SCANNER_TYPE": "clamav",
"ANTIVIRUS_CLAMAV_SOCKET": "tcp://clamav:3310",
@@ -293,6 +301,7 @@ config = {
"skip": False,
"withRemotePhp": [True],
"antivirusNeeded": True,
"generateVirusFiles": True,
"extraServerEnvironment": {
"ANTIVIRUS_SCANNER_TYPE": "clamav",
"ANTIVIRUS_CLAMAV_SOCKET": "tcp://clamav:3310",
@@ -658,10 +667,10 @@ def testPipelines(ctx):
storage = "decomposed"
if "skip" not in config["cs3ApiTests"] or not config["cs3ApiTests"]["skip"]:
pipelines += cs3ApiTests(ctx, storage)
pipelines += cs3ApiTests(ctx, storage, "default")
if "skip" not in config["wopiValidatorTests"] or not config["wopiValidatorTests"]["skip"]:
pipelines += wopiValidatorTests(ctx, storage, "builtin")
pipelines += wopiValidatorTests(ctx, storage, "cs3")
pipelines += wopiValidatorTests(ctx, storage, "builtin", "default")
pipelines += wopiValidatorTests(ctx, storage, "cs3", "default")
pipelines += localApiTestPipeline(ctx)
pipelines += coreApiTestPipeline(ctx)
@@ -1050,12 +1059,12 @@ def codestyle(ctx):
return pipelines
def cs3ApiTests(ctx, storage):
def cs3ApiTests(ctx, storage, accounts_hash_difficulty = 4):
pipeline = {
"name": "test-cs3-API-%s" % storage,
"steps": evaluateWorkflowStep() +
restoreBuildArtifactCache(ctx, dirs["opencloudBinArtifact"], dirs["opencloudBinPath"]) +
opencloudServer(storage, deploy_type = "cs3api_validator") +
opencloudServer(storage, accounts_hash_difficulty, deploy_type = "cs3api_validator") +
[
{
"name": "cs3ApiTests",
@@ -1086,7 +1095,7 @@ def cs3ApiTests(ctx, storage):
])
return [pipeline]
def wopiValidatorTests(ctx, storage, wopiServerType):
def wopiValidatorTests(ctx, storage, wopiServerType, accounts_hash_difficulty = 4):
testgroups = [
"BaseWopiViewing",
"CheckFileInfoSchema",
@@ -1164,7 +1173,7 @@ def wopiValidatorTests(ctx, storage, wopiServerType):
"steps": evaluateWorkflowStep() +
restoreBuildArtifactCache(ctx, dirs["opencloudBinArtifact"], dirs["opencloudBinPath"]) +
waitForServices("fake-office", ["fakeoffice:8080"]) +
opencloudServer(storage, deploy_type = "wopi_validator", extra_server_environment = extra_server_environment) +
opencloudServer(storage, accounts_hash_difficulty, deploy_type = "wopi_validator", extra_server_environment = extra_server_environment) +
wopiServer +
waitForServices("wopi-fakeoffice", ["wopi-fakeoffice:9300"]) +
[
@@ -1209,21 +1218,29 @@ def wopiValidatorTests(ctx, storage, wopiServerType):
def localApiTestPipeline(ctx):
pipelines = []
with_remote_php = [True]
enable_watch_fs = [False]
if ctx.build.event == "cron":
with_remote_php.append(False)
enable_watch_fs.append(True)
defaults = {
"suites": {},
"skip": False,
"extraTestEnvironment": {},
"extraServerEnvironment": {},
"storages": ["posix"],
"accounts_hash_difficulty": 4,
"emailNeeded": False,
"antivirusNeeded": False,
"tikaNeeded": False,
"federationServer": False,
"collaborationServiceNeeded": False,
"extraCollaborationEnvironment": {},
"withRemotePhp": [True],
"enableWatchFs": [False],
"withRemotePhp": with_remote_php,
"enableWatchFs": enable_watch_fs,
"ldapNeeded": False,
"generateVirusFiles": False,
}
if "localApiTests" in config:
@@ -1238,14 +1255,6 @@ def localApiTestPipeline(ctx):
if "[decomposed]" in ctx.build.title.lower() or name.startswith("cli"):
params["storages"] = ["decomposed"]
if ctx.build.event == "cron":
params["withRemotePhp"] = [True, False]
params["enableWatchFs"] = [True, False]
# override withRemotePhp if specified in the suite config
if "withRemotePhp" in matrix:
params["withRemotePhp"] = matrix["withRemotePhp"]
for storage in params["storages"]:
for run_with_remote_php in params["withRemotePhp"]:
for run_with_watch_fs_enabled in params["enableWatchFs"]:
@@ -1270,15 +1279,16 @@ def localApiTestPipeline(ctx):
(waitForLdapService() if params["ldapNeeded"] else []) +
opencloudServer(
storage,
params["accounts_hash_difficulty"],
extra_server_environment = params["extraServerEnvironment"],
with_wrapper = True,
tika_enabled = params["tikaNeeded"],
watch_fs_enabled = run_with_watch_fs_enabled,
) +
(opencloudServer(storage, deploy_type = "federation", extra_server_environment = params["extraServerEnvironment"], watch_fs_enabled = run_with_watch_fs_enabled) if params["federationServer"] else []) +
(opencloudServer(storage, params["accounts_hash_difficulty"], deploy_type = "federation", extra_server_environment = params["extraServerEnvironment"], watch_fs_enabled = run_with_watch_fs_enabled) if params["federationServer"] else []) +
((wopiCollaborationService("fakeoffice") + wopiCollaborationService("collabora") + wopiCollaborationService("onlyoffice")) if params["collaborationServiceNeeded"] else []) +
(openCloudHealthCheck("wopi", ["wopi-collabora:9304", "wopi-onlyoffice:9304", "wopi-fakeoffice:9304"]) if params["collaborationServiceNeeded"] else []) +
localApiTest(params["suites"], storage, params["extraTestEnvironment"], run_with_remote_php) +
localApiTest(params["suites"], storage, params["extraTestEnvironment"], run_with_remote_php, params["generateVirusFiles"]) +
logRequests(),
"services": (emailService() if params["emailNeeded"] else []) +
(clamavService() if params["antivirusNeeded"] else []) +
@@ -1302,7 +1312,7 @@ def localApiTestPipeline(ctx):
pipelines.append(pipeline)
return pipelines
def localApiTest(suites, storage = "decomposed", extra_environment = {}, with_remote_php = False):
def localApiTest(suites, storage = "decomposed", extra_environment = {}, with_remote_php = False, generate_virus_files = False):
test_dir = "%s/tests/acceptance" % dirs["base"]
expected_failures_file = "%s/expected-failures-%s-storage.md" % (test_dir, storage)
@@ -1327,6 +1337,11 @@ def localApiTest(suites, storage = "decomposed", extra_environment = {}, with_re
commands = []
# Generate EICAR virus test files if needed
if generate_virus_files:
commands.append("chmod +x %s/tests/acceptance/scripts/generate-virus-files.sh" % dirs["base"])
commands.append("bash %s/tests/acceptance/scripts/generate-virus-files.sh" % dirs["base"])
# Merge expected failures
if not with_remote_php:
commands.append("cat %s/expected-failures-without-remotephp.md >> %s" % (test_dir, expected_failures_file))
@@ -1349,6 +1364,7 @@ def coreApiTestPipeline(ctx):
"numberOfParts": 7,
"skipExceptParts": [],
"skip": False,
"accounts_hash_difficulty": 4,
}
pipelines = []
@@ -1369,10 +1385,6 @@ def coreApiTestPipeline(ctx):
params["withRemotePhp"] = [True, False]
params["enableWatchFs"] = [True, False]
# override withRemotePhp if specified in the suite config
if "withRemotePhp" in matrix:
params["withRemotePhp"] = matrix["withRemotePhp"]
debugParts = params["skipExceptParts"]
debugPartsEnabled = (len(debugParts) != 0)
@@ -1394,6 +1406,7 @@ def coreApiTestPipeline(ctx):
restoreBuildArtifactCache(ctx, dirs["opencloudBinArtifact"], dirs["opencloudBinPath"]) +
opencloudServer(
storage,
params["accounts_hash_difficulty"],
with_wrapper = True,
watch_fs_enabled = run_with_watch_fs_enabled,
) +
@@ -2311,7 +2324,7 @@ def notifyMatrix(ctx):
return result
def opencloudServer(storage = "decomposed", depends_on = [], deploy_type = "", extra_server_environment = {}, with_wrapper = False, tika_enabled = False, watch_fs_enabled = False):
def opencloudServer(storage = "decomposed", accounts_hash_difficulty = 4, depends_on = [], deploy_type = "", extra_server_environment = {}, with_wrapper = False, tika_enabled = False, watch_fs_enabled = False):
user = "0:0"
container_name = OC_SERVER_NAME
environment = {
@@ -2407,6 +2420,13 @@ def opencloudServer(storage = "decomposed", depends_on = [], deploy_type = "", e
if watch_fs_enabled:
environment["STORAGE_USERS_POSIX_WATCH_FS"] = True
# Pass in "default" accounts_hash_difficulty to not set this environment variable.
# That will allow OpenCloud to use whatever its built-in default is.
# Otherwise pass in a value from 4 to about 11 or 12 (default 4, for making regular tests fast)
# The high values cause lots of CPU to be used when hashing passwords, and really slow down the tests.
if accounts_hash_difficulty != "default":
environment["ACCOUNTS_HASH_DIFFICULTY"] = accounts_hash_difficulty
for item in extra_server_environment:
environment[item] = extra_server_environment[item]

View File

@@ -1,82 +1,5 @@
# Changelog
## [5.0.0](https://github.com/opencloud-eu/opencloud/releases/tag/v5.0.0) - 2026-01-23
### ❤️ Thanks to all contributors! ❤️
@ScharfViktor, @butonic, @dragonchaser, @flimmy, @fschade, @micbar, @rhafer, @saw-jan
### 💥 Breaking changes
- merge ocdav into frontend [[#1958](https://github.com/opencloud-eu/opencloud/pull/1958)]
### ✅ Tests
- [test-only] replace exception to assertions [[#2196](https://github.com/opencloud-eu/opencloud/pull/2196)]
- test(api): auto-generate test virus files before test run [[#2191](https://github.com/opencloud-eu/opencloud/pull/2191)]
- test(api): remove accountsHashDifficulty test suite [[#2190](https://github.com/opencloud-eu/opencloud/pull/2190)]
- test(api): update without-remotephp expected-failures list [[#2184](https://github.com/opencloud-eu/opencloud/pull/2184)]
- [full-ci] test: use single command to run the containers and the API tests [[#2169](https://github.com/opencloud-eu/opencloud/pull/2169)]
- [tests-only] test: setup for running wopi API tests locally [[#2139](https://github.com/opencloud-eu/opencloud/pull/2139)]
- fix flaky #2145 [[#2161](https://github.com/opencloud-eu/opencloud/pull/2161)]
- Run wopi validator tests localy [[#2151](https://github.com/opencloud-eu/opencloud/pull/2151)]
- ci: fix unwanted workflow skip in the cron pipelines [[#2117](https://github.com/opencloud-eu/opencloud/pull/2117)]
- [POC] ci: skip previously passed workflows on pipeline restart [[#2099](https://github.com/opencloud-eu/opencloud/pull/2099)]
- [tests-only] test: wait post-processing to finish for MKCOL requests [[#2092](https://github.com/opencloud-eu/opencloud/pull/2092)]
- [tests-only] test: fix API tests [[#2087](https://github.com/opencloud-eu/opencloud/pull/2087)]
- [full-ci] use graph api in the enforcePasswordPublicLink.feature [[#2050](https://github.com/opencloud-eu/opencloud/pull/2050)]
- [full-ci][tests-only] test: check last email content with retries as emails can be delayed [[#2038](https://github.com/opencloud-eu/opencloud/pull/2038)]
- skip collaborativePosix tests in CI [[#2039](https://github.com/opencloud-eu/opencloud/pull/2039)]
### 📚 Documentation
- Update release template [[#2182](https://github.com/opencloud-eu/opencloud/pull/2182)]
- Clarify what the two requests are used for [[#2179](https://github.com/opencloud-eu/opencloud/pull/2179)]
- fix: markdown links formatting [[#2143](https://github.com/opencloud-eu/opencloud/pull/2143)]
### 🐛 Bug Fixes
- fix: Show username in unprivileged search results [[#2104](https://github.com/opencloud-eu/opencloud/pull/2104)]
- fix(thumbnailer): missing font panic [[#2097](https://github.com/opencloud-eu/opencloud/pull/2097)]
- Remove sub-service binary entrypoints and fix antivirus only server cmd [[#2043](https://github.com/opencloud-eu/opencloud/pull/2043)]
- fix(thumbnailer): respect image boundaries and text wrappings [[#2062](https://github.com/opencloud-eu/opencloud/pull/2062)]
- fix: cobra viper flags and env [[#2047](https://github.com/opencloud-eu/opencloud/pull/2047)]
- fix service name in suture logs [[#2052](https://github.com/opencloud-eu/opencloud/pull/2052)]
### 📈 Enhancement
- benchmark client enhancements [[#1856](https://github.com/opencloud-eu/opencloud/pull/1856)]
- allow http2 connections to proxy [[#2040](https://github.com/opencloud-eu/opencloud/pull/2040)]
- migrate from urfave/cli to spf13/cobra [[#1954](https://github.com/opencloud-eu/opencloud/pull/1954)]
### 📦️ Dependencies
- build(deps): bump github.com/go-resty/resty/v2 from 2.7.0 to 2.17.1 [[#2197](https://github.com/opencloud-eu/opencloud/pull/2197)]
- build(deps): bump github.com/open-policy-agent/opa from 1.11.1 to 1.12.3 [[#2166](https://github.com/opencloud-eu/opencloud/pull/2166)]
- build(deps): bump github.com/kovidgoyal/imaging from 1.8.18 to 1.8.19 [[#2167](https://github.com/opencloud-eu/opencloud/pull/2167)]
- build(deps): bump github.com/grpc-ecosystem/grpc-gateway/v2 from 2.27.3 to 2.27.4 [[#2164](https://github.com/opencloud-eu/opencloud/pull/2164)]
- build(deps): bump github.com/sirupsen/logrus from 1.9.4-0.20230606125235-dd1b4c2e81af to 1.9.4 [[#2163](https://github.com/opencloud-eu/opencloud/pull/2163)]
- build(deps): bump github.com/go-chi/chi/v5 from 5.2.3 to 5.2.4 [[#2162](https://github.com/opencloud-eu/opencloud/pull/2162)]
- build(deps): bump go.opentelemetry.io/contrib/zpages from 0.63.0 to 0.64.0 [[#2158](https://github.com/opencloud-eu/opencloud/pull/2158)]
- build(deps): bump github.com/blevesearch/bleve/v2 from 2.5.5 to 2.5.7 [[#2157](https://github.com/opencloud-eu/opencloud/pull/2157)]
- build(deps): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace from 1.38.0 to 1.39.0 [[#2154](https://github.com/opencloud-eu/opencloud/pull/2154)]
- build(deps): bump golang.org/x/image from 0.34.0 to 0.35.0 [[#2153](https://github.com/opencloud-eu/opencloud/pull/2153)]
- build(deps): bump github.com/nats-io/nats.go from 1.47.0 to 1.48.0 [[#2147](https://github.com/opencloud-eu/opencloud/pull/2147)]
- build(deps): bump github.com/onsi/ginkgo/v2 from 2.27.2 to 2.27.5 [[#2148](https://github.com/opencloud-eu/opencloud/pull/2148)]
- build(deps): bump github.com/olekukonko/tablewriter from 1.1.1 to 1.1.2 [[#2144](https://github.com/opencloud-eu/opencloud/pull/2144)]
- build(deps): bump github.com/spf13/cobra from 1.10.1 to 1.10.2 [[#2141](https://github.com/opencloud-eu/opencloud/pull/2141)]
- build(deps): bump golang.org/x/net from 0.48.0 to 0.49.0 [[#2140](https://github.com/opencloud-eu/opencloud/pull/2140)]
- build(deps): bump github.com/onsi/gomega from 1.38.2 to 1.39.0 [[#2133](https://github.com/opencloud-eu/opencloud/pull/2133)]
- build(deps): bump golang.org/x/crypto from 0.46.0 to 0.47.0 [[#2132](https://github.com/opencloud-eu/opencloud/pull/2132)]
- build(deps): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp from 0.63.0 to 0.64.0 [[#2109](https://github.com/opencloud-eu/opencloud/pull/2109)]
- build(deps): bump github.com/kovidgoyal/imaging from 1.8.17 to 1.8.18 [[#2107](https://github.com/opencloud-eu/opencloud/pull/2107)]
- build(deps): bump google.golang.org/grpc from 1.77.0 to 1.78.0 [[#2106](https://github.com/opencloud-eu/opencloud/pull/2106)]
- build(deps): bump go.opentelemetry.io/otel/sdk from 1.38.0 to 1.39.0 [[#2069](https://github.com/opencloud-eu/opencloud/pull/2069)]
- build(deps): bump github.com/opensearch-project/opensearch-go/v4 from 4.5.0 to 4.6.0 [[#2068](https://github.com/opencloud-eu/opencloud/pull/2068)]
- build(deps): bump github.com/testcontainers/testcontainers-go/modules/opensearch from 0.39.0 to 0.40.0 [[#1967](https://github.com/opencloud-eu/opencloud/pull/1967)]
- build(deps): bump golang.org/x/net from 0.47.0 to 0.48.0 [[#2061](https://github.com/opencloud-eu/opencloud/pull/2061)]
- build(deps): bump github.com/open-policy-agent/opa from 1.10.1 to 1.11.0 [[#1930](https://github.com/opencloud-eu/opencloud/pull/1930)]
## [4.1.0](https://github.com/opencloud-eu/opencloud/releases/tag/v4.1.0) - 2025-12-15
### ❤️ Thanks to all contributors! ❤️

View File

@@ -1,161 +0,0 @@
## Basic Settings ##
# Define the docker compose log driver used.
# Defaults to local
LOG_DRIVER=
# If you're on an internet facing server, comment out following line.
# It skips certificate validation for various parts of OpenCloud and is
# needed when self signed certificates are used.
INSECURE=true
## Features ##
COMPOSE_FILE=docker-compose.yml:traefik.yml:keycloak.yml:ldap-server.yml
## Traefik Settings ##
# Note: Traefik is always enabled and can't be disabled.
# Serve Traefik dashboard.
# Defaults to "false".
TRAEFIK_DASHBOARD=
# Domain of Traefik, where you can find the dashboard.
# Defaults to "traefik.opencloud.test"
TRAEFIK_DOMAIN=
# Basic authentication for the traefik dashboard.
# Defaults to user "admin" and password "admin" (written as: "admin:$2y$05$KDHu3xq92SPaO3G8Ybkc7edd51pPLJcG1nWk3lmlrIdANQ/B6r5pq").
# To create user:password pair, it's possible to use this command:
# echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g
TRAEFIK_BASIC_AUTH_USERS=
# Email address for obtaining LetsEncrypt certificates.
# Needs only be changed if this is a public facing server.
TRAEFIK_ACME_MAIL=
# Set to the following for testing to check the certificate process:
# "https://acme-staging-v02.api.letsencrypt.org/directory"
# With staging configured, there will be an SSL error in the browser.
# When certificates are displayed and are emitted by # "Fake LE Intermediate X1",
# the process went well and the envvar can be reset to empty to get valid certificates.
TRAEFIK_ACME_CASERVER=
# Enable the Traefik ACME (Automatic Certificate Management Environment) for automatic SSL certificate management.
TRAEFIK_SERVICES_TLS_CONFIG="tls.certresolver=letsencrypt"
# Enable Traefik to use local certificates.
#TRAEFIK_SERVICES_TLS_CONFIG="tls=true"
# You also need to provide a config file in ./config/traefik/dynamic/certs.yml
# Example:
# cat ./config/traefik/dynamic/certs.yml
# tls:
# certificates:
# - certFile: /certs/opencloud.test.crt
# keyFile: /certs/opencloud.test.key
# stores:
# - default
#
# The certificates need to copied into ./certs/, the absolute path inside the container is /certs/.
# You can also use TRAEFIK_CERTS_DIR=/path/on/host to set the path to the certificates directory.
# Enable the access log for Traefik by setting the following variable to true.
TRAEFIK_ACCESS_LOG=
# Configure the log level for Traefik.
# Possible values are "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL" and "PANIC". Default is "ERROR".
TRAEFIK_LOG_LEVEL=
## OpenCloud Settings ##
# The opencloud container image.
# For production releases: "opencloudeu/opencloud"
# For rolling releases: "opencloudeu/opencloud-rolling"
# Defaults to production if not set otherwise
OC_DOCKER_IMAGE=opencloudeu/opencloud-rolling
# The openCloud container version.
# Defaults to "latest" and points to the latest stable tag.
OC_DOCKER_TAG=
# Domain of openCloud, where you can find the frontend.
# Defaults to "cloud.opencloud.test"
OC_DOMAIN=
# Demo users should not be created on a production instance,
# because their passwords are public. Defaults to "false".
# If demo users is set to "true", the following user accounts are created automatically:
# alan, mary, margaret, dennis and lynn - the password is 'demo' for all.
DEMO_USERS=
# Admin Password for the OpenCloud admin user.
# NOTE: This is only needed when using the built-in LDAP server (idm).
# If you are using an external LDAP server, the admin password is managed by the LDAP server.
# NOTE: This variable needs to be set before the first start of OpenCloud. Changes to this variable after the first start will be IGNORED.
# If not set, opencloud will not work properly. The container will be restarting.
# After the first initialization, the admin password can only be changed via the OpenCloud User Settings UI or by using the OpenCloud CLI.
# Documentation: https://docs.opencloud.eu/docs/admin/resources/common-issues#-change-admin-password-set-in-env
INITIAL_ADMIN_PASSWORD=
# Define the openCloud loglevel used.
#
LOG_LEVEL=
# Define the kind of logging.
# The default log can be read by machines.
# Set this to true to make the log human readable.
# LOG_PRETTY=true
#
# Define the openCloud storage location. Set the paths for config and data to a local path.
# Ensure that the configuration and data directories are owned by the user and group with ID 1000:1000.
# This matches the default user inside the container and avoids permission issues when accessing files.
# Note that especially the data directory can grow big.
# Leaving it default stores data in docker internal volumes.
# OC_CONFIG_DIR=/your/local/opencloud/config
# OC_DATA_DIR=/your/local/opencloud/data
### Compose Configuration ###
# Path separator for supplemental compose files specified in COMPOSE_FILE.
COMPOSE_PATH_SEPARATOR=:
### Ldap Settings ###
# LDAP is always needed for OpenCloud to store user data as there is no relational database.
# The built-in LDAP server should used for testing purposes or small installations only.
# For production installations, it is recommended to use an external LDAP server.
# We are using OpenLDAP as the default LDAP server because it is proven to be stable and reliable.
# This LDAP configuration is known to work with OpenCloud and provides a blueprint for
# configuring an external LDAP server based on other products like Microsoft Active Directory or other LDAP servers.
#
# Password of LDAP bind user "cn=admin,dc=opencloud,dc=eu". Defaults to "admin"
LDAP_BIND_PASSWORD=
# The LDAP server also creates an openCloud admin user dn: uid=admin,ou=users,dc=opencloud,dc=eu
# The initial password for this user is "admin"
# NOTE: This password can only be set once, if you want to change it later, you have to use the OpenCloud User Settings UI.
# If you changed the password and lost it, you need to execute the following LDAP query to reset it:
# enter the ldap-server container with `docker compose exec ldap-server sh`
# and run the following command to change the password:
# ldappasswd -H ldap://127.0.0.1:1389 -D "cn=admin,dc=opencloud,dc=eu" -W "uid=admin,ou=users,dc=opencloud,dc=eu"
# You will be prompted for the LDAP bind password.
# The output should provide you a new password for the admin user.
### Keycloak Settings ###
# Keycloak is an open-source identity and access management solution.
# We are using Keycloak as the default identity provider on production installations.
# It can be used to federate authentication with other identity providers like
# Microsoft Entra ID, ADFS or other SAML/OIDC providers.
# The use of Keycloak as bridge between OpenCloud and other identity providers creates more control over the
# authentication process, the allowed clients and the session management.
# Keycloak also manages the Role Based Access Control (RBAC) for OpenCloud.
# Keycloak can be used in two different modes:
# 1. Autoprovisioning: New users are automatically created in openCloud when they log in for the first time.
# 2. Shared User Directory: Users are created in Keycloak and can be used in OpenCloud immediately
# because the LDAP server is connected to both Keycloak and OpenCloud.
# Only use one of the two modes at a time.
## Autoprovisioning Mode ##
# Use together with idm/external-idp.yml
# If you want to use a keycloak for local testing, you can use testing/external-keycloak.yml and testing/ldap-manager.yml
# Domain of your Identity Provider.
IDP_DOMAIN=
# IdP Issuer URL, which is used to identify the Identity Provider.
# We need the complete URL, including the protocol (http or https) and the realm.
# Example: "https://keycloak.opencloud.test/realms/openCloud"
IDP_ISSUER_URL=
# Url of the account edit page from your Identity Provider.
IDP_ACCOUNT_URL=
## Shared User Directory Mode ##
# Use together with idm/ldap-keycloak.yml and traefik/ldap-keycloak.yml
# Domain for Keycloak. Defaults to "keycloak.opencloud.test".
KEYCLOAK_DOMAIN=
# Admin user login name. Defaults to "kcadmin".
KEYCLOAK_ADMIN=
# Admin user login password. Defaults to "admin".
KEYCLOAK_ADMIN_PASSWORD=
# Keycloak Database username. Defaults to "keycloak".
KC_DB_USERNAME=
# Keycloak Database password. Defaults to "keycloak".
KC_DB_PASSWORD=

View File

@@ -1,49 +0,0 @@
# Development/Test Deployment for a multi-tenacy setup
The docker compose files in this directory are derived from the
opencloud-compose project and can be used to deploy a Development or Testing
environment for a multi-tenancy setup of OpenCloud. It consists of the
following services:
* `provisioning`: The OpenCloud graph service deployed in a standalone mode. It
is configured to provide the libregraph education API for managing tenants
and users. The `ldap-server`service (see below) is used to store the tenants
and users.
* `ldap-server`: An OpenLDAP server that is used by the provisioning service to
store tenants and users. Used by the OpenCloud services as the user directory
(for looking up users and searching for sharees).
* `keycloak`: The OpenID Connect Provider used for authenticating users. The
pre-loaded realm is configured to add `tenantid` claim into the identity and
access tokens. It's also currently consuming user from the `ldap-server`
(this federation will likely go away in the future and is optional for future
configurations).
* `opencloud`: The OpenCloud configured so that is hides users from different
tenants from each other.
To deploy the setup, run:
```bash
docker compose -f docker-compose.yml -f keycloak.yml -f ldap-server.yml -f traefik.yml up
```
Once deployed you can use the `initialize_users.go` to create a couple of example
tenants and some users in each tenant:
* Tenant `Famous Coders` with users `dennis` and `grace`
* Tenant `Scientists` with users `einstein` and `marie`
The passwords for the users is set to `demo` in keycloak
```
> go run initialize_users.go
Created tenant: Famous Coders with id fc58e19a-3a2a-4afc-90ec-8f94986db340
Created user: Dennis Ritchie with id ee1e14e7-b00b-4eec-8b03-a6bf0e29c77c
Created user: Grace Hopper with id a29f3afd-e4a3-4552-91e8-cc99e26bffce
Created tenant: Scientists with id 18406c53-e2d6-4e83-98b6-a55880eef195
Created user: Albert Einstein with id 12023d37-d6ce-4f19-a318-b70866f265ba
Created user: Marie Curie with id 30c3c825-c37d-4e85-8195-0142e4884872
Setting password for user: grace
Setting password for user: marie
Setting password for user: dennis
Setting password for user: einstein
```

View File

@@ -1,63 +0,0 @@
{
"clientId": "OpenCloudAndroid",
"name": "OpenCloud Android App",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"oc://android.opencloud.eu"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "oc://android.opencloud.eu",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View File

@@ -1,64 +0,0 @@
{
"clientId": "OpenCloudDesktop",
"name": "OpenCloud Desktop Client",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"http://127.0.0.1",
"http://localhost"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "+",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View File

@@ -1,63 +0,0 @@
{
"clientId": "OpenCloudIOS",
"name": "OpenCloud iOS App",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"oc://ios.opencloud.eu"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "oc://ios.opencloud.eu",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View File

@@ -1,66 +0,0 @@
{
"clientId": "Cyberduck",
"name": "Cyberduck",
"description": "File transfer utility client",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"x-cyberduck-action:oauth",
"x-mountainduck-action:oauth"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View File

@@ -1,74 +0,0 @@
{
"clientId": "web",
"name": "OpenCloud Web App",
"description": "",
"rootUrl": "{{OC_URL}}",
"adminUrl": "{{OC_URL}}",
"baseUrl": "",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"{{OC_URL}}/",
"{{OC_URL}}/oidc-callback.html",
"{{OC_URL}}/oidc-silent-redirect.html"
],
"webOrigins": [
"{{OC_URL}}"
],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.url": "{{OC_URL}}/backchannel_logout",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View File

@@ -1,8 +0,0 @@
#!/bin/bash
printenv
# replace openCloud domain and LDAP password in keycloak realm import
mkdir /opt/keycloak/data/import
sed -e "s/cloud.opencloud.test/${OC_DOMAIN}/g" -e "s/ldap-admin-password/${LDAP_ADMIN_PASSWORD:-admin}/g" /opt/keycloak/data/import-dist/openCloud-realm.json > /opt/keycloak/data/import/openCloud-realm.json
# run original docker-entrypoint
/opt/keycloak/bin/kc.sh "$@"

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
:root {
--pf-global--primary-color--100: #e2baff;
--pf-global--primary-color--200: #e2baff;
--pf-global--primary-color--dark-100: #e2baff;
--pf-global--Color--light-100: #20434f;
}
@font-face {
font-family: OpenCloud;
src: url('../fonts/OpenCloud500-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: OpenCloud;
src: url('../fonts/OpenCloud750-Bold.woff2') format('woff2');
font-weight: bold;
font-style: normal;
}
body {
font-family: "OpenCloud", "Open Sans", Helvetica, Arial, sans-serif;
background: url(../img/background.png) no-repeat center !important;
background-size: cover !important;
}
.kc-logo-text {
background-image: url(../img/logo.svg) !important;
background-size: contain;
width: 400px;
margin: 0 !important;
}
#kc-header-wrapper{
display: flex;
justify-content: center;
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,14 +0,0 @@
<svg width="170" height="35" viewBox="0 0 170 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.1928 23.7428C42.5766 22.2684 41.0547 19.5569 41.0547 16.1555C41.0547 14.4433 41.4115 12.921 42.1485 11.589C43.5753 8.92532 46.2869 7.35547 49.6879 7.35547C51.377 7.35547 52.8514 7.73604 54.1593 8.47339C56.7523 9.97188 58.2508 12.7307 58.2508 16.1555C58.2508 17.8443 57.894 19.3663 57.1801 20.6748C55.753 23.315 53.042 24.8605 49.6879 24.8605C47.9992 24.8605 46.501 24.504 45.1928 23.7428ZM49.6641 22.1254C52.9942 22.1254 55.1824 19.7947 55.1824 16.1555C55.1824 12.4215 52.8755 10.0667 49.6641 10.0667C46.4534 10.0667 44.1224 12.4215 44.1224 16.1555C44.1224 19.771 46.3345 22.1254 49.6641 22.1254Z" fill="#E2BAFF"/>
<path d="M62.0912 11.9934L63.6608 14.7522C65.0879 12.4929 66.967 11.9934 68.4176 11.9934C69.5828 11.9934 70.5821 12.255 71.4618 12.8259C72.3657 13.4202 73.0558 14.1814 73.5077 15.1328C74.0069 16.1318 74.2448 17.2259 74.2448 18.4387C74.2448 19.6518 74.0069 20.7697 73.5077 21.7687C73.0558 22.7201 72.3419 23.4813 71.4381 24.0283C70.5821 24.5989 69.5828 24.8605 68.4176 24.8605C67.6327 24.8605 66.8477 24.6943 66.0869 24.3613C65.3971 24.0283 64.7787 23.5526 64.2792 22.958V28.9992H61.3774V16.5599L59.5938 13.4443L62.0912 11.9934ZM64.2792 18.4387C64.2792 19.1764 64.4219 19.8658 64.7073 20.4605C65.0641 21.1027 65.4685 21.5546 65.9442 21.84C66.4913 22.1493 67.0856 22.292 67.7754 22.292C68.489 22.292 69.0836 22.1493 69.6069 21.84C70.1302 21.5308 70.5583 21.0789 70.8196 20.4605C71.1289 19.8658 71.2478 19.1764 71.2478 18.4387C71.2478 17.2497 70.9386 16.2983 70.3204 15.6323C69.6782 14.9187 68.8457 14.562 67.7516 14.562C66.7291 14.562 65.8728 14.9187 65.2306 15.6323C64.6122 16.2983 64.2792 17.2497 64.2792 18.4387Z" fill="#E2BAFF"/>
<path d="M76.5405 15.133C77.0162 14.1812 77.7297 13.3966 78.7049 12.7782C79.7039 12.2311 80.7981 11.9695 82.0587 11.9695C83.319 11.9695 84.3656 12.2311 85.1981 12.7306C86.1017 13.3015 86.768 14.0626 87.2437 15.0137C87.6715 15.9892 87.9094 17.1544 87.9094 18.4386L87.8621 19.1286H78.6098C78.7049 20.1987 79.0855 21.0077 79.7515 21.6261C80.4175 22.1491 81.2976 22.387 82.3438 22.387C83.1528 22.387 83.8661 22.2446 84.5318 21.9826C85.1981 21.721 85.7925 21.388 86.2685 20.9836C86.3398 21.0553 87.505 23.1005 87.505 23.1005C86.768 23.6476 85.9352 24.0758 85.0316 24.4084C84.1991 24.7177 83.2239 24.8604 82.1535 24.8604C80.8694 24.8604 79.7277 24.599 78.8001 24.052C77.8962 23.5287 77.1589 22.7675 76.5642 21.7689C76.0409 20.7696 75.7793 19.6516 75.7793 18.4386C75.7793 17.2496 76.0172 16.1317 76.5405 15.133ZM78.6811 17.1544H85.0316C84.9365 16.203 84.6273 15.5373 84.104 15.1089C83.5331 14.6097 82.8671 14.348 82.0587 14.348C81.2259 14.348 80.5126 14.6097 79.8704 15.0616C79.2996 15.5135 78.919 16.203 78.6811 17.1544Z" fill="#E2BAFF"/>
<path d="M102.054 17.0594V24.4799H99.1283V17.3211C99.1283 16.3459 98.9142 15.6561 98.4623 15.2042C98.0342 14.6809 97.3919 14.443 96.5121 14.443C95.9411 14.443 95.4656 14.5857 94.9899 14.895C94.538 15.1328 94.1812 15.5372 93.9433 16.0367C93.7293 16.5124 93.5866 17.0832 93.5866 17.773V24.4799H90.6371V16.5599L88.8535 13.4202L91.3745 11.9934L92.8492 14.5144C94.3477 12.2785 95.9648 11.9934 97.059 11.9934C98.2958 11.9934 99.0569 12.2072 99.5564 12.445C100.056 12.6832 100.508 12.9921 100.841 13.3727C101.649 14.2527 102.054 15.4896 102.054 17.0594Z" fill="#E2BAFF"/>
<path d="M117.538 23.838C117.015 24.0999 116.325 24.3613 115.469 24.5991C114.684 24.7656 113.851 24.8608 112.971 24.8608C111.259 24.8608 109.737 24.4802 108.429 23.7669C107.144 23.0531 106.145 22.0303 105.408 20.6748C104.694 19.3663 104.338 17.8443 104.338 16.1083C104.338 14.4195 104.694 12.8972 105.384 11.5414C106.074 10.2097 107.144 9.16318 108.405 8.47339C109.713 7.71225 111.283 7.35547 113.138 7.35547C114.066 7.35547 114.874 7.42683 115.564 7.61711C116.373 7.80739 117.038 8.04525 117.514 8.33068C118.014 8.56854 118.608 8.90153 119.298 9.35346L117.8 11.8982C117.062 11.3987 116.349 11.0185 115.659 10.733C114.85 10.4238 113.994 10.2573 113.066 10.2573C111.83 10.2573 110.807 10.4952 109.999 10.9944C109.118 11.5414 108.476 12.2312 108.048 13.0875C107.62 14.0151 107.382 15.0379 107.382 16.1555C107.382 17.2738 107.62 18.2725 108.048 19.1525C108.476 20.0088 109.118 20.6986 109.999 21.1981C110.831 21.6735 111.854 21.9117 113.043 21.9117C113.733 21.9117 114.351 21.84 114.922 21.6973C115.54 21.5308 116.064 21.3408 116.491 21.103C117.086 20.7937 117.538 20.5083 117.871 20.2467L119.346 22.8152C118.632 23.2434 118.037 23.6001 117.538 23.838Z" fill="#E2BAFF"/>
<rect x="121.127" y="7.23633" width="2.90154" height="17.2437" fill="#E2BAFF"/>
<path d="M126.195 18.4387C126.195 14.6809 128.859 11.9934 132.688 11.9934C136.494 11.9934 139.134 14.6571 139.134 18.4152C139.134 22.2206 136.494 24.8605 132.664 24.8605C129.002 24.8605 126.195 22.3633 126.195 18.4387ZM132.664 22.3633C134.71 22.3633 136.113 20.9124 136.113 18.4152C136.113 15.9891 134.71 14.4671 132.664 14.4671C130.548 14.4671 129.192 16.0604 129.192 18.4152C129.192 20.8411 130.572 22.3633 132.664 22.3633Z" fill="#E2BAFF"/>
<path d="M145.86 24.8606C144.456 24.8606 143.339 24.4324 142.482 23.5048C141.674 22.5774 141.27 21.3168 141.27 19.747V12.374H144.171V19.5329C144.171 20.4605 144.385 21.1741 144.837 21.6736C145.265 22.1493 145.931 22.3634 146.811 22.3634C147.382 22.3634 147.881 22.2444 148.334 21.9828C148.785 21.7212 149.118 21.3168 149.356 20.8173C149.618 20.294 149.737 19.6994 149.737 19.081V12.374H152.662V20.3654L154.422 23.4337L151.925 24.8606L150.522 22.4585C150.522 22.4585 149.974 23.1721 149.237 23.7905C148.334 24.5517 147.382 24.8606 145.86 24.8606Z" fill="#E2BAFF"/>
<path d="M156.104 15.1328C156.651 14.0624 157.293 13.2775 158.173 12.8259C159.053 12.2788 160.076 11.9696 161.17 11.9696C162.098 11.9696 162.859 12.1599 163.525 12.4929C164.191 12.8259 164.809 13.254 165.309 13.8959V7.23657H168.21V20.3415L169.947 23.4099L167.449 24.837L165.927 22.1968C165.927 22.1968 164.88 23.6712 163.525 24.3375C162.954 24.623 162.05 24.837 161.194 24.837C160.076 24.837 159.053 24.5751 158.173 24.0045C157.317 23.5526 156.651 22.7677 156.104 21.7687C155.604 20.7697 155.391 19.6993 155.391 18.4387C155.391 17.1546 155.604 16.0604 156.104 15.1328ZM158.363 18.4387C158.363 19.2474 158.506 19.8896 158.792 20.437C159.125 21.1027 159.553 21.5073 160.004 21.7925C160.528 22.1017 161.098 22.2682 161.812 22.2682C162.526 22.2682 163.144 22.1017 163.644 21.7925C164.143 21.5073 164.571 21.0789 164.88 20.437C165.166 19.8896 165.309 19.2474 165.309 18.4387C165.309 17.2497 164.999 16.2507 164.333 15.6323C163.763 14.9187 162.883 14.5385 161.836 14.5385C160.766 14.5385 159.933 14.9187 159.339 15.6323C158.673 16.2507 158.363 17.2497 158.363 18.4387Z" fill="#E2BAFF"/>
<path d="M13.4814 25.5141L14.9505 24.6659V18.5276L20.234 15.4772V13.785L18.7649 12.9368L13.4462 16.0076L8.20127 12.9794L6.73242 13.8273V15.5198L12.0159 18.5703V24.668L13.4814 25.5141Z" fill="#E2BAFF"/>
<path d="M26.9649 7.78377L13.4828 0L0 7.78408V11.1725L13.4824 3.38806L26.9649 11.1721V7.78377Z" fill="#E2BAFF"/>
<path d="M26.9646 23.8279L13.4821 31.612L0 23.8279V27.2163L13.4821 35L26.9646 27.2163V23.8279Z" fill="#E2BAFF"/>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -1,19 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
const setLogoUrl = (url) => {
const logoTextSelector = document.querySelector(".kc-logo-text");
if (!logoTextSelector) {
return
}
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
const parent = logoTextSelector.parentNode;
parent.insertBefore(link, logoTextSelector);
link.appendChild(logoTextSelector);
}
setLogoUrl('https://opencloud.eu')
});

View File

@@ -1,5 +0,0 @@
parent=keycloak
import=common/keycloak
styles=css/login.css css/theme.css
scripts=js/script.js

View File

@@ -1,9 +0,0 @@
#!/bin/bash
echo "Running custom LDAP entrypoint script..."
if [ ! -f /opt/bitnami/openldap/share/openldap.key ]
then
openssl req -x509 -newkey rsa:4096 -keyout /opt/bitnami/openldap/share/openldap.key -out /opt/bitnami/openldap/share/openldap.crt -sha256 -days 365 -batch -nodes
fi
# run original docker-entrypoint
/opt/bitnami/scripts/openldap/entrypoint.sh "$@"

View File

@@ -1,20 +0,0 @@
dn: dc=opencloud,dc=eu
objectClass: organization
objectClass: dcObject
dc: opencloud
o: openCloud
dn: ou=users,dc=opencloud,dc=eu
objectClass: organizationalUnit
ou: users
dn: cn=admin,dc=opencloud,dc=eu
objectClass: inetOrgPerson
objectClass: person
cn: admin
sn: admin
uid: ldapadmin
dn: ou=tenants,dc=opencloud,dc=eu
objectClass: organizationalUnit
ou: tenants

View File

@@ -1,20 +0,0 @@
dn: uid=admin,ou=users,dc=opencloud,dc=eu
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: admin
givenName: Admin
sn: Admin
cn: admin
displayName: Admin
description: An admin for this OpenCloud instance.
mail: admin@example.org
userPassword:: e1NTSEF9UWhmaFB3dERydTUydURoWFFObDRMbzVIckI3TkI5Nmo==
dn: cn=administrators,ou=groups,dc=opencloud,dc=eu
objectClass: groupOfNames
objectClass: top
cn: administrators
description: OpenCloud Administrators
member: uid=admin,ou=users,dc=opencloud,dc=eu

View File

@@ -1,44 +0,0 @@
directives:
child-src:
- '''self'''
connect-src:
- '''self'''
- 'blob:'
- 'https://${COMPANION_DOMAIN|companion.opencloud.test}/'
- 'wss://${COMPANION_DOMAIN|companion.opencloud.test}/'
- 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/'
- 'https://${IDP_DOMAIN|keycloak.opencloud.test}/'
default-src:
- '''none'''
font-src:
- '''self'''
frame-ancestors:
- '''self'''
frame-src:
- '''self'''
- 'blob:'
- 'https://embed.diagrams.net/'
# In contrary to bash and docker the default is given after the | character
- 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}/'
# This is needed for the external-sites web extension when embedding sites
- 'https://docs.opencloud.eu'
img-src:
- '''self'''
- 'data:'
- 'blob:'
- 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/'
# In contrary to bash and docker the default is given after the | character
- 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}/'
manifest-src:
- '''self'''
media-src:
- '''self'''
object-src:
- '''self'''
- 'blob:'
script-src:
- '''self'''
- '''unsafe-inline'''
style-src:
- '''self'''
- '''unsafe-inline'''

View File

@@ -1,40 +0,0 @@
# This adds four additional routes to the proxy. Forwarding
# request on '/carddav/', '/caldav/' and the respective '/.well-knwown'
# endpoints to the radicale container and setting the required headers.
additional_policies:
- name: default
routes:
- endpoint: /caldav/
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /caldav
- endpoint: /.well-known/caldav
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /caldav
- endpoint: /carddav/
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /carddav
- endpoint: /.well-known/carddav
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /carddav
# To enable the radicale web UI add this rule.
# "unprotected" is True because the Web UI itself ask for
# the password.
# Also set "type" to "internal" in the config/radicale/config
# - endpoint: /caldav/.web/
# backend: http://radicale:5232/
# unprotected: true
# skip_x_access_token: true
# additional_headers:
# - X-Script-Name: /caldav

View File

@@ -1,72 +0,0 @@
set -e
printenv
# Function to add arguments to the command
add_arg() {
TRAEFIK_CMD="$TRAEFIK_CMD $1"
}
# Initialize the base command
TRAEFIK_CMD="traefik"
# Base Traefik arguments (from your existing configuration)
add_arg "--log.level=${TRAEFIK_LOG_LEVEL:-ERROR}"
# enable dashboard
add_arg "--api.dashboard=true"
# define entrypoints
add_arg "--entryPoints.http.address=:80"
add_arg "--entryPoints.http.http.redirections.entryPoint.to=https"
add_arg "--entryPoints.http.http.redirections.entryPoint.scheme=https"
add_arg "--entryPoints.https.address=:443"
# change default timeouts for long-running requests
# this is needed for webdav clients that do not support the TUS protocol
add_arg "--entryPoints.https.transport.respondingTimeouts.readTimeout=12h"
add_arg "--entryPoints.https.transport.respondingTimeouts.writeTimeout=12h"
add_arg "--entryPoints.https.transport.respondingTimeouts.idleTimeout=3m"
# docker provider (get configuration from container labels)
add_arg "--providers.docker.endpoint=unix:///var/run/docker.sock"
add_arg "--providers.docker.exposedByDefault=false"
# access log
add_arg "--accessLog=${TRAEFIK_ACCESS_LOG:-false}"
add_arg "--accessLog.format=json"
add_arg "--accessLog.fields.headers.names.X-Request-Id=keep"
# Add Let's Encrypt configuration if enabled
if [ "${TRAEFIK_SERVICES_TLS_CONFIG}" = "tls.certresolver=letsencrypt" ]; then
echo "Configuring Traefik with Let's Encrypt..."
add_arg "--certificatesResolvers.letsencrypt.acme.email=${TRAEFIK_ACME_MAIL:-example@example.org}"
add_arg "--certificatesResolvers.letsencrypt.acme.storage=/certs/acme.json"
add_arg "--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http"
add_arg "--certificatesResolvers.letsencrypt.acme.caserver=${TRAEFIK_ACME_CASERVER:-https://acme-v02.api.letsencrypt.org/directory}"
fi
# Add local certificate configuration if enabled
if [ "${TRAEFIK_SERVICES_TLS_CONFIG}" = "tls=true" ]; then
echo "Configuring Traefik with local certificates..."
add_arg "--providers.file.directory=/etc/traefik/dynamic"
add_arg "--providers.file.watch=true"
fi
# Warning if neither certificate method is enabled
if [ "${TRAEFIK_SERVICES_TLS_CONFIG}" != "tls=true" ] && [ "${TRAEFIK_SERVICES_TLS_CONFIG}" != "tls.certresolver=letsencrypt" ]; then
echo "WARNING: Neither Let's Encrypt nor local certificates are enabled."
echo "HTTPS will not work properly without certificate configuration."
fi
# Add any custom arguments from environment variable
if [ -n "${TRAEFIK_CUSTOM_ARGS}" ]; then
echo "Adding custom Traefik arguments: ${TRAEFIK_CUSTOM_ARGS}"
TRAEFIK_CMD="$TRAEFIK_CMD $TRAEFIK_CUSTOM_ARGS"
fi
# Add any additional arguments passed to the script
for arg in "$@"; do
add_arg "$arg"
done
# Print the final command for debugging
echo "Starting Traefik with command:"
echo "$TRAEFIK_CMD"
# Execute Traefik
exec $TRAEFIK_CMD

View File

@@ -1,119 +0,0 @@
---
services:
# OpenCloud instance configured for multi-tenancy using keycloak as identity provider
# The graph service is setup to consume users via the CS3 API.
opencloud:
image: ${OC_DOCKER_IMAGE:-opencloudeu/opencloud-rolling}:${OC_DOCKER_TAG:-latest}
# changelog: https://github.com/opencloud-eu/opencloud/tree/main/changelog
# release notes: https://docs.opencloud.eu/opencloud_release_notes.html
networks:
opencloud-net:
entrypoint:
- /bin/sh
# run opencloud init to initialize a configuration file with random secrets
# it will fail on subsequent runs, because the config file already exists
# therefore we ignore the error and then start the opencloud server
command: ["-c", "opencloud init || true; opencloud server"]
environment:
OC_MULTI_TENANT_ENABLED: "true"
# enable services that are not started automatically
OC_URL: https://${OC_DOMAIN:-cloud.opencloud.test}
OC_LOG_LEVEL: ${LOG_LEVEL:-info}
OC_LOG_COLOR: "${LOG_PRETTY:-false}"
OC_LOG_PRETTY: "${LOG_PRETTY:-false}"
OC_EXCLUDE_RUN_SERVICES: idm,idp
PROXY_ROLE_ASSIGNMENT_DRIVER: "oidc"
OC_OIDC_ISSUER: https://${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}/realms/openCloud
PROXY_OIDC_REWRITE_WELLKNOWN: "true"
WEB_OIDC_CLIENT_ID: ${OC_OIDC_CLIENT_ID:-web}
PROXY_USER_OIDC_CLAIM: "uuid"
PROXY_USER_CS3_CLAIM: "userid"
WEB_OPTION_ACCOUNT_EDIT_LINK_HREF: "https://${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}/realms/openCloud/account"
# admin and demo accounts must be created in Keycloak
OC_ADMIN_USER_ID: ""
SETTINGS_SETUP_DEFAULT_ASSIGNMENTS: "false"
GRAPH_ASSIGN_DEFAULT_USER_ROLE: "false"
GRAPH_USERNAME_MATCH: "none"
GROUPS_DRIVER: "null"
# This is needed to set the correct CSP rules for OpenCloud
IDP_DOMAIN: ${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}
# do not use SSL between the reverse proxy and OpenCloud
PROXY_TLS: "false"
# INSECURE: needed if OpenCloud / reverse proxy is using self generated certificates
OC_INSECURE: "${INSECURE:-false}"
# basic auth (not recommended, but needed for eg. WebDav clients that do not support OpenID Connect)
PROXY_ENABLE_BASIC_AUTH: "false"
GRAPH_IDENTITY_BACKEND: "cs3"
PROXY_CSP_CONFIG_FILE_LOCATION: /etc/opencloud/csp.yaml
OC_LDAP_URI: ldaps://ldap-server:1636
OC_LDAP_INSECURE: "true"
OC_LDAP_BIND_DN: "cn=admin,dc=opencloud,dc=eu"
OC_LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-admin}
OC_LDAP_USER_BASE_DN: "ou=users,dc=opencloud,dc=eu"
OC_LDAP_USER_SCHEMA_TENANT_ID: "openCloudMemberOfSchool"
PROXY_LOG_LEVEL: "debug"
volumes:
- ./config/opencloud/csp.yaml:/etc/opencloud/csp.yaml
# configure the .env file to use own paths instead of docker internal volumes
- ${OC_CONFIG_DIR:-opencloud-config}:/etc/opencloud
- ${OC_DATA_DIR:-opencloud-data}:/var/lib/opencloud
logging:
driver: ${LOG_DRIVER:-local}
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.opencloud.entrypoints=https"
- "traefik.http.routers.opencloud.rule=Host(`${OC_DOMAIN:-cloud.opencloud.test}`)"
- "traefik.http.routers.opencloud.service=opencloud"
- "traefik.http.services.opencloud.loadbalancer.server.port=9200"
- "traefik.http.routers.opencloud.${TRAEFIK_SERVICES_TLS_CONFIG}"
# Stand-alone instance of the 'graph' service to serve the provisioning API
provsioning:
image: ${OC_DOCKER_IMAGE:-opencloudeu/opencloud-rolling}:${OC_DOCKER_TAG:-latest}
networks:
opencloud-net:
entrypoint:
- /bin/sh
# run opencloud init to initialize a configuration file with random secrets
# it will fail on subsequent runs, because the config file already exists
# therefore we ignore the error and then start the opencloud server
command: ["-c", "opencloud init || true; opencloud graph server"]
environment:
OC_LOG_LEVEL: "debug"
OC_LOG_COLOR: "${LOG_PRETTY:-false}"
OC_LOG_PRETTY: "${LOG_PRETTY:-false}"
# This just runs the standalone graph service we don't need access to the registry
MICRO_REGISTRY: "memory"
# INSECURE: needed if OpenCloud / reverse proxy is using self generated certificates
OC_INSECURE: "${INSECURE:-false}"
GRAPH_HTTP_ADDR: "0.0.0.0:9120"
GRAPH_HTTP_API_TOKEN: "${PROVISIONING_API_TOKEN:-changeme}"
# disable listening for events
GRAPH_EVENTS_ENDPOINT: ""
GRAPH_STORE_NODES: ""
GRAPH_ASSIGN_DEFAULT_USER_ROLE: "false"
GRAPH_USERNAME_MATCH: "none"
GRAPH_LDAP_EDUCATION_RESOURCES_ENABLED: "true"
GRAPH_LDAP_SCHOOL_BASE_DN: "ou=tenants,dc=opencloud,dc=eu"
OC_LDAP_URI: ldaps://ldap-server:1636
OC_LDAP_INSECURE: "true"
OC_LDAP_BIND_DN: "cn=admin,dc=opencloud,dc=eu"
OC_LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-admin}
OC_LDAP_USER_BASE_DN: "ou=users,dc=opencloud,dc=eu"
OC_LDAP_USER_FILTER: "(objectclass=inetOrgPerson)"
volumes:
# configure the .env file to use own paths instead of docker internal volumes
- ${PROVISIONING_CONFIG_DIR:-provisioning-config}:/etc/opencloud
logging:
driver: ${LOG_DRIVER:-local}
restart: always
ports:
- "9120:9120"
volumes:
opencloud-config:
opencloud-data:
provisioning-config:
networks:
opencloud-net:

View File

@@ -1,169 +0,0 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"github.com/Nerzal/gocloak/v13"
"github.com/go-resty/resty/v2"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
const (
provisioningAPIURL = "http://localhost:9120/graph"
provisioningAuthToken = "changeme"
)
type tenantWithUsers struct {
tenant libregraph.EducationSchool
users []libregraph.EducationUser
}
var demoTenants = []tenantWithUsers{
{
tenant: libregraph.EducationSchool{
DisplayName: libregraph.PtrString("Famous Coders"),
},
users: []libregraph.EducationUser{
{
DisplayName: libregraph.PtrString("Dennis Ritchie"),
OnPremisesSamAccountName: libregraph.PtrString("dennis"),
Mail: libregraph.PtrString("dennis@example.org"),
},
{
DisplayName: libregraph.PtrString("Grace Hopper"),
OnPremisesSamAccountName: libregraph.PtrString("grace"),
Mail: libregraph.PtrString("grace@example.org"),
},
},
},
{
tenant: libregraph.EducationSchool{
DisplayName: libregraph.PtrString("Scientists"),
},
users: []libregraph.EducationUser{
{
DisplayName: libregraph.PtrString("Albert Einstein"),
OnPremisesSamAccountName: libregraph.PtrString("einstein"),
Mail: libregraph.PtrString("einstein@example.org"),
},
{
DisplayName: libregraph.PtrString("Marie Curie"),
OnPremisesSamAccountName: libregraph.PtrString("marie"),
Mail: libregraph.PtrString("marie@example.org"),
},
},
},
}
func main() {
lgconf := libregraph.NewConfiguration()
lgconf.Servers = libregraph.ServerConfigurations{
{
URL: provisioningAPIURL,
},
}
lgconf.DefaultHeader = map[string]string{"Authorization": "Bearer " + provisioningAuthToken}
lgclient := libregraph.NewAPIClient(lgconf)
for _, tenant := range demoTenants {
tenantid, err := createTenant(lgclient, tenant.tenant)
if err != nil {
fmt.Printf("Failed to create tenant: %s\n", err)
continue
}
for _, user := range tenant.users {
userid1, err := createUser(lgclient, user)
if err != nil {
fmt.Printf("Failed to create user: %s\n", err)
continue
}
_, err = lgclient.EducationSchoolApi.AddUserToSchool(context.TODO(), tenantid).EducationUserReference(libregraph.EducationUserReference{
OdataId: libregraph.PtrString(fmt.Sprintf("%s/education/users/%s", provisioningAPIURL, userid1)),
}).Execute()
if err != nil {
fmt.Printf("Failed to add user to tenant: %s\n", err)
continue
}
}
}
resetAllUserPasswords()
setUserRoles()
}
func createUser(client *libregraph.APIClient, user libregraph.EducationUser) (string, error) {
newUser, _, err := client.EducationUserApi.CreateEducationUser(context.TODO()).EducationUser(user).Execute()
if err != nil {
fmt.Printf("Failed to create user: %s\n", err)
return "", err
}
fmt.Printf("Created user: %s with id %s\n", newUser.GetDisplayName(), newUser.GetId())
return newUser.GetId(), nil
}
func createTenant(client *libregraph.APIClient, tenant libregraph.EducationSchool) (string, error) {
newTenant, _, err := client.EducationSchoolApi.CreateSchool(context.TODO()).EducationSchool(tenant).Execute()
if err != nil {
fmt.Printf("Failed to create tenant: %s\n", err)
return "", err
}
fmt.Printf("Created tenant: %s with id %s\n", newTenant.GetDisplayName(), newTenant.GetId())
return newTenant.GetId(), nil
}
func setUserRoles() {
tls := tls.Config{InsecureSkipVerify: true}
restyClient := resty.New().SetTLSClientConfig(&tls)
client := gocloak.NewClient("https://keycloak.opencloud.test")
client.SetRestyClient(restyClient)
ctx := context.Background()
token, err := client.LoginAdmin(ctx, "kcadmin", "admin", "master")
if err != nil {
fmt.Printf("Failed to login: %s\n", err)
panic("Something wrong with the credentials or url")
}
role, _ := client.GetRealmRole(ctx, token.AccessToken, "openCloud", "opencloudUser")
users, err := client.GetUsers(ctx, token.AccessToken, "openCloud", gocloak.GetUsersParams{})
if err != nil {
fmt.Printf("%s\n", err)
panic("Oh no!, failed to list users :(")
}
for _, user := range users {
err := client.AddRealmRoleToUser(ctx, token.AccessToken, "openCloud", *user.ID, []gocloak.Role{
*role,
})
if err != nil {
fmt.Printf("Failed to assign role to user %s: %s\n", *user.Username, err)
}
}
}
func resetAllUserPasswords() {
tls := tls.Config{InsecureSkipVerify: true}
restyClient := resty.New().SetTLSClientConfig(&tls)
client := gocloak.NewClient("https://keycloak.opencloud.test")
client.SetRestyClient(restyClient)
ctx := context.Background()
token, err := client.LoginAdmin(ctx, "kcadmin", "admin", "master")
if err != nil {
fmt.Printf("Failed to login: %s\n", err)
panic("Something wrong with the credentials or url")
}
users, err := client.GetUsers(ctx, token.AccessToken, "openCloud", gocloak.GetUsersParams{})
if err != nil {
fmt.Printf("%s\n", err)
panic("Oh no!, failed to list users :(")
}
for _, user := range users {
fmt.Printf("Setting password for user: %s\n", *user.Username)
err = client.SetPassword(ctx, token.AccessToken, *user.ID, "openCloud", "demo", false)
if err != nil {
fmt.Printf("Failed to set password for user %s: %s\n", *user.Username, err)
}
}
}

View File

@@ -1,55 +0,0 @@
---
services:
opencloud:
environment:
postgres:
image: postgres:alpine
networks:
opencloud-net:
volumes:
- keycloak_postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: ${KC_DB_USERNAME:-keycloak}
POSTGRES_PASSWORD: ${KC_DB_PASSWORD:-keycloak}
logging:
driver: ${LOG_DRIVER:-local}
restart: always
keycloak:
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.entrypoints=https"
- "traefik.http.routers.keycloak.rule=Host(`${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}`)"
- "traefik.http.routers.keycloak.${TRAEFIK_SERVICES_TLS_CONFIG}"
- "traefik.http.routers.keycloak.service=keycloak"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
image: quay.io/keycloak/keycloak:26.4
networks:
opencloud-net:
command: [ "start", "--spi-connections-http-client-default-disable-trust-manager=${INSECURE:-false}", "--import-realm" ]
entrypoint: [ "/bin/sh", "/opt/keycloak/bin/docker-entrypoint-override.sh" ]
volumes:
- "./config/keycloak/docker-entrypoint-override.sh:/opt/keycloak/bin/docker-entrypoint-override.sh"
- "./config/keycloak/openCloud-realm.dist.json:/opt/keycloak/data/import-dist/openCloud-realm.json"
- "./config/keycloak/themes/opencloud:/opt/keycloak/themes/opencloud"
environment:
OC_DOMAIN: ${OC_DOMAIN:-cloud.opencloud.test}
KC_HOSTNAME: ${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}
KC_DB: postgres
KC_DB_URL: "jdbc:postgresql://postgres:5432/keycloak"
KC_DB_USERNAME: ${KC_DB_USERNAME:-keycloak}
KC_DB_PASSWORD: ${KC_DB_PASSWORD:-keycloak}
KC_FEATURES: impersonation
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: true
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-kcadmin}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
depends_on:
- postgres
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
keycloak_postgres_data:

View File

@@ -1,32 +0,0 @@
---
services:
ldap-server:
image: bitnamilegacy/openldap:2.6
networks:
opencloud-net:
entrypoint: [ "/bin/sh", "/opt/bitnami/scripts/openldap/docker-entrypoint-override.sh", "/opt/bitnami/scripts/openldap/run.sh" ]
environment:
BITNAMI_DEBUG: true
LDAP_TLS_VERIFY_CLIENT: never
LDAP_ENABLE_TLS: "yes"
LDAP_TLS_CA_FILE: /opt/bitnami/openldap/share/openldap.crt
LDAP_TLS_CERT_FILE: /opt/bitnami/openldap/share/openldap.crt
LDAP_TLS_KEY_FILE: /opt/bitnami/openldap/share/openldap.key
LDAP_ROOT: "dc=opencloud,dc=eu"
LDAP_ADMIN_PASSWORD: ${LDAP_BIND_PASSWORD:-admin}
ports:
- "127.0.0.1:389:1389"
- "127.0.0.1:636:1636"
volumes:
# Only use the base ldif file to create the base structure
- ./config/ldap/ldif/10_base.ldif:/ldifs/10_base.ldif
# Use the custom schema from opencloud because we are in full control of the ldap server
- ../shared/config/ldap/schemas/10_opencloud_schema.ldif:/schemas/10_opencloud_schema.ldif
- ../shared/config/ldap/schemas/20_opencloud_education_schema.ldif:/schemas/20_opencloud_education_schema.ldif
- ./config/ldap/docker-entrypoint-override.sh:/opt/bitnami/scripts/openldap/docker-entrypoint-override.sh
- ${LDAP_CERTS_DIR:-ldap-certs}:/opt/bitnami/openldap/share
- ${LDAP_DATA_DIR:-ldap-data}:/bitnami/openldap
volumes:
ldap-certs:
ldap-data:

View File

@@ -1,24 +0,0 @@
---
# This file can be used to be added to the opencloud_full example
# to browse the LDAP server with a web interface.
# This is not a production ready setup.
services:
ldap-manager:
image: phpldapadmin/phpldapadmin:latest
networks:
opencloud-net:
environment:
LDAP_HOST: ldap-server
LDAP_PORT: 1389
LDAP_LOGIN_OBJECTCLASS: "inetOrgPerson"
APP_URL: "https://${LDAP_MANAGER_DOMAIN:-ldap.opencloud.test}"
labels:
- "traefik.enable=true"
- "traefik.http.routers.ldap-manager.entrypoints=https"
- "traefik.http.routers.ldap-manager.rule=Host(`${LDAP_MANAGER_DOMAIN:-ldap.opencloud.test}`)"
- "traefik.http.routers.ldap-manager.${TRAEFIK_SERVICES_TLS_CONFIG}"
- "traefik.http.routers.ldap-manager.service=ldap-manager"
- "traefik.http.services.ldap-manager.loadbalancer.server.port=8080"
logging:
driver: ${LOG_DRIVER:-local}
restart: always

View File

@@ -1,45 +0,0 @@
---
services:
opencloud:
labels:
- "traefik.enable=true"
- "traefik.http.routers.opencloud.entrypoints=https"
- "traefik.http.routers.opencloud.rule=Host(`${OC_DOMAIN:-cloud.opencloud.test}`)"
- "traefik.http.routers.opencloud.service=opencloud"
- "traefik.http.services.opencloud.loadbalancer.server.port=9200"
- "traefik.http.routers.opencloud.${TRAEFIK_SERVICES_TLS_CONFIG}"
traefik:
image: traefik:v3.3.1
# release notes: https://github.com/traefik/traefik/releases
networks:
opencloud-net:
aliases:
- ${OC_DOMAIN:-cloud.opencloud.test}
- ${KEYCLOAK_DOMAIN:-keycloak.opencloud.test}
entrypoint: [ "/bin/sh", "/opt/traefik/bin/docker-entrypoint-override.sh"]
environment:
- "TRAEFIK_SERVICES_TLS_CONFIG=${TRAEFIK_SERVICES_TLS_CONFIG:-tls.certresolver=letsencrypt}"
- "TRAEFIK_ACME_MAIL=${TRAEFIK_ACME_MAIL:-example@example.org}"
- "TRAEFIK_ACME_CASERVER=${TRAEFIK_ACME_CASERVER:-https://acme-v02.api.letsencrypt.org/directory}"
- "TRAEFIK_LOG_LEVEL=${TRAEFIK_LOG_LEVEL:-ERROR}"
- "TRAEFIK_ACCESS_LOG=${TRAEFIK_ACCESS_LOG:-false}"
ports:
- "80:80"
- "443:443"
volumes:
- "${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock:ro"
- "./config/traefik/docker-entrypoint-override.sh:/opt/traefik/bin/docker-entrypoint-override.sh"
- "${TRAEFIK_CERTS_DIR:-./certs}:/certs"
- "./config/traefik/dynamic:/etc/traefik/dynamic"
labels:
- "traefik.enable=${TRAEFIK_DASHBOARD:-false}"
# defaults to admin:admin
- "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_BASIC_AUTH_USERS:-admin:$$apr1$$4vqie50r$$YQAmQdtmz5n9rEALhxJ4l.}"
- "traefik.http.routers.traefik.entrypoints=https"
- "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DOMAIN:-traefik.opencloud.test}`)"
- "traefik.http.routers.traefik.middlewares=traefik-auth"
- "traefik.http.routers.traefik.${TRAEFIK_SERVICES_TLS_CONFIG}"
- "traefik.http.routers.traefik.service=api@internal"
logging:
driver: ${LOG_DRIVER:-local}
restart: always

25
go.mod
View File

@@ -34,7 +34,6 @@ require (
github.com/go-micro/plugins/v4/wrapper/monitoring/prometheus v1.2.0
github.com/go-micro/plugins/v4/wrapper/trace/opentelemetry v1.2.0
github.com/go-playground/validator/v10 v10.30.1
github.com/go-resty/resty/v2 v2.17.1
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang/protobuf v1.5.4
github.com/google/go-cmp v0.7.0
@@ -58,14 +57,14 @@ require (
github.com/nats-io/nats-server/v2 v2.12.3
github.com/nats-io/nats.go v1.48.0
github.com/oklog/run v1.2.0
github.com/olekukonko/tablewriter v1.1.2
github.com/olekukonko/tablewriter v1.1.3
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.27.5
github.com/onsi/gomega v1.39.0
github.com/open-policy-agent/opa v1.12.3
github.com/open-policy-agent/opa v1.11.1
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
github.com/opencloud-eu/reva/v2 v2.41.1-0.20260120144836-2769c3c07a19
github.com/opencloud-eu/reva/v2 v2.41.1-0.20260107152322-93760b632993
github.com/opensearch-project/opensearch-go/v4 v4.6.0
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
@@ -125,7 +124,7 @@ require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
@@ -166,7 +165,7 @@ require (
github.com/ceph/go-ceph v0.37.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cevaris/ordered_map v0.0.0-20190319150403-3adeae072e73 // indirect
github.com/clipperhouse/displaywidth v0.6.0 // indirect
github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
@@ -222,6 +221,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
@@ -250,7 +250,6 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-plugin v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
@@ -289,10 +288,10 @@ require (
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/mileusna/useragent v1.3.5 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/crc64nvme v1.1.0 // indirect
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.98 // indirect
github.com/minio/minio-go/v7 v7.0.97 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
@@ -313,7 +312,7 @@ require (
github.com/nxadm/tail v1.4.8 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.1.3 // indirect
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
@@ -328,7 +327,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/pquerna/cachecontrol v0.2.0 // indirect
github.com/prometheus/alertmanager v0.30.1 // indirect
github.com/prometheus/alertmanager v0.30.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
@@ -343,7 +342,7 @@ require (
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/kafka-go v0.4.50 // indirect
github.com/segmentio/kafka-go v0.4.49 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/sercand/kuberesolver/v5 v5.1.1 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
@@ -363,7 +362,7 @@ require (
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect

51
go.sum
View File

@@ -65,8 +65,8 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CiscoM31/godata v1.0.11 h1:w7y8twuW02LdH6mak3/GJ5i0GrCv2IoZUJVqa/g5Yeo=
github.com/CiscoM31/godata v1.0.11/go.mod h1:ZMiT6JuD3Rm83HEtiTx4JEChsd25YCrxchKGag/sdTc=
@@ -223,8 +223,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
@@ -461,8 +461,8 @@ github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -651,8 +651,6 @@ github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
@@ -853,14 +851,14 @@ github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk=
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
@@ -942,11 +940,11 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg=
github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc=
github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg=
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@@ -959,8 +957,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/open-policy-agent/opa v1.12.3 h1:qe3m/w52baKC/HJtippw+hYBUKCzuBCPjB+D5P9knfc=
github.com/open-policy-agent/opa v1.12.3/go.mod h1:RnDgm04GA1RjEXJvrsG9uNT/+FyBNmozcPvA2qz60M4=
github.com/open-policy-agent/opa v1.11.1 h1:4bMlG6DjRZTRAswRyF+KUCgxHu1Gsk0h9EbZ4W9REvM=
github.com/open-policy-agent/opa v1.11.1/go.mod h1:QimuJO4T3KYxWzrmAymqlFvsIanCjKrGjmmC8GgAdgE=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a h1:Sakl76blJAaM6NxylVkgSzktjo2dS504iDotEFJsh3M=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY=
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+lP5lUUIzjRGDg93WrQfZJZCaV1ZP3KeyXi8bzY=
@@ -969,8 +967,8 @@ github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9 h1:dIft
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76 h1:vD/EdfDUrv4omSFjrinT8Mvf+8D7f9g4vgQ2oiDrVUI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.41.1-0.20260120144836-2769c3c07a19 h1:8loHHe7FYd7zgIcGTlbHwre+bU/AAwREEYVd4SWM9/s=
github.com/opencloud-eu/reva/v2 v2.41.1-0.20260120144836-2769c3c07a19/go.mod h1:pv+w23JG0/qJweZbTzNNev//YEvlUML1L/2iXgKGkkg=
github.com/opencloud-eu/reva/v2 v2.41.1-0.20260107152322-93760b632993 h1:qWU0bKhD1wqQIq6giMTvUUbG1IlaT/lzchLDSjuedi0=
github.com/opencloud-eu/reva/v2 v2.41.1-0.20260107152322-93760b632993/go.mod h1:foXaMxugUi4TTRsK3AAXRAb/kyFd4A9k2+wNv+p+vbU=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -1022,8 +1020,8 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:Om
github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k=
github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/alertmanager v0.30.1 h1:427prmCHuy1rMmV7fl/TVQFh5A/78XQ/Mp+TsswZNGM=
github.com/prometheus/alertmanager v0.30.1/go.mod h1:93PBumcTLr/gNtNtM0m7BcCffbvYP5bKuLBWiOnISaA=
github.com/prometheus/alertmanager v0.30.0 h1:E4dnxSFXK8V2Bb8iqudlisTmaIrF3hRJSWnliG08tBM=
github.com/prometheus/alertmanager v0.30.0/go.mod h1:93PBumcTLr/gNtNtM0m7BcCffbvYP5bKuLBWiOnISaA=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
@@ -1113,8 +1111,8 @@ github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/kafka-go v0.4.50 h1:mcyC3tT5WeyWzrFbd6O374t+hmcu1NKt2Pu1L3QaXmc=
github.com/segmentio/kafka-go v0.4.50/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
github.com/segmentio/kafka-go v0.4.49 h1:GJiNX1d/g+kG6ljyJEoi9++PUMdXGAxb7JGPiDCuNmk=
github.com/segmentio/kafka-go v0.4.49/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY=
@@ -1226,8 +1224,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
@@ -1464,6 +1462,7 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/opencloud-eu/teams/204053/pt/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/opencloud-eu/teams/204053/nl/)\n"

View File

@@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Radoslaw Posim, 2025\n"
"Language-Team: Polish (https://app.transifex.com/opencloud-eu/teams/204053/pl/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/opencloud-eu/teams/204053/pt/)\n"

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Lulufox, 2025\n"
"Language-Team: Russian (https://app.transifex.com/opencloud-eu/teams/204053/ru/)\n"

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: miguel tapias, 2025\n"
"Language-Team: Spanish (https://app.transifex.com/opencloud-eu/teams/204053/es/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-23 00:11+0000\n"
"POT-Creation-Date: 2026-01-03 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/opencloud-eu/teams/204053/fi/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/opencloud-eu/teams/204053/nl/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/opencloud-eu/teams/204053/pt/)\n"

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Lulufox, 2025\n"
"Language-Team: Russian (https://app.transifex.com/opencloud-eu/teams/204053/ru/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-23 00:11+0000\n"
"POT-Creation-Date: 2026-01-03 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/opencloud-eu/teams/204053/fi/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: idoet <idoet@protonmail.ch>, 2025\n"
"Language-Team: Indonesian (https://app.transifex.com/opencloud-eu/teams/204053/id/)\n"

View File

@@ -13,7 +13,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Radoslaw Posim, 2025\n"
"Language-Team: Polish (https://app.transifex.com/opencloud-eu/teams/204053/pl/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/opencloud-eu/teams/204053/pt/)\n"

View File

@@ -158,7 +158,6 @@ func metadataDrivers(localEndpoint string, cfg *config.Config) map[string]interf
"permissionssvc": localEndpoint,
"max_acquire_lock_cycles": cfg.Drivers.Decomposed.MaxAcquireLockCycles,
"lock_cycle_duration_factor": cfg.Drivers.Decomposed.LockCycleDurationFactor,
"multi_tenant_enabled": false, // storage-system doesn't use tenants, even if it's enabled for storage-users
"disable_versioning": true,
"statcache": map[string]interface{}{
"cache_store": "noop",

View File

@@ -100,7 +100,6 @@ func Posix(cfg *config.Config, enableFSScan, enableFSWatch bool) map[string]inte
"scan_debounce_delay": cfg.Drivers.Posix.ScanDebounceDelay,
"max_quota": cfg.Drivers.Posix.MaxQuota,
"disable_versioning": cfg.Drivers.Posix.DisableVersioning,
"multi_tenant_enabled": cfg.Commons.MultiTenantEnabled,
"propagator": cfg.Drivers.Posix.Propagator,
"async_propagator_options": map[string]interface{}{
"propagation_delay": cfg.Drivers.Posix.AsyncPropagatorOptions.PropagationDelay,
@@ -204,7 +203,6 @@ func Decomposed(cfg *config.Config) map[string]interface{} {
"asyncfileuploads": cfg.Drivers.Decomposed.AsyncUploads,
"max_quota": cfg.Drivers.Decomposed.MaxQuota,
"disable_versioning": cfg.Drivers.Decomposed.DisableVersioning,
"multi_tenant_enabled": cfg.Commons.MultiTenantEnabled,
"filemetadatacache": map[string]interface{}{
"cache_store": cfg.FilemetadataCache.Store,
"cache_nodes": cfg.FilemetadataCache.Nodes,
@@ -259,7 +257,6 @@ func DecomposedNoEvents(cfg *config.Config) map[string]interface{} {
"max_concurrency": cfg.Drivers.Decomposed.MaxConcurrency,
"max_quota": cfg.Drivers.Decomposed.MaxQuota,
"disable_versioning": cfg.Drivers.Decomposed.DisableVersioning,
"multi_tenant_enabled": cfg.Commons.MultiTenantEnabled,
"filemetadatacache": map[string]interface{}{
"cache_store": cfg.FilemetadataCache.Store,
"cache_nodes": cfg.FilemetadataCache.Nodes,
@@ -315,7 +312,6 @@ func DecomposedS3(cfg *config.Config) map[string]interface{} {
"lock_cycle_duration_factor": cfg.Drivers.DecomposedS3.LockCycleDurationFactor,
"max_concurrency": cfg.Drivers.DecomposedS3.MaxConcurrency,
"disable_versioning": cfg.Drivers.DecomposedS3.DisableVersioning,
"multi_tenant_enabled": cfg.Commons.MultiTenantEnabled,
"asyncfileuploads": cfg.Drivers.DecomposedS3.AsyncUploads,
"filemetadatacache": map[string]interface{}{
"cache_store": cfg.FilemetadataCache.Store,
@@ -374,7 +370,6 @@ func DecomposedS3NoEvents(cfg *config.Config) map[string]interface{} {
"max_acquire_lock_cycles": cfg.Drivers.DecomposedS3.MaxAcquireLockCycles,
"max_concurrency": cfg.Drivers.DecomposedS3.MaxConcurrency,
"disable_versioning": cfg.Drivers.DecomposedS3.DisableVersioning,
"multi_tenant_enabled": cfg.Commons.MultiTenantEnabled,
"lock_cycle_duration_factor": cfg.Drivers.DecomposedS3.LockCycleDurationFactor,
"filemetadatacache": map[string]interface{}{
"cache_store": cfg.FilemetadataCache.Store,

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/opencloud-eu/teams/204053/nl/)\n"

View File

@@ -13,7 +13,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Radoslaw Posim, 2025\n"
"Language-Team: Polish (https://app.transifex.com/opencloud-eu/teams/204053/pl/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-01-22 00:11+0000\n"
"POT-Creation-Date: 2026-01-02 00:06+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/opencloud-eu/teams/204053/pt/)\n"

View File

@@ -466,6 +466,15 @@ For antivirus running localy on Linux OS, use `ANTIVIRUS_CLAMAV_SOCKET= "/var/ru
For antivirus running localy on Mac OS, use `ANTIVIRUS_CLAMAV_SOCKET= "/tmp/clamd.sock"`.
For antivirus running with docker, use `ANTIVIRUS_CLAMAV_SOCKET= "tcp://host.docker.internal:3310"`
#### Create Virus Files
The antivirus tests require EICAR test files which are not stored in the repository
They are generated dynamically when needed for testing.
```bash
tests/acceptance/scripts/generate-virus-files.sh
```
#### Run the Acceptance Test
Run the acceptance test with the following command:

View File

@@ -62,7 +62,7 @@ class HttpRequestHelper {
/**
*
* @param string $url
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $method
* @param string|null $user
@@ -80,8 +80,8 @@ class HttpRequestHelper {
* @throws GuzzleException
*/
public static function sendRequestOnce(
string $url,
?string $xRequestId = null,
?string $url,
?string $xRequestId,
?string $method = 'GET',
?string $user = null,
?string $password = null,
@@ -100,7 +100,7 @@ class HttpRequestHelper {
$parsedUrl = parse_url($url);
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
$baseUrl .= isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
$testUrl = $baseUrl . "/graph/v1.0/me";
$testUrl = $baseUrl . "/graph/v1.0/user/$user";
if (OcHelper::isTestingOnReva()) {
$url = $baseUrl . "/ocs/v2.php/cloud/users/$user";
}

View File

@@ -23,7 +23,7 @@ namespace TestHelpers;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\Assert;
use Exception;
/**
* Helper for obtaining bearer tokens for users
@@ -143,15 +143,21 @@ class TokenHelper {
]
);
Assert::assertEquals(
200,
$response->getStatusCode(),
'Token refresh failed: Expected status code 200 but received ' . $response->getStatusCode()
);
if ($response->getStatusCode() !== 200) {
throw new Exception(
\sprintf(
'Token refresh failed: Expected status code 200 but received %d. Message: %s',
$response->getStatusCode(),
$response->getReasonPhrase()
)
);
}
$data = json_decode($response->getBody()->getContents(), true);
Assert::assertArrayHasKey('access_token', $data, 'Missing access_token in refresh response');
Assert::assertArrayHasKey('refresh_token', $data, 'Missing refresh_token in refresh response');
if (!isset($data['access_token']) || !isset($data['refresh_token'])) {
throw new Exception('Missing tokens in refresh response');
}
return [
'access_token' => $data['access_token'],
@@ -247,24 +253,21 @@ class TokenHelper {
): string {
$response = self::makeLoginRequest($username, $password, $baseUrl, $cookieJar);
Assert::assertEquals(
200,
$response->getStatusCode(),
'Logon failed: Expected status code 200 but received: ' . $response->getStatusCode()
);
if ($response->getStatusCode() !== 200) {
throw new Exception(
\sprintf(
'Logon failed: Expected status code 200 but received %d. Message: %s',
$response->getStatusCode(),
$response->getReasonPhrase()
)
);
}
$data = json_decode($response->getBody()->getContents(), true);
Assert::assertArrayHasKey(
'hello',
$data,
'Logon response does not contain "hello" object'
);
Assert::assertArrayHasKey(
'continue_uri',
$data['hello'],
'Missing continue_uri in logon response'
);
if (!isset($data['hello']['continue_uri'])) {
throw new Exception('Missing continue_uri in logon response');
}
return $data['hello']['continue_uri'];
}
@@ -306,17 +309,42 @@ class TokenHelper {
]
);
Assert::assertEquals(
302,
$response->getStatusCode(),
'Authorization request failed: Expected status code 302 but received: ' . $response->getStatusCode()
);
if ($response->getStatusCode() !== 302) {
// Add debugging to understand what is happening
$body = $response->getBody()->getContents();
throw new Exception(
\sprintf(
'Authorization failed: Expected status code 302 but received %d. Message: %s. Body: %s',
$response->getStatusCode(),
$response->getReasonPhrase(),
$body
)
);
}
$location = $response->getHeader('Location')[0] ?? '';
Assert::assertNotEmpty($location, 'Missing Location header in authorization response');
if (empty($location)) {
throw new Exception('Missing Location header in authorization response');
}
parse_str(parse_url($location, PHP_URL_QUERY), $queryParams);
Assert::assertArrayHasKey('code', $queryParams, 'Missing code parameter in redirect URL');
// Check for errors
if (isset($queryParams['error'])) {
throw new Exception(
\sprintf(
'Authorization error: %s - %s',
$queryParams['error'],
urldecode($queryParams['error_description'] ?? 'No description')
)
);
}
if (!isset($queryParams['code'])) {
throw new Exception('Missing auth code in redirect URL. Location: ' . $location);
}
return $queryParams['code'];
}
@@ -355,15 +383,21 @@ class TokenHelper {
]
);
Assert::assertEquals(
200,
$response->getStatusCode(),
'Token request failed: Expected status code 200 but received: ' . $response->getStatusCode()
);
if ($response->getStatusCode() !== 200) {
throw new Exception(
\sprintf(
'Token request failed: Expected status code 200 but received %d. Message: %s',
$response->getStatusCode(),
$response->getReasonPhrase()
)
);
}
$data = json_decode($response->getBody()->getContents(), true);
Assert::assertArrayHasKey('access_token', $data, 'Missing access_token in token response');
Assert::assertArrayHasKey('refresh_token', $data, 'Missing refresh_token in token response');
if (!isset($data['access_token']) || !isset($data['refresh_token'])) {
throw new Exception('Missing tokens in response');
}
return [
'access_token' => $data['access_token'],

View File

@@ -195,20 +195,13 @@ class UploadHelper extends Assert {
}
/**
* get the path of the acceptance tests directory
* get the path of a file from FilesForUpload directory
*
* @param string|null $name name of the file to upload
*
* @return string
*/
public static function getAcceptanceTestsDir(): string {
return \dirname(__FILE__) . "/../";
}
/**
* get the path of the filesForUpload directory
*
* @return string
*/
public static function getFilesForUploadDir(): string {
return \dirname(__FILE__) . "/../filesForUpload/";
public static function getUploadFilesDir(?string $name): string {
return \getenv("FILES_FOR_UPLOAD") . $name;
}
}

View File

@@ -25,7 +25,6 @@ use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
use TestHelpers\UploadHelper;
require_once 'bootstrap.php';
@@ -50,7 +49,7 @@ class ChecksumContext implements Context {
string $checksum
): ResponseInterface {
$file = \file_get_contents(
UploadHelper::getAcceptanceTestsDir() . $source
$this->featureContext->acceptanceTestsDirLocation() . $source
);
return $this->featureContext->makeDavRequest(
$user,

View File

@@ -43,7 +43,6 @@ use TestHelpers\WebDavHelper;
use TestHelpers\SettingsHelper;
use TestHelpers\OcConfigHelper;
use TestHelpers\BehatHelper;
use TestHelpers\UploadHelper;
use Swaggest\JsonSchema\InvalidValue as JsonSchemaException;
use Swaggest\JsonSchema\Exception\ArrayException;
use Swaggest\JsonSchema\Exception\ConstException;
@@ -563,38 +562,6 @@ class FeatureContext extends BehatVariablesContext {
}
}
/**
* @BeforeScenario @antivirus
*
* @return void
* @throws Exception
*/
public function createTestVirusFiles(): void {
$uploadDir = UploadHelper::getFilesForUploadDir() . 'filesWithVirus/';
$virusFile = $uploadDir . 'eicar.com';
$virusZipFile = $uploadDir . 'eicar_com.zip';
if (file_exists($virusFile) && file_exists($virusZipFile)) {
return;
}
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755);
}
$res1 = HttpRequestHelper::sendRequestOnce('https://secure.eicar.org/eicar.com');
if ($res1->getStatusCode() !== 200) {
throw new Exception("Could not download eicar.com test virus file");
}
file_put_contents($virusFile, $res1->getBody()->getContents());
$res2 = HttpRequestHelper::sendRequestOnce('https://secure.eicar.org/eicar_com.zip');
file_put_contents($virusZipFile, $res2->getBody()->getContents());
if ($res2->getStatusCode() !== 200) {
throw new Exception("Could not download eicar_com.zip test virus file");
}
}
/**
*
* @BeforeScenario
@@ -2629,11 +2596,18 @@ class FeatureContext extends BehatVariablesContext {
return "work_tmp";
}
/**
* @return string
*/
public function acceptanceTestsDirLocation(): string {
return \dirname(__FILE__) . "/../";
}
/**
* @return string
*/
public function workStorageDirLocation(): string {
return UploadHelper::getAcceptanceTestsDir() . $this->temporaryStorageSubfolderName() . "/";
return $this->acceptanceTestsDirLocation() . $this->temporaryStorageSubfolderName() . "/";
}
/**

View File

@@ -26,7 +26,6 @@ use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
use TestHelpers\UploadHelper;
require_once 'bootstrap.php';
@@ -863,7 +862,7 @@ class PublicWebDavContext implements Context {
string $destination,
): void {
$content = \file_get_contents(
UploadHelper::getAcceptanceTestsDir() . $source
$this->featureContext->acceptanceTestsDirLocation() . $source
);
$response = $this->publicUploadContent(
$destination,
@@ -889,7 +888,7 @@ class PublicWebDavContext implements Context {
string $password
): void {
$content = \file_get_contents(
UploadHelper::getAcceptanceTestsDir() . $source
$this->featureContext->acceptanceTestsDirLocation() . $source
);
$response = $this->publicUploadContent(
$destination,

View File

@@ -32,7 +32,6 @@ use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
use TestHelpers\UploadHelper;
require_once 'bootstrap.php';
@@ -365,7 +364,7 @@ class TUSContext implements Context {
$client->setChecksumAlgorithm('sha1');
$client->setApiPath(WebDavHelper::getDavPath($davPathVersion, $suffixPath));
$client->setMetadata($uploadMetadata);
$sourceFile = UploadHelper::getAcceptanceTestsDir() . $source;
$sourceFile = $this->featureContext->acceptanceTestsDirLocation() . $source;
$client->setKey((string)rand())->file($sourceFile, $destination);
$this->featureContext->pauseUploadDelete();
@@ -519,7 +518,7 @@ class TUSContext implements Context {
*/
public function writeDataToTempFile(string $content): string {
$temporaryFileName = \tempnam(
UploadHelper::getAcceptanceTestsDir(),
$this->featureContext->acceptanceTestsDirLocation(),
"tus-upload-test-"
);
if ($temporaryFileName === false) {

View File

@@ -1648,7 +1648,7 @@ trait WebDav {
?bool $isGivenStep = false
): ResponseInterface {
$user = $this->getActualUsername($user);
$file = \fopen(UploadHelper::getAcceptanceTestsDir() . $source, 'r');
$file = \fopen($this->acceptanceTestsDirLocation() . $source, 'r');
$this->pauseUploadDelete();
$response = $this->makeDavRequest(
$user,
@@ -1781,7 +1781,7 @@ trait WebDav {
}
return $this->uploadFileWithHeaders(
$user,
UploadHelper::getAcceptanceTestsDir() . $source,
$this->acceptanceTestsDirLocation() . $source,
$destination,
$headers,
$noOfChunks
@@ -2222,7 +2222,7 @@ trait WebDav {
$this->getBaseUrl(),
$user,
$this->getPasswordForUser($user),
UploadHelper::getAcceptanceTestsDir() . $source,
$this->acceptanceTestsDirLocation() . $source,
$destination,
$this->getStepLineRef(),
["X-OC-Mtime" => $mtime],
@@ -2257,7 +2257,7 @@ trait WebDav {
$this->getBaseUrl(),
$user,
$this->getPasswordForUser($user),
UploadHelper::getAcceptanceTestsDir() . $source,
$this->acceptanceTestsDirLocation() . $source,
$destination,
$this->getStepLineRef(),
["X-OC-Mtime" => $mtime],

View File

@@ -3,9 +3,9 @@ default:
"": "%paths.base%/../bootstrap"
suites:
apiSpaces:
apiAccountsHashDifficulty:
paths:
- "%paths.base%/../features/apiSpaces"
- "%paths.base%/../features/apiAccountsHashDifficulty"
context: &common_ldap_suite_context
parameters:
ldapAdminPassword: admin
@@ -18,6 +18,21 @@ default:
adminPassword: admin
regularUserPassword: 123456
- SettingsContext:
- GraphContext:
- SpacesContext:
- CapabilitiesContext:
- FilesVersionsContext:
- NotificationContext:
- OCSContext:
- PublicWebDavContext:
apiSpaces:
paths:
- "%paths.base%/../features/apiSpaces"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SettingsContext:
- SpacesContext:
- CapabilitiesContext:
- FilesVersionsContext:
@@ -427,7 +442,7 @@ default:
- AuthAppContext:
- CliContext:
- OcConfigContext:
apiTenancy:
paths:
- "%paths.base%/../features/apiTenancy"

View File

@@ -14,6 +14,7 @@ services:
SHARING_USER_JSON_FILE: /srv/app/tmp/opencloud/shares.json
PROXY_ENABLE_BASIC_AUTH: "true"
WEB_UI_CONFIG_FILE: /woodpecker/src/github.com/opencloud-eu/opencloud/tests/config/woodpecker/opencloud-config.json
ACCOUNTS_HASH_DIFFICULTY: 4
OC_INSECURE: "true"
IDM_CREATE_DEMO_USERS: "true"
IDM_ADMIN_PASSWORD: "admin"

View File

@@ -19,6 +19,8 @@
#### [Settings service user can list other peoples assignments](https://github.com/owncloud/ocis/issues/5032)
- [apiAccountsHashDifficulty/assignRole.feature:27](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature#L27)
- [apiAccountsHashDifficulty/assignRole.feature:28](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature#L28)
- [apiGraph/getAssignedRole.feature:31](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L31)
- [apiGraph/getAssignedRole.feature:32](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L32)
- [apiGraph/getAssignedRole.feature:33](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L33)

View File

@@ -19,6 +19,8 @@
#### [Settings service user can list other peoples assignments](https://github.com/owncloud/ocis/issues/5032)
- [apiAccountsHashDifficulty/assignRole.feature:27](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature#L27)
- [apiAccountsHashDifficulty/assignRole.feature:28](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature#L28)
- [apiGraph/getAssignedRole.feature:31](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L31)
- [apiGraph/getAssignedRole.feature:32](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L32)
- [apiGraph/getAssignedRole.feature:33](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L33)

View File

@@ -201,9 +201,9 @@
- [apiAntivirus/antivirus.feature:143](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L143)
- [apiAntivirus/antivirus.feature:144](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L144)
- [apiAntivirus/antivirus.feature:145](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L145)
- [apiAntivirus/antivirus.feature:356](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L356)
- [apiAntivirus/antivirus.feature:357](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L357)
- [apiAntivirus/antivirus.feature:358](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L358)
- [apiAntivirus/antivirus.feature:359](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L359)
- [apiCollaboration/wopi.feature:956](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L956)
- [apiCollaboration/wopi.feature:957](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L957)
- [apiCollaboration/wopi.feature:958](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L958)
@@ -320,6 +320,7 @@
- [coreApiWebdavUploadTUS/uploadFile.feature:122](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L122)
- [coreApiWebdavUploadTUS/uploadFile.feature:133](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L133)
- [coreApiWebdavUploadTUS/uploadFile.feature:146](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L146)
- [coreApiWebdavUploadTUS/uploadFile.feature:168](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L168)
- [coreApiWebdavUploadTUS/uploadFile.feature:187](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L187)
- [coreApiWebdavUploadTUS/uploadFile.feature:199](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L199)
- [coreApiWebdavUploadTUS/uploadFile.feature:212](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L212)

View File

@@ -0,0 +1,16 @@
@skipOnReva
Feature: add user
As an admin
I want to be able to add users and store their password with the full hash difficulty
So that I can give people controlled individual access to resources on the OpenCloud server
Scenario: admin creates a user
When the user "Admin" creates a new user with the following attributes using the Graph API:
| userName | brand-new-user |
| displayName | Brand New User |
| email | new@example.org |
| password | %alt1% |
Then the HTTP status code should be "201"
And user "brand-new-user" should exist
And user "brand-new-user" should be able to upload file "filesForUpload/lorem.txt" to "lorem.txt"

View File

@@ -0,0 +1,64 @@
Feature: assign role
As an admin,
I want to assign roles to users
So that I can provide them different authority
Scenario Outline: only admin user can see all existing roles
Given user "Alice" has been created with default attributes
And the administrator has given "Alice" the role "<user-role>" using the settings api
When user "Alice" tries to get all existing roles using the settings API
Then the HTTP status code should be "<http-status-code>"
Examples:
| user-role | http-status-code |
| Admin | 201 |
| Space Admin | 201 |
| User | 201 |
@issue-5032
Scenario Outline: only admin user can see assignments list
Given user "Alice" has been created with default attributes
And the administrator has given "Alice" the role "<user-role>" using the settings api
When user "Alice" tries to get list of assignment using the settings API
Then the HTTP status code should be "<http-status-code>"
Examples:
| user-role | http-status-code |
| Admin | 201 |
| Space Admin | 401 |
| User | 401 |
Scenario Outline: a user cannot change own role
Given user "Alice" has been created with default attributes
And the administrator has given "Alice" the role "<user-role>" using the settings api
When user "Alice" changes his own role to "<desired-role>"
Then the HTTP status code should be "400"
And user "Alice" should have the role "<user-role>"
Examples:
| user-role | desired-role |
| Admin | User |
| Admin | Space Admin |
| Space Admin | Admin |
| Space Admin | Space Admin |
| User | Admin |
| User | Space Admin |
Scenario Outline: only admin user can change the role for another user
Given these users have been created with default attributes:
| username |
| Alice |
| Brian |
And the administrator has given "Alice" the role "<user-role>" using the settings api
When user "Alice" changes the role "<desired-role>" for user "Brian"
Then the HTTP status code should be "<http-status-code>"
And user "Brian" should have the role "<expected-role>"
Examples:
| user-role | desired-role | http-status-code | expected-role |
| Admin | User | 201 | User |
| Admin | Space Admin | 201 | Space Admin |
| Admin | Admin | 201 | Admin |
| Space Admin | Admin | 400 | User |
| Space Admin | Space Admin | 400 | User |
| User | Admin | 400 | User |
| User | Space Admin | 400 | User |

View File

@@ -0,0 +1,18 @@
@skipOnReva
Feature: sharing
As a user
I want to be able to share files when passwords are stored with the full hash difficulty
So that I can give people secure controlled access to my data
Scenario Outline: creating a share of a file with a user
Given using OCS API version "<ocs-api-version>"
And user "Alice" has been created with default attributes
And user "Alice" has uploaded file with content "OpenCloud test text file 0" to "/textfile0.txt"
And user "Brian" has been created with default attributes
When user "Alice" shares file "textfile0.txt" with user "Brian" using the sharing API
And the content of file "/Shares/textfile0.txt" for user "Brian" should be "OpenCloud test text file 0"
Examples:
| ocs-api-version |
| 1 |
| 2 |

View File

@@ -0,0 +1,21 @@
@skipOnReva
Feature: upload file
As a user
I want to be able to upload files when passwords are stored with the full hash difficulty
So that I can store and share files securely between multiple client systems
Scenario Outline: upload a file and check download content
Given using OCS API version "<ocs-api-version>"
And user "Alice" has been created with default attributes
And using <dav-path-version> DAV path
When user "Alice" uploads file with content "uploaded content" to "/upload.txt" using the WebDAV API
Then the content of file "/upload.txt" for user "Alice" should be "uploaded content"
Examples:
| ocs-api-version | dav-path-version |
| 1 | old |
| 1 | new |
| 1 | spaces |
| 2 | old |
| 2 | new |
| 2 | spaces |

View File

@@ -0,0 +1,31 @@
@skipOnReva
Feature: attempt to PUT files with invalid password
As an admin
I want the system to be secure when passwords are stored with the full hash difficulty
So that unauthorised users do not have access to data
Background:
Given user "Alice" has been created with default attributes
And user "Alice" has created folder "/PARENT"
Scenario: send PUT requests to webDav endpoints as normal user with wrong password
When user "Alice" requests these endpoints with "PUT" including body "doesnotmatter" using password "invalid" about user "Alice"
| endpoint |
| /webdav/textfile0.txt |
| /dav/files/%username%/textfile0.txt |
| /webdav/PARENT |
| /dav/files/%username%/PARENT |
| /dav/files/%username%/PARENT/parent.txt |
Then the HTTP status code of responses on all endpoints should be "401"
Scenario: send PUT requests to webDav endpoints as normal user with no password
When user "Alice" requests these endpoints with "PUT" including body "doesnotmatter" using password "" about user "Alice"
| endpoint |
| /webdav/textfile0.txt |
| /dav/files/%username%/textfile0.txt |
| /webdav/PARENT |
| /dav/files/%username%/PARENT |
| /dav/files/%username%/PARENT/parent.txt |
Then the HTTP status code of responses on all endpoints should be "401"

View File

@@ -21,7 +21,7 @@ Feature: List upload sessions via CLI command
And the CLI response should not contain these entries:
| file0.txt |
@antivirus
Scenario: list all upload sessions that are currently in postprocessing
Given the following configs have been set:
| config | value |
@@ -39,7 +39,7 @@ Feature: List upload sessions via CLI command
And the CLI response should not contain these entries:
| virusFile.txt |
@antivirus
Scenario: list all upload sessions that are infected by virus
Given the following configs have been set:
| config | value |
@@ -109,7 +109,7 @@ Feature: List upload sessions via CLI command
And the CLI response should not contain these entries:
| file2.txt |
@antivirus
Scenario: clean all upload sessions that are not in post-processing
Given the following configs have been set:
| config | value |
@@ -126,7 +126,7 @@ Feature: List upload sessions via CLI command
And the CLI response should not contain these entries:
| file1.txt |
@antivirus
Scenario: clean upload sessions that are not in post-processing and is not virus infected
Given the following configs have been set:
| config | value |

View File

@@ -0,0 +1,19 @@
#!/bin/bash
# tests/acceptance/scripts/generate-virus-files.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET_DIR="$SCRIPT_DIR/../filesForUpload/filesWithVirus"
echo "Generating EICAR test files..."
mkdir -p "$TARGET_DIR"
cd "$TARGET_DIR"
echo "Downloading eicar.com..."
curl -s -o eicar.com https://secure.eicar.org/eicar.com
echo "Downloading eicar_com.zip..."
curl -s -o eicar_com.zip https://secure.eicar.org/eicar_com.zip

View File

@@ -547,6 +547,7 @@ fi
export IPV4_URL
export IPV6_URL
export FILES_FOR_UPLOAD="${SCRIPT_PATH}/filesForUpload/"
TEST_LOG_FILE=$(mktemp)
SCENARIOS_THAT_PASSED=0

View File

@@ -64,6 +64,7 @@ SUITES=(
"apiSharingNgShareInvitation"
"apiSharingNgLinkSharePermission"
"apiSharingNgLinkShareRoot"
"apiAccountsHashDifficulty"
"apiSearchContent"
"apiNotification"
)

View File

@@ -1,7 +1,7 @@
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
reflection interface similar to Go's standard library `json` and `xml` packages.
Compatible with TOML version [v1.1.0](https://toml.io/en/v1.1.0).
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
Documentation: https://pkg.go.dev/github.com/BurntSushi/toml

View File

@@ -206,13 +206,6 @@ func markDecodedRecursive(md *MetaData, tmap map[string]any) {
markDecodedRecursive(md, tmap)
md.context = md.context[0 : len(md.context)-1]
}
if tarr, ok := tmap[key].([]map[string]any); ok {
for _, elm := range tarr {
md.context = append(md.context, key)
markDecodedRecursive(md, elm)
md.context = md.context[0 : len(md.context)-1]
}
}
}
}
@@ -430,7 +423,7 @@ func (md *MetaData) unifyString(data any, rv reflect.Value) error {
if i, ok := data.(int64); ok {
rv.SetString(strconv.FormatInt(i, 10))
} else if f, ok := data.(float64); ok {
rv.SetString(strconv.FormatFloat(f, 'g', -1, 64))
rv.SetString(strconv.FormatFloat(f, 'f', -1, 64))
} else {
return md.badtype("string", data)
}

View File

@@ -228,9 +228,9 @@ func (enc *Encoder) eElement(rv reflect.Value) {
}
switch v.Location() {
default:
enc.write(v.Format(format))
enc.wf(v.Format(format))
case internal.LocalDatetime, internal.LocalDate, internal.LocalTime:
enc.write(v.In(time.UTC).Format(format))
enc.wf(v.In(time.UTC).Format(format))
}
return
case Marshaler:
@@ -279,40 +279,40 @@ func (enc *Encoder) eElement(rv reflect.Value) {
case reflect.String:
enc.writeQuoted(rv.String())
case reflect.Bool:
enc.write(strconv.FormatBool(rv.Bool()))
enc.wf(strconv.FormatBool(rv.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
enc.write(strconv.FormatInt(rv.Int(), 10))
enc.wf(strconv.FormatInt(rv.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
enc.write(strconv.FormatUint(rv.Uint(), 10))
enc.wf(strconv.FormatUint(rv.Uint(), 10))
case reflect.Float32:
f := rv.Float()
if math.IsNaN(f) {
if math.Signbit(f) {
enc.write("-")
enc.wf("-")
}
enc.write("nan")
enc.wf("nan")
} else if math.IsInf(f, 0) {
if math.Signbit(f) {
enc.write("-")
enc.wf("-")
}
enc.write("inf")
enc.wf("inf")
} else {
enc.write(floatAddDecimal(strconv.FormatFloat(f, 'g', -1, 32)))
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
}
case reflect.Float64:
f := rv.Float()
if math.IsNaN(f) {
if math.Signbit(f) {
enc.write("-")
enc.wf("-")
}
enc.write("nan")
enc.wf("nan")
} else if math.IsInf(f, 0) {
if math.Signbit(f) {
enc.write("-")
enc.wf("-")
}
enc.write("inf")
enc.wf("inf")
} else {
enc.write(floatAddDecimal(strconv.FormatFloat(f, 'g', -1, 64)))
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
}
case reflect.Array, reflect.Slice:
enc.eArrayOrSliceElement(rv)
@@ -330,32 +330,27 @@ func (enc *Encoder) eElement(rv reflect.Value) {
// By the TOML spec, all floats must have a decimal with at least one number on
// either side.
func floatAddDecimal(fstr string) string {
for _, c := range fstr {
if c == 'e' { // Exponent syntax
return fstr
}
if c == '.' {
return fstr
}
if !strings.Contains(fstr, ".") {
return fstr + ".0"
}
return fstr + ".0"
return fstr
}
func (enc *Encoder) writeQuoted(s string) {
enc.write(`"` + dblQuotedReplacer.Replace(s) + `"`)
enc.wf("\"%s\"", dblQuotedReplacer.Replace(s))
}
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
length := rv.Len()
enc.write("[")
enc.wf("[")
for i := 0; i < length; i++ {
elem := eindirect(rv.Index(i))
enc.eElement(elem)
if i != length-1 {
enc.write(", ")
enc.wf(", ")
}
}
enc.write("]")
enc.wf("]")
}
func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
@@ -368,7 +363,7 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
continue
}
enc.newline()
enc.writef("%s[[%s]]", enc.indentStr(key), key)
enc.wf("%s[[%s]]", enc.indentStr(key), key)
enc.newline()
enc.eMapOrStruct(key, trv, false)
}
@@ -381,7 +376,7 @@ func (enc *Encoder) eTable(key Key, rv reflect.Value) {
enc.newline()
}
if len(key) > 0 {
enc.writef("%s[%s]", enc.indentStr(key), key)
enc.wf("%s[%s]", enc.indentStr(key), key)
enc.newline()
}
enc.eMapOrStruct(key, rv, false)
@@ -427,7 +422,7 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
if inline {
enc.writeKeyValue(Key{mapKey.String()}, val, true)
if trailC || i != len(mapKeys)-1 {
enc.write(", ")
enc.wf(", ")
}
} else {
enc.encode(key.add(mapKey.String()), val)
@@ -436,12 +431,12 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
}
if inline {
enc.write("{")
enc.wf("{")
}
writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0)
writeMapKeys(mapKeysSub, false)
if inline {
enc.write("}")
enc.wf("}")
}
}
@@ -539,7 +534,7 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
if inline {
enc.writeKeyValue(Key{keyName}, fieldVal, true)
if fieldIndex[0] != totalFields-1 {
enc.write(", ")
enc.wf(", ")
}
} else {
enc.encode(key.add(keyName), fieldVal)
@@ -548,14 +543,14 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
}
if inline {
enc.write("{")
enc.wf("{")
}
l := len(fieldsDirect) + len(fieldsSub)
writeFields(fieldsDirect, l)
writeFields(fieldsSub, l)
if inline {
enc.write("}")
enc.wf("}")
}
}
@@ -705,7 +700,7 @@ func isEmpty(rv reflect.Value) bool {
func (enc *Encoder) newline() {
if enc.hasWritten {
enc.write("\n")
enc.wf("\n")
}
}
@@ -727,22 +722,14 @@ func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
enc.eElement(val)
return
}
enc.writef("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.eElement(val)
if !inline {
enc.newline()
}
}
func (enc *Encoder) write(s string) {
_, err := enc.w.WriteString(s)
if err != nil {
encPanic(err)
}
enc.hasWritten = true
}
func (enc *Encoder) writef(format string, v ...any) {
func (enc *Encoder) wf(format string, v ...any) {
_, err := fmt.Fprintf(enc.w, format, v...)
if err != nil {
encPanic(err)

View File

@@ -13,6 +13,7 @@ type itemType int
const (
itemError itemType = iota
itemNIL // used in the parser to indicate no type
itemEOF
itemText
itemString
@@ -46,13 +47,14 @@ func (p Position) String() string {
}
type lexer struct {
input string
start int
pos int
line int
state stateFn
items chan item
esc bool
input string
start int
pos int
line int
state stateFn
items chan item
tomlNext bool
esc bool
// Allow for backing up up to 4 runes. This is necessary because TOML
// contains 3-rune tokens (""" and ''').
@@ -88,13 +90,14 @@ func (lx *lexer) nextItem() item {
}
}
func lex(input string) *lexer {
func lex(input string, tomlNext bool) *lexer {
lx := &lexer{
input: input,
state: lexTop,
items: make(chan item, 10),
stack: make([]stateFn, 0, 10),
line: 1,
input: input,
state: lexTop,
items: make(chan item, 10),
stack: make([]stateFn, 0, 10),
line: 1,
tomlNext: tomlNext,
}
return lx
}
@@ -105,7 +108,7 @@ func (lx *lexer) push(state stateFn) {
func (lx *lexer) pop() stateFn {
if len(lx.stack) == 0 {
panic("BUG in lexer: no states to pop")
return lx.errorf("BUG in lexer: no states to pop")
}
last := lx.stack[len(lx.stack)-1]
lx.stack = lx.stack[0 : len(lx.stack)-1]
@@ -302,8 +305,6 @@ func lexTop(lx *lexer) stateFn {
return lexTableStart
case eof:
if lx.pos > lx.start {
// TODO: never reached? I think this can only occur on a bug in the
// lexer(?)
return lx.errorf("unexpected EOF")
}
lx.emit(itemEOF)
@@ -391,6 +392,8 @@ func lexTableNameStart(lx *lexer) stateFn {
func lexTableNameEnd(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.next(); {
case isWhitespace(r):
return lexTableNameEnd
case r == '.':
lx.ignore()
return lexTableNameStart
@@ -409,7 +412,7 @@ func lexTableNameEnd(lx *lexer) stateFn {
// Lexes only one part, e.g. only 'a' inside 'a.b'.
func lexBareName(lx *lexer) stateFn {
r := lx.next()
if isBareKeyChar(r) {
if isBareKeyChar(r, lx.tomlNext) {
return lexBareName
}
lx.backup()
@@ -417,23 +420,23 @@ func lexBareName(lx *lexer) stateFn {
return lx.pop()
}
// lexQuotedName lexes one part of a quoted key or table name. It assumes that
// it starts lexing at the quote itself (" or ').
// lexBareName lexes one part of a key or table.
//
// It assumes that at least one valid character for the table has already been
// read.
//
// Lexes only one part, e.g. only '"a"' inside '"a".b'.
func lexQuotedName(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexValue)
case r == '"':
lx.ignore() // ignore the '"'
return lexString
case r == '\'':
lx.ignore() // ignore the "'"
return lexRawString
// TODO: I don't think any of the below conditions can ever be reached?
case isWhitespace(r):
return lexSkip(lx, lexValue)
case r == eof:
return lx.errorf("unexpected EOF; expected value")
default:
@@ -461,19 +464,17 @@ func lexKeyStart(lx *lexer) stateFn {
func lexKeyNameStart(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.peek(); {
default:
lx.push(lexKeyEnd)
return lexBareName
case r == '"' || r == '\'':
lx.ignore()
lx.push(lexKeyEnd)
return lexQuotedName
// TODO: I think these can never be reached?
case r == '=' || r == eof:
return lx.errorf("unexpected '='")
case r == '.':
return lx.errorf("unexpected '.'")
case r == '"' || r == '\'':
lx.ignore()
lx.push(lexKeyEnd)
return lexQuotedName
default:
lx.push(lexKeyEnd)
return lexBareName
}
}
@@ -484,7 +485,7 @@ func lexKeyEnd(lx *lexer) stateFn {
switch r := lx.next(); {
case isWhitespace(r):
return lexSkip(lx, lexKeyEnd)
case r == eof: // TODO: never reached
case r == eof:
return lx.errorf("unexpected EOF; expected key separator '='")
case r == '.':
lx.ignore()
@@ -627,7 +628,10 @@ func lexInlineTableValue(lx *lexer) stateFn {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValue)
case isNL(r):
return lexSkip(lx, lexInlineTableValue)
if lx.tomlNext {
return lexSkip(lx, lexInlineTableValue)
}
return lx.errorPrevLine(errLexInlineTableNL{})
case r == '#':
lx.push(lexInlineTableValue)
return lexCommentStart
@@ -649,7 +653,10 @@ func lexInlineTableValueEnd(lx *lexer) stateFn {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValueEnd)
case isNL(r):
return lexSkip(lx, lexInlineTableValueEnd)
if lx.tomlNext {
return lexSkip(lx, lexInlineTableValueEnd)
}
return lx.errorPrevLine(errLexInlineTableNL{})
case r == '#':
lx.push(lexInlineTableValueEnd)
return lexCommentStart
@@ -657,7 +664,10 @@ func lexInlineTableValueEnd(lx *lexer) stateFn {
lx.ignore()
lx.skip(isWhitespace)
if lx.peek() == '}' {
return lexInlineTableValueEnd
if lx.tomlNext {
return lexInlineTableValueEnd
}
return lx.errorf("trailing comma not allowed in inline tables")
}
return lexInlineTableValue
case r == '}':
@@ -845,6 +855,9 @@ func lexStringEscape(lx *lexer) stateFn {
r := lx.next()
switch r {
case 'e':
if !lx.tomlNext {
return lx.error(errLexEscape{r})
}
fallthrough
case 'b':
fallthrough
@@ -865,6 +878,9 @@ func lexStringEscape(lx *lexer) stateFn {
case '\\':
return lx.pop()
case 'x':
if !lx.tomlNext {
return lx.error(errLexEscape{r})
}
return lexHexEscape
case 'u':
return lexShortUnicodeEscape
@@ -912,9 +928,19 @@ func lexLongUnicodeEscape(lx *lexer) stateFn {
// lexBaseNumberOrDate can differentiate base prefixed integers from other
// types.
func lexNumberOrDateStart(lx *lexer) stateFn {
if lx.next() == '0' {
r := lx.next()
switch r {
case '0':
return lexBaseNumberOrDate
}
if !isDigit(r) {
// The only way to reach this state is if the value starts
// with a digit, so specifically treat anything else as an
// error.
return lx.errorf("expected a digit but got %q", r)
}
return lexNumberOrDate
}
@@ -1170,13 +1196,13 @@ func lexSkip(lx *lexer, nextState stateFn) stateFn {
}
func (s stateFn) String() string {
if s == nil {
return "<nil>"
}
name := runtime.FuncForPC(reflect.ValueOf(s).Pointer()).Name()
if i := strings.LastIndexByte(name, '.'); i > -1 {
name = name[i+1:]
}
if s == nil {
name = "<nil>"
}
return name + "()"
}
@@ -1184,6 +1210,8 @@ func (itype itemType) String() string {
switch itype {
case itemError:
return "Error"
case itemNIL:
return "NIL"
case itemEOF:
return "EOF"
case itemText:
@@ -1198,22 +1226,18 @@ func (itype itemType) String() string {
return "Float"
case itemDatetime:
return "DateTime"
case itemArray:
return "Array"
case itemArrayEnd:
return "ArrayEnd"
case itemTableStart:
return "TableStart"
case itemTableEnd:
return "TableEnd"
case itemArrayTableStart:
return "ArrayTableStart"
case itemArrayTableEnd:
return "ArrayTableEnd"
case itemKeyStart:
return "KeyStart"
case itemKeyEnd:
return "KeyEnd"
case itemArray:
return "Array"
case itemArrayEnd:
return "ArrayEnd"
case itemCommentStart:
return "CommentStart"
case itemInlineTableStart:
@@ -1242,7 +1266,7 @@ func isDigit(r rune) bool { return r >= '0' && r <= '9' }
func isBinary(r rune) bool { return r == '0' || r == '1' }
func isOctal(r rune) bool { return r >= '0' && r <= '7' }
func isHex(r rune) bool { return (r >= '0' && r <= '9') || (r|0x20 >= 'a' && r|0x20 <= 'f') }
func isBareKeyChar(r rune) bool {
func isBareKeyChar(r rune, tomlNext bool) bool {
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') || r == '_' || r == '-'
}

View File

@@ -3,6 +3,7 @@ package toml
import (
"fmt"
"math"
"os"
"strconv"
"strings"
"time"
@@ -16,6 +17,7 @@ type parser struct {
context Key // Full key for the current hash in scope.
currentKey string // Base key name for everything except hashes.
pos Position // Current position in the TOML file.
tomlNext bool
ordered []Key // List of keys in the order that they appear in the TOML data.
@@ -30,6 +32,8 @@ type keyInfo struct {
}
func parse(data string) (p *parser, err error) {
_, tomlNext := os.LookupEnv("BURNTSUSHI_TOML_110")
defer func() {
if r := recover(); r != nil {
if pErr, ok := r.(ParseError); ok {
@@ -69,9 +73,10 @@ func parse(data string) (p *parser, err error) {
p = &parser{
keyInfo: make(map[string]keyInfo),
mapping: make(map[string]any),
lx: lex(data),
lx: lex(data, tomlNext),
ordered: make([]Key, 0),
implicits: make(map[string]struct{}),
tomlNext: tomlNext,
}
for {
item := p.next()
@@ -345,14 +350,17 @@ func (p *parser) valueFloat(it item) (any, tomlType) {
var dtTypes = []struct {
fmt string
zone *time.Location
next bool
}{
{time.RFC3339Nano, time.Local},
{"2006-01-02T15:04:05.999999999", internal.LocalDatetime},
{"2006-01-02", internal.LocalDate},
{"15:04:05.999999999", internal.LocalTime},
{"2006-01-02T15:04Z07:00", time.Local},
{"2006-01-02T15:04", internal.LocalDatetime},
{"15:04", internal.LocalTime},
{time.RFC3339Nano, time.Local, false},
{"2006-01-02T15:04:05.999999999", internal.LocalDatetime, false},
{"2006-01-02", internal.LocalDate, false},
{"15:04:05.999999999", internal.LocalTime, false},
// tomlNext
{"2006-01-02T15:04Z07:00", time.Local, true},
{"2006-01-02T15:04", internal.LocalDatetime, true},
{"15:04", internal.LocalTime, true},
}
func (p *parser) valueDatetime(it item) (any, tomlType) {
@@ -363,6 +371,9 @@ func (p *parser) valueDatetime(it item) (any, tomlType) {
err error
)
for _, dt := range dtTypes {
if dt.next && !p.tomlNext {
continue
}
t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone)
if err == nil {
if missingLeadingZero(it.val, dt.fmt) {
@@ -633,11 +644,6 @@ func (p *parser) setValue(key string, value any) {
// Note that since it has already been defined (as a hash), we don't
// want to overwrite it. So our business is done.
if p.isArray(keyContext) {
if !p.isImplicit(keyContext) {
if _, ok := hash[key]; ok {
p.panicf("Key '%s' has already been defined.", keyContext)
}
}
p.removeImplicit(keyContext)
hash[key] = value
return
@@ -796,8 +802,10 @@ func (p *parser) replaceEscapes(it item, str string) string {
b.WriteByte(0x0d)
skip = 1
case 'e':
b.WriteByte(0x1b)
skip = 1
if p.tomlNext {
b.WriteByte(0x1b)
skip = 1
}
case '"':
b.WriteByte(0x22)
skip = 1
@@ -807,9 +815,11 @@ func (p *parser) replaceEscapes(it item, str string) string {
// The lexer guarantees the correct number of characters are present;
// don't need to check here.
case 'x':
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+4])
b.WriteRune(escaped)
skip = 3
if p.tomlNext {
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+4])
b.WriteRune(escaped)
skip = 3
}
case 'u':
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+6])
b.WriteRune(escaped)

View File

@@ -1,5 +1,15 @@
# Changelog
## [0.6.1]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.6.0...v0.6.1)
### Changed
- Perf improvements: replaced the ASCII lookup table with a simple
function. A bit more cache-friendly. More inlining.
- Bug fix: single regional indicators are now treated as width 2, since that
is what actual terminals do.
## [0.6.0]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.5.0...v0.6.0)

View File

@@ -33,42 +33,82 @@ func main() {
}
```
For most purposes, you should use the `String` or `Bytes` methods.
For most purposes, you should use the `String` or `Bytes` methods. They sum
the widths of grapheme clusters in the string or byte slice.
> Note: in your application, iterating over runes to measure width is likely incorrect;
the smallest unit of display is a grapheme, not a rune.
### Iterating over graphemes
If you need the individual graphemes:
```go
import (
"fmt"
"github.com/clipperhouse/displaywidth"
)
func main() {
g := displaywidth.StringGraphemes("Hello, 世界!")
for g.Next() {
width := g.Width()
value := g.Value()
// do something with the width or value
}
}
```
### Options
You can specify East Asian Width settings. When false (default),
[East Asian Ambiguous characters](https://www.unicode.org/reports/tr11/#Ambiguous)
are treated as width 1. When true, East Asian Ambiguous characters are treated
as width 2.
There is one option, `displaywidth.Options.EastAsianWidth`, which defines
how [East Asian Ambiguous characters](https://www.unicode.org/reports/tr11/#Ambiguous)
are treated.
When `false` (default), East Asian Ambiguous characters are treated as width 1.
When `true`, they are treated as width 2.
You may wish to configure this based on environment variables or locale.
`go-runewidth`, for example, does so
[during package initialization](https://github.com/mattn/go-runewidth/blob/master/runewidth.go#L26C1-L45C2).
`displaywidth` does not do this automatically, we prefer to leave it to you.
You might do something like:
```go
myOptions := displaywidth.Options{
EastAsianWidth: true,
var width displaywidth.Options // zero value is default
func init() {
if os.Getenv("EAST_ASIAN_WIDTH") == "true" {
width = displaywidth.Options{EastAsianWidth: true}
}
// or check locale, or any other logic you want
}
width := myOptions.String("Hello, 世界!")
fmt.Println(width)
// use it in your logic
func myApp() {
fmt.Println(width.String("Hello, 世界!"))
}
```
## Technical details
## Technical standards and compatibility
This package implements the Unicode East Asian Width standard
([UAX #11](https://www.unicode.org/reports/tr11/)), and handles
([UAX #11](https://www.unicode.org/reports/tr11/tr11-43.html)), and handles
[version selectors](https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)),
and [regional indicator pairs](https://en.wikipedia.org/wiki/Regional_indicator_symbol)
(flags). We implement [Unicode TR51](https://unicode.org/reports/tr51/).
(flags). We implement [Unicode TR51](https://www.unicode.org/reports/tr51/tr51-27.html). We are keeping
an eye on [emerging standards](https://www.jeffquast.com/post/state-of-terminal-emulation-2025/).
`clipperhouse/displaywidth`, `mattn/go-runewidth`, and `rivo/uniseg` will
give the same outputs for most real-world text. See extensive details in the
give the same outputs for most real-world text. Extensive details are in the
[compatibility analysis](comparison/COMPATIBILITY_ANALYSIS.md).
If you wish to investigate the core logic, see the `lookupProperties` and `width`
functions in [width.go](width.go#L135). The essential trie generation logic is in
`buildPropertyBitmap` in [unicode.go](internal/gen/unicode.go#L317).
functions in [width.go](width.go#L139). The essential trie generation logic is in
`buildPropertyBitmap` in [unicode.go](internal/gen/unicode.go#L316).
I (@clipperhouse) am keeping an eye on [emerging standards and test suites](https://www.jeffquast.com/post/state-of-terminal-emulation-2025/).
## Prior Art
@@ -93,31 +133,33 @@ goarch: arm64
pkg: github.com/clipperhouse/displaywidth/comparison
cpu: Apple M2
BenchmarkString_Mixed/clipperhouse/displaywidth-8 10469 ns/op 161.15 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/mattn/go-runewidth-8 14250 ns/op 118.39 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/rivo/uniseg-8 19258 ns/op 87.60 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/clipperhouse/displaywidth-8 10326 ns/op 163.37 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/mattn/go-runewidth-8 14415 ns/op 117.03 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/rivo/uniseg-8 19343 ns/op 87.21 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/clipperhouse/displaywidth-8 10518 ns/op 160.39 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/mattn/go-runewidth-8 23827 ns/op 70.80 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/rivo/uniseg-8 19537 ns/op 86.35 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/clipperhouse/displaywidth-8 10561 ns/op 159.74 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/mattn/go-runewidth-8 23790 ns/op 70.91 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/rivo/uniseg-8 19322 ns/op 87.31 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/clipperhouse/displaywidth-8 1027 ns/op 124.61 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/mattn/go-runewidth-8 1166 ns/op 109.78 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/rivo/uniseg-8 1551 ns/op 82.52 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/clipperhouse/displaywidth-8 1033 ns/op 123.88 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/mattn/go-runewidth-8 1168 ns/op 109.59 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/rivo/uniseg-8 1585 ns/op 80.74 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/clipperhouse/displaywidth-8 3164 ns/op 228.84 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/mattn/go-runewidth-8 4728 ns/op 153.13 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/rivo/uniseg-8 6489 ns/op 111.57 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/clipperhouse/displaywidth-8 3034 ns/op 238.61 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/mattn/go-runewidth-8 4797 ns/op 150.94 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/rivo/uniseg-8 6612 ns/op 109.50 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Mixed/clipperhouse/displaywidth-8 3429 ns/op 491.96 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Mixed/mattn/go-runewidth-8 5308 ns/op 317.81 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Mixed/clipperhouse/displaywidth-8 3343 ns/op 504.67 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Mixed/mattn/go-runewidth-8 5414 ns/op 311.62 MB/s 0 B/op 0 allocs/op
BenchmarkRune_EastAsian/clipperhouse/displaywidth-8 3419 ns/op 493.49 MB/s 0 B/op 0 allocs/op
BenchmarkRune_EastAsian/mattn/go-runewidth-8 15321 ns/op 110.11 MB/s 0 B/op 0 allocs/op
BenchmarkRune_EastAsian/clipperhouse/displaywidth-8 3393 ns/op 497.17 MB/s 0 B/op 0 allocs/op
BenchmarkRune_EastAsian/mattn/go-runewidth-8 15312 ns/op 110.17 MB/s 0 B/op 0 allocs/op
BenchmarkRune_ASCII/clipperhouse/displaywidth-8 254.4 ns/op 503.19 MB/s 0 B/op 0 allocs/op
BenchmarkRune_ASCII/mattn/go-runewidth-8 264.3 ns/op 484.31 MB/s 0 B/op 0 allocs/op
BenchmarkRune_ASCII/clipperhouse/displaywidth-8 256.9 ns/op 498.32 MB/s 0 B/op 0 allocs/op
BenchmarkRune_ASCII/mattn/go-runewidth-8 265.7 ns/op 481.75 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Emoji/clipperhouse/displaywidth-8 1374 ns/op 527.02 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Emoji/mattn/go-runewidth-8 2210 ns/op 327.66 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Emoji/clipperhouse/displaywidth-8 1336 ns/op 541.96 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Emoji/mattn/go-runewidth-8 2304 ns/op 314.23 MB/s 0 B/op 0 allocs/op
```
Here are some notes on [how to make Unicode things fast](https://clipperhouse.com/go-unicode/).

View File

@@ -1,91 +0,0 @@
package displaywidth
// propertyWidths is a jump table of sorts, instead of a switch
var propertyWidths = [5]int{
_Default: 1,
_Zero_Width: 0,
_East_Asian_Wide: 2,
_East_Asian_Ambiguous: 1,
_Emoji: 2,
}
// asciiWidths is a lookup table for single-byte character widths. Printable
// ASCII characters have width 1, control characters have width 0.
//
// It is intended for valid single-byte UTF-8, which means <128.
//
// If you look up an index >= 128, that is either:
// - invalid UTF-8, or
// - a multi-byte UTF-8 sequence, in which case you should be operating on
// the grapheme cluster, and not using this table
//
// We will return a default value of 1 in those cases, so as not to panic.
var asciiWidths = [256]int8{
// Control characters (0x00-0x1F): width 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// Printable ASCII (0x20-0x7E): width 1
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// DEL (0x7F): width 0
0,
// >= 128
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
}
// asciiProperties is a lookup table for single-byte character properties.
// It is intended for valid single-byte UTF-8, which means <128.
//
// If you look up an index >= 128, that is either:
// - invalid UTF-8, or
// - a multi-byte UTF-8 sequence, in which case you should be operating on
// the grapheme cluster, and not using this table
//
// We will return a default value of _Default in those cases, so as not to
// panic.
var asciiProperties = [256]property{
// Control characters (0x00-0x1F): _Zero_Width
_Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width,
_Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width,
_Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width,
_Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width, _Zero_Width,
// Printable ASCII (0x20-0x7E): _Default
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default,
// DEL (0x7F): _Zero_Width
_Zero_Width,
// >= 128
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
_Default, _Default, _Default, _Default, _Default, _Default, _Default, _Default,
}

View File

@@ -10,12 +10,10 @@ type property uint8
const (
// Always 0 width, includes combining marks, control characters, non-printable, etc
_Zero_Width property = iota + 1
// Always 2 wide (East Asian Wide F/W)
_East_Asian_Wide
// Always 2 wide (East Asian Wide F/W, Emoji, Regional Indicator)
_Wide
// Width depends on EastAsianWidth option
_East_Asian_Ambiguous
// Extended_Pictographic + Emoji_Presentation
_Emoji
)
// lookup returns the trie value for the first UTF-8 encoding in s and
@@ -81,7 +79,7 @@ func lookup[T stringish.Interface](s T) (v uint8, sz int) {
return 0, 1
}
// stringWidthTrie. Total size: 17728 bytes (17.31 KiB). Checksum: b4b51ae347944fdb.
// stringWidthTrie. Total size: 17664 bytes (17.25 KiB). Checksum: c77d82ff2d69f0d2.
// type stringWidthTrie struct { }
// func newStringWidthTrie(i int) *stringWidthTrie {
@@ -96,9 +94,9 @@ func lookupValue(n uint32, b byte) uint8 {
}
}
// stringWidthValues: 247 blocks, 15808 entries, 15808 bytes
// stringWidthValues: 246 blocks, 15744 entries, 15744 bytes
// The third block is the zero block.
var stringWidthValues = [15808]uint8{
var stringWidthValues = [15744]uint8{
// Block 0x0, offset 0x0
// Block 0x1, offset 0x40
// Block 0x2, offset 0x80
@@ -577,13 +575,13 @@ var stringWidthValues = [15808]uint8{
0x167f: 0x0003,
// Block 0x5a, offset 0x1680
0x1692: 0x0003,
0x169a: 0x0004, 0x169b: 0x0004,
0x169a: 0x0002, 0x169b: 0x0002,
0x16a9: 0x0002,
0x16aa: 0x0002,
// Block 0x5b, offset 0x16c0
0x16e9: 0x0004,
0x16ea: 0x0004, 0x16eb: 0x0004, 0x16ec: 0x0004,
0x16f0: 0x0004, 0x16f3: 0x0004,
0x16e9: 0x0002,
0x16ea: 0x0002, 0x16eb: 0x0002, 0x16ec: 0x0002,
0x16f0: 0x0002, 0x16f3: 0x0002,
// Block 0x5c, offset 0x1700
0x1720: 0x0003, 0x1721: 0x0003, 0x1722: 0x0003, 0x1723: 0x0003,
0x1724: 0x0003, 0x1725: 0x0003, 0x1726: 0x0003, 0x1727: 0x0003, 0x1728: 0x0003, 0x1729: 0x0003,
@@ -642,63 +640,63 @@ var stringWidthValues = [15808]uint8{
0x1862: 0x0003, 0x1863: 0x0003,
0x1864: 0x0003, 0x1865: 0x0003,
0x186f: 0x0003,
0x187d: 0x0004, 0x187e: 0x0004,
0x187d: 0x0002, 0x187e: 0x0002,
// Block 0x62, offset 0x1880
0x1885: 0x0003,
0x1886: 0x0003, 0x1889: 0x0003,
0x188e: 0x0003, 0x188f: 0x0003,
0x1894: 0x0004, 0x1895: 0x0004,
0x1894: 0x0002, 0x1895: 0x0002,
0x189c: 0x0003,
0x189e: 0x0003,
0x18b0: 0x0002, 0x18b1: 0x0002, 0x18b2: 0x0002, 0x18b3: 0x0002, 0x18b4: 0x0002, 0x18b5: 0x0002,
0x18b6: 0x0002, 0x18b7: 0x0002,
// Block 0x63, offset 0x18c0
0x18c0: 0x0003, 0x18c2: 0x0003,
0x18c8: 0x0004, 0x18c9: 0x0004, 0x18ca: 0x0004, 0x18cb: 0x0004,
0x18cc: 0x0004, 0x18cd: 0x0004, 0x18ce: 0x0004, 0x18cf: 0x0004, 0x18d0: 0x0004, 0x18d1: 0x0004,
0x18d2: 0x0004, 0x18d3: 0x0004,
0x18c8: 0x0002, 0x18c9: 0x0002, 0x18ca: 0x0002, 0x18cb: 0x0002,
0x18cc: 0x0002, 0x18cd: 0x0002, 0x18ce: 0x0002, 0x18cf: 0x0002, 0x18d0: 0x0002, 0x18d1: 0x0002,
0x18d2: 0x0002, 0x18d3: 0x0002,
0x18e0: 0x0003, 0x18e1: 0x0003, 0x18e3: 0x0003,
0x18e4: 0x0003, 0x18e5: 0x0003, 0x18e7: 0x0003, 0x18e8: 0x0003, 0x18e9: 0x0003,
0x18ea: 0x0003, 0x18ec: 0x0003, 0x18ed: 0x0003, 0x18ef: 0x0003,
0x18ff: 0x0004,
0x18ff: 0x0002,
// Block 0x64, offset 0x1900
0x190a: 0x0002, 0x190b: 0x0002,
0x190c: 0x0002, 0x190d: 0x0002, 0x190e: 0x0002, 0x190f: 0x0002,
0x1913: 0x0004,
0x191e: 0x0003, 0x191f: 0x0003, 0x1921: 0x0004,
0x192a: 0x0004, 0x192b: 0x0004,
0x193d: 0x0004, 0x193e: 0x0004, 0x193f: 0x0003,
0x1913: 0x0002,
0x191e: 0x0003, 0x191f: 0x0003, 0x1921: 0x0002,
0x192a: 0x0002, 0x192b: 0x0002,
0x193d: 0x0002, 0x193e: 0x0002, 0x193f: 0x0003,
// Block 0x65, offset 0x1940
0x1944: 0x0004, 0x1945: 0x0004,
0x1944: 0x0002, 0x1945: 0x0002,
0x1946: 0x0003, 0x1947: 0x0003, 0x1948: 0x0003, 0x1949: 0x0003, 0x194a: 0x0003, 0x194b: 0x0003,
0x194c: 0x0003, 0x194d: 0x0003, 0x194e: 0x0004, 0x194f: 0x0003, 0x1950: 0x0003, 0x1951: 0x0003,
0x1952: 0x0003, 0x1953: 0x0003, 0x1954: 0x0004, 0x1955: 0x0003, 0x1956: 0x0003, 0x1957: 0x0003,
0x194c: 0x0003, 0x194d: 0x0003, 0x194e: 0x0002, 0x194f: 0x0003, 0x1950: 0x0003, 0x1951: 0x0003,
0x1952: 0x0003, 0x1953: 0x0003, 0x1954: 0x0002, 0x1955: 0x0003, 0x1956: 0x0003, 0x1957: 0x0003,
0x1958: 0x0003, 0x1959: 0x0003, 0x195a: 0x0003, 0x195b: 0x0003, 0x195c: 0x0003, 0x195d: 0x0003,
0x195e: 0x0003, 0x195f: 0x0003, 0x1960: 0x0003, 0x1961: 0x0003, 0x1963: 0x0003,
0x1968: 0x0003, 0x1969: 0x0003,
0x196a: 0x0004, 0x196b: 0x0003, 0x196c: 0x0003, 0x196d: 0x0003, 0x196e: 0x0003, 0x196f: 0x0003,
0x1970: 0x0003, 0x1971: 0x0003, 0x1972: 0x0004, 0x1973: 0x0004, 0x1974: 0x0003, 0x1975: 0x0004,
0x1976: 0x0003, 0x1977: 0x0003, 0x1978: 0x0003, 0x1979: 0x0003, 0x197a: 0x0004, 0x197b: 0x0003,
0x197c: 0x0003, 0x197d: 0x0004, 0x197e: 0x0003, 0x197f: 0x0003,
0x196a: 0x0002, 0x196b: 0x0003, 0x196c: 0x0003, 0x196d: 0x0003, 0x196e: 0x0003, 0x196f: 0x0003,
0x1970: 0x0003, 0x1971: 0x0003, 0x1972: 0x0002, 0x1973: 0x0002, 0x1974: 0x0003, 0x1975: 0x0002,
0x1976: 0x0003, 0x1977: 0x0003, 0x1978: 0x0003, 0x1979: 0x0003, 0x197a: 0x0002, 0x197b: 0x0003,
0x197c: 0x0003, 0x197d: 0x0002, 0x197e: 0x0003, 0x197f: 0x0003,
// Block 0x66, offset 0x1980
0x1985: 0x0004,
0x198a: 0x0004, 0x198b: 0x0004,
0x19a8: 0x0004,
0x1985: 0x0002,
0x198a: 0x0002, 0x198b: 0x0002,
0x19a8: 0x0002,
0x19bd: 0x0003,
// Block 0x67, offset 0x19c0
0x19cc: 0x0004, 0x19ce: 0x0004,
0x19d3: 0x0004, 0x19d4: 0x0004, 0x19d5: 0x0004, 0x19d7: 0x0004,
0x19cc: 0x0002, 0x19ce: 0x0002,
0x19d3: 0x0002, 0x19d4: 0x0002, 0x19d5: 0x0002, 0x19d7: 0x0002,
0x19f6: 0x0003, 0x19f7: 0x0003, 0x19f8: 0x0003, 0x19f9: 0x0003, 0x19fa: 0x0003, 0x19fb: 0x0003,
0x19fc: 0x0003, 0x19fd: 0x0003, 0x19fe: 0x0003, 0x19ff: 0x0003,
// Block 0x68, offset 0x1a00
0x1a15: 0x0004, 0x1a16: 0x0004, 0x1a17: 0x0004,
0x1a30: 0x0004,
0x1a3f: 0x0004,
0x1a15: 0x0002, 0x1a16: 0x0002, 0x1a17: 0x0002,
0x1a30: 0x0002,
0x1a3f: 0x0002,
// Block 0x69, offset 0x1a40
0x1a5b: 0x0004, 0x1a5c: 0x0004,
0x1a5b: 0x0002, 0x1a5c: 0x0002,
// Block 0x6a, offset 0x1a80
0x1a90: 0x0004,
0x1a95: 0x0004, 0x1a96: 0x0003, 0x1a97: 0x0003,
0x1a90: 0x0002,
0x1a95: 0x0002, 0x1a96: 0x0003, 0x1a97: 0x0003,
0x1a98: 0x0003, 0x1a99: 0x0003,
// Block 0x6b, offset 0x1ac0
0x1aef: 0x0001,
@@ -1273,9 +1271,9 @@ var stringWidthValues = [15808]uint8{
0x3604: 0x0001, 0x3605: 0x0001,
0x3606: 0x0001, 0x3607: 0x0001, 0x3608: 0x0001, 0x3609: 0x0001, 0x360a: 0x0001,
// Block 0xd9, offset 0x3640
0x3644: 0x0004,
0x3644: 0x0002,
// Block 0xda, offset 0x3680
0x368f: 0x0004,
0x368f: 0x0002,
// Block 0xdb, offset 0x36c0
0x36c0: 0x0003, 0x36c1: 0x0003, 0x36c2: 0x0003, 0x36c3: 0x0003, 0x36c4: 0x0003, 0x36c5: 0x0003,
0x36c6: 0x0003, 0x36c7: 0x0003, 0x36c8: 0x0003, 0x36c9: 0x0003, 0x36ca: 0x0003,
@@ -1302,246 +1300,228 @@ var stringWidthValues = [15808]uint8{
// Block 0xdd, offset 0x3740
0x3740: 0x0003, 0x3741: 0x0003, 0x3742: 0x0003, 0x3743: 0x0003, 0x3744: 0x0003, 0x3745: 0x0003,
0x3746: 0x0003, 0x3747: 0x0003, 0x3748: 0x0003, 0x3749: 0x0003, 0x374a: 0x0003, 0x374b: 0x0003,
0x374c: 0x0003, 0x374d: 0x0003, 0x374e: 0x0004, 0x374f: 0x0003, 0x3750: 0x0003, 0x3751: 0x0004,
0x3752: 0x0004, 0x3753: 0x0004, 0x3754: 0x0004, 0x3755: 0x0004, 0x3756: 0x0004, 0x3757: 0x0004,
0x3758: 0x0004, 0x3759: 0x0004, 0x375a: 0x0004, 0x375b: 0x0003, 0x375c: 0x0003, 0x375d: 0x0003,
0x374c: 0x0003, 0x374d: 0x0003, 0x374e: 0x0002, 0x374f: 0x0003, 0x3750: 0x0003, 0x3751: 0x0002,
0x3752: 0x0002, 0x3753: 0x0002, 0x3754: 0x0002, 0x3755: 0x0002, 0x3756: 0x0002, 0x3757: 0x0002,
0x3758: 0x0002, 0x3759: 0x0002, 0x375a: 0x0002, 0x375b: 0x0003, 0x375c: 0x0003, 0x375d: 0x0003,
0x375e: 0x0003, 0x375f: 0x0003, 0x3760: 0x0003, 0x3761: 0x0003, 0x3762: 0x0003, 0x3763: 0x0003,
0x3764: 0x0003, 0x3765: 0x0003, 0x3766: 0x0003, 0x3767: 0x0003, 0x3768: 0x0003, 0x3769: 0x0003,
0x376a: 0x0003, 0x376b: 0x0003, 0x376c: 0x0003,
// Block 0xde, offset 0x3780
0x3780: 0x0002, 0x3781: 0x0004, 0x3782: 0x0002,
0x3790: 0x0002, 0x3791: 0x0002,
0x3792: 0x0002, 0x3793: 0x0002, 0x3794: 0x0002, 0x3795: 0x0002, 0x3796: 0x0002, 0x3797: 0x0002,
0x3798: 0x0002, 0x3799: 0x0002, 0x379a: 0x0004, 0x379b: 0x0002, 0x379c: 0x0002, 0x379d: 0x0002,
0x379e: 0x0002, 0x379f: 0x0002, 0x37a0: 0x0002, 0x37a1: 0x0002, 0x37a2: 0x0002, 0x37a3: 0x0002,
0x37a4: 0x0002, 0x37a5: 0x0002, 0x37a6: 0x0002, 0x37a7: 0x0002, 0x37a8: 0x0002, 0x37a9: 0x0002,
0x37aa: 0x0002, 0x37ab: 0x0002, 0x37ac: 0x0002, 0x37ad: 0x0002, 0x37ae: 0x0002, 0x37af: 0x0004,
0x37b0: 0x0002, 0x37b1: 0x0002, 0x37b2: 0x0004, 0x37b3: 0x0004, 0x37b4: 0x0004, 0x37b5: 0x0004,
0x37b6: 0x0004, 0x37b7: 0x0002, 0x37b8: 0x0004, 0x37b9: 0x0004, 0x37ba: 0x0004, 0x37bb: 0x0002,
0x37a6: 0x0002, 0x37a7: 0x0002, 0x37a8: 0x0002, 0x37a9: 0x0002,
0x37aa: 0x0002, 0x37ab: 0x0002, 0x37ac: 0x0002, 0x37ad: 0x0002, 0x37ae: 0x0002, 0x37af: 0x0002,
0x37b0: 0x0002, 0x37b1: 0x0002, 0x37b2: 0x0002, 0x37b3: 0x0002, 0x37b4: 0x0002, 0x37b5: 0x0002,
0x37b6: 0x0002, 0x37b7: 0x0002, 0x37b8: 0x0002, 0x37b9: 0x0002, 0x37ba: 0x0002, 0x37bb: 0x0002,
0x37bc: 0x0002, 0x37bd: 0x0002, 0x37be: 0x0002, 0x37bf: 0x0002,
// Block 0xdf, offset 0x37c0
0x37c0: 0x0002, 0x37c1: 0x0002, 0x37c2: 0x0002, 0x37c3: 0x0002, 0x37c4: 0x0002, 0x37c5: 0x0002,
0x37c6: 0x0002, 0x37c7: 0x0002, 0x37c8: 0x0002,
0x37d0: 0x0004, 0x37d1: 0x0004,
0x37e0: 0x0002, 0x37e1: 0x0002, 0x37e2: 0x0002, 0x37e3: 0x0002,
0x37e4: 0x0002, 0x37e5: 0x0002,
0x37c0: 0x0002, 0x37c1: 0x0002, 0x37c2: 0x0002,
0x37d0: 0x0002, 0x37d1: 0x0002,
0x37d2: 0x0002, 0x37d3: 0x0002, 0x37d4: 0x0002, 0x37d5: 0x0002, 0x37d6: 0x0002, 0x37d7: 0x0002,
0x37d8: 0x0002, 0x37d9: 0x0002, 0x37da: 0x0002, 0x37db: 0x0002, 0x37dc: 0x0002, 0x37dd: 0x0002,
0x37de: 0x0002, 0x37df: 0x0002, 0x37e0: 0x0002, 0x37e1: 0x0002, 0x37e2: 0x0002, 0x37e3: 0x0002,
0x37e4: 0x0002, 0x37e5: 0x0002, 0x37e6: 0x0002, 0x37e7: 0x0002, 0x37e8: 0x0002, 0x37e9: 0x0002,
0x37ea: 0x0002, 0x37eb: 0x0002, 0x37ec: 0x0002, 0x37ed: 0x0002, 0x37ee: 0x0002, 0x37ef: 0x0002,
0x37f0: 0x0002, 0x37f1: 0x0002, 0x37f2: 0x0002, 0x37f3: 0x0002, 0x37f4: 0x0002, 0x37f5: 0x0002,
0x37f6: 0x0002, 0x37f7: 0x0002, 0x37f8: 0x0002, 0x37f9: 0x0002, 0x37fa: 0x0002, 0x37fb: 0x0002,
// Block 0xe0, offset 0x3800
0x3800: 0x0004, 0x3801: 0x0004, 0x3802: 0x0004, 0x3803: 0x0004, 0x3804: 0x0004, 0x3805: 0x0004,
0x3806: 0x0004, 0x3807: 0x0004, 0x3808: 0x0004, 0x3809: 0x0004, 0x380a: 0x0004, 0x380b: 0x0004,
0x380c: 0x0004, 0x380d: 0x0004, 0x380e: 0x0004, 0x380f: 0x0004, 0x3810: 0x0004, 0x3811: 0x0004,
0x3812: 0x0004, 0x3813: 0x0004, 0x3814: 0x0004, 0x3815: 0x0004, 0x3816: 0x0004, 0x3817: 0x0004,
0x3818: 0x0004, 0x3819: 0x0004, 0x381a: 0x0004, 0x381b: 0x0004, 0x381c: 0x0004, 0x381d: 0x0004,
0x381e: 0x0004, 0x381f: 0x0004, 0x3820: 0x0004,
0x382d: 0x0004, 0x382e: 0x0004, 0x382f: 0x0004,
0x3830: 0x0004, 0x3831: 0x0004, 0x3832: 0x0004, 0x3833: 0x0004, 0x3834: 0x0004, 0x3835: 0x0004,
0x3837: 0x0004, 0x3838: 0x0004, 0x3839: 0x0004, 0x383a: 0x0004, 0x383b: 0x0004,
0x383c: 0x0004, 0x383d: 0x0004, 0x383e: 0x0004, 0x383f: 0x0004,
0x3800: 0x0002, 0x3801: 0x0002, 0x3802: 0x0002, 0x3803: 0x0002, 0x3804: 0x0002, 0x3805: 0x0002,
0x3806: 0x0002, 0x3807: 0x0002, 0x3808: 0x0002,
0x3810: 0x0002, 0x3811: 0x0002,
0x3820: 0x0002, 0x3821: 0x0002, 0x3822: 0x0002, 0x3823: 0x0002,
0x3824: 0x0002, 0x3825: 0x0002,
// Block 0xe1, offset 0x3840
0x3840: 0x0004, 0x3841: 0x0004, 0x3842: 0x0004, 0x3843: 0x0004, 0x3844: 0x0004, 0x3845: 0x0004,
0x3846: 0x0004, 0x3847: 0x0004, 0x3848: 0x0004, 0x3849: 0x0004, 0x384a: 0x0004, 0x384b: 0x0004,
0x384c: 0x0004, 0x384d: 0x0004, 0x384e: 0x0004, 0x384f: 0x0004, 0x3850: 0x0004, 0x3851: 0x0004,
0x3852: 0x0004, 0x3853: 0x0004, 0x3854: 0x0004, 0x3855: 0x0004, 0x3856: 0x0004, 0x3857: 0x0004,
0x3858: 0x0004, 0x3859: 0x0004, 0x385a: 0x0004, 0x385b: 0x0004, 0x385c: 0x0004, 0x385d: 0x0004,
0x385e: 0x0004, 0x385f: 0x0004, 0x3860: 0x0004, 0x3861: 0x0004, 0x3862: 0x0004, 0x3863: 0x0004,
0x3864: 0x0004, 0x3865: 0x0004, 0x3866: 0x0004, 0x3867: 0x0004, 0x3868: 0x0004, 0x3869: 0x0004,
0x386a: 0x0004, 0x386b: 0x0004, 0x386c: 0x0004, 0x386d: 0x0004, 0x386e: 0x0004, 0x386f: 0x0004,
0x3870: 0x0004, 0x3871: 0x0004, 0x3872: 0x0004, 0x3873: 0x0004, 0x3874: 0x0004, 0x3875: 0x0004,
0x3876: 0x0004, 0x3877: 0x0004, 0x3878: 0x0004, 0x3879: 0x0004, 0x387a: 0x0004, 0x387b: 0x0004,
0x387c: 0x0004, 0x387e: 0x0004, 0x387f: 0x0004,
0x3840: 0x0002, 0x3841: 0x0002, 0x3842: 0x0002, 0x3843: 0x0002, 0x3844: 0x0002, 0x3845: 0x0002,
0x3846: 0x0002, 0x3847: 0x0002, 0x3848: 0x0002, 0x3849: 0x0002, 0x384a: 0x0002, 0x384b: 0x0002,
0x384c: 0x0002, 0x384d: 0x0002, 0x384e: 0x0002, 0x384f: 0x0002, 0x3850: 0x0002, 0x3851: 0x0002,
0x3852: 0x0002, 0x3853: 0x0002, 0x3854: 0x0002, 0x3855: 0x0002, 0x3856: 0x0002, 0x3857: 0x0002,
0x3858: 0x0002, 0x3859: 0x0002, 0x385a: 0x0002, 0x385b: 0x0002, 0x385c: 0x0002, 0x385d: 0x0002,
0x385e: 0x0002, 0x385f: 0x0002, 0x3860: 0x0002,
0x386d: 0x0002, 0x386e: 0x0002, 0x386f: 0x0002,
0x3870: 0x0002, 0x3871: 0x0002, 0x3872: 0x0002, 0x3873: 0x0002, 0x3874: 0x0002, 0x3875: 0x0002,
0x3877: 0x0002, 0x3878: 0x0002, 0x3879: 0x0002, 0x387a: 0x0002, 0x387b: 0x0002,
0x387c: 0x0002, 0x387d: 0x0002, 0x387e: 0x0002, 0x387f: 0x0002,
// Block 0xe2, offset 0x3880
0x3880: 0x0004, 0x3881: 0x0004, 0x3882: 0x0004, 0x3883: 0x0004, 0x3884: 0x0004, 0x3885: 0x0004,
0x3886: 0x0004, 0x3887: 0x0004, 0x3888: 0x0004, 0x3889: 0x0004, 0x388a: 0x0004, 0x388b: 0x0004,
0x388c: 0x0004, 0x388d: 0x0004, 0x388e: 0x0004, 0x388f: 0x0004, 0x3890: 0x0004, 0x3891: 0x0004,
0x3892: 0x0004, 0x3893: 0x0004,
0x38a0: 0x0004, 0x38a1: 0x0004, 0x38a2: 0x0004, 0x38a3: 0x0004,
0x38a4: 0x0004, 0x38a5: 0x0004, 0x38a6: 0x0004, 0x38a7: 0x0004, 0x38a8: 0x0004, 0x38a9: 0x0004,
0x38aa: 0x0004, 0x38ab: 0x0004, 0x38ac: 0x0004, 0x38ad: 0x0004, 0x38ae: 0x0004, 0x38af: 0x0004,
0x38b0: 0x0004, 0x38b1: 0x0004, 0x38b2: 0x0004, 0x38b3: 0x0004, 0x38b4: 0x0004, 0x38b5: 0x0004,
0x38b6: 0x0004, 0x38b7: 0x0004, 0x38b8: 0x0004, 0x38b9: 0x0004, 0x38ba: 0x0004, 0x38bb: 0x0004,
0x38bc: 0x0004, 0x38bd: 0x0004, 0x38be: 0x0004, 0x38bf: 0x0004,
0x3880: 0x0002, 0x3881: 0x0002, 0x3882: 0x0002, 0x3883: 0x0002, 0x3884: 0x0002, 0x3885: 0x0002,
0x3886: 0x0002, 0x3887: 0x0002, 0x3888: 0x0002, 0x3889: 0x0002, 0x388a: 0x0002, 0x388b: 0x0002,
0x388c: 0x0002, 0x388d: 0x0002, 0x388e: 0x0002, 0x388f: 0x0002, 0x3890: 0x0002, 0x3891: 0x0002,
0x3892: 0x0002, 0x3893: 0x0002, 0x3894: 0x0002, 0x3895: 0x0002, 0x3896: 0x0002, 0x3897: 0x0002,
0x3898: 0x0002, 0x3899: 0x0002, 0x389a: 0x0002, 0x389b: 0x0002, 0x389c: 0x0002, 0x389d: 0x0002,
0x389e: 0x0002, 0x389f: 0x0002, 0x38a0: 0x0002, 0x38a1: 0x0002, 0x38a2: 0x0002, 0x38a3: 0x0002,
0x38a4: 0x0002, 0x38a5: 0x0002, 0x38a6: 0x0002, 0x38a7: 0x0002, 0x38a8: 0x0002, 0x38a9: 0x0002,
0x38aa: 0x0002, 0x38ab: 0x0002, 0x38ac: 0x0002, 0x38ad: 0x0002, 0x38ae: 0x0002, 0x38af: 0x0002,
0x38b0: 0x0002, 0x38b1: 0x0002, 0x38b2: 0x0002, 0x38b3: 0x0002, 0x38b4: 0x0002, 0x38b5: 0x0002,
0x38b6: 0x0002, 0x38b7: 0x0002, 0x38b8: 0x0002, 0x38b9: 0x0002, 0x38ba: 0x0002, 0x38bb: 0x0002,
0x38bc: 0x0002, 0x38be: 0x0002, 0x38bf: 0x0002,
// Block 0xe3, offset 0x38c0
0x38c0: 0x0004, 0x38c1: 0x0004, 0x38c2: 0x0004, 0x38c3: 0x0004, 0x38c4: 0x0004, 0x38c5: 0x0004,
0x38c6: 0x0004, 0x38c7: 0x0004, 0x38c8: 0x0004, 0x38c9: 0x0004, 0x38ca: 0x0004,
0x38cf: 0x0004, 0x38d0: 0x0004, 0x38d1: 0x0004,
0x38d2: 0x0004, 0x38d3: 0x0004,
0x38e0: 0x0004, 0x38e1: 0x0004, 0x38e2: 0x0004, 0x38e3: 0x0004,
0x38e4: 0x0004, 0x38e5: 0x0004, 0x38e6: 0x0004, 0x38e7: 0x0004, 0x38e8: 0x0004, 0x38e9: 0x0004,
0x38ea: 0x0004, 0x38eb: 0x0004, 0x38ec: 0x0004, 0x38ed: 0x0004, 0x38ee: 0x0004, 0x38ef: 0x0004,
0x38f0: 0x0004, 0x38f4: 0x0004,
0x38f8: 0x0004, 0x38f9: 0x0004, 0x38fa: 0x0004, 0x38fb: 0x0002,
0x38c0: 0x0002, 0x38c1: 0x0002, 0x38c2: 0x0002, 0x38c3: 0x0002, 0x38c4: 0x0002, 0x38c5: 0x0002,
0x38c6: 0x0002, 0x38c7: 0x0002, 0x38c8: 0x0002, 0x38c9: 0x0002, 0x38ca: 0x0002, 0x38cb: 0x0002,
0x38cc: 0x0002, 0x38cd: 0x0002, 0x38ce: 0x0002, 0x38cf: 0x0002, 0x38d0: 0x0002, 0x38d1: 0x0002,
0x38d2: 0x0002, 0x38d3: 0x0002,
0x38e0: 0x0002, 0x38e1: 0x0002, 0x38e2: 0x0002, 0x38e3: 0x0002,
0x38e4: 0x0002, 0x38e5: 0x0002, 0x38e6: 0x0002, 0x38e7: 0x0002, 0x38e8: 0x0002, 0x38e9: 0x0002,
0x38ea: 0x0002, 0x38eb: 0x0002, 0x38ec: 0x0002, 0x38ed: 0x0002, 0x38ee: 0x0002, 0x38ef: 0x0002,
0x38f0: 0x0002, 0x38f1: 0x0002, 0x38f2: 0x0002, 0x38f3: 0x0002, 0x38f4: 0x0002, 0x38f5: 0x0002,
0x38f6: 0x0002, 0x38f7: 0x0002, 0x38f8: 0x0002, 0x38f9: 0x0002, 0x38fa: 0x0002, 0x38fb: 0x0002,
0x38fc: 0x0002, 0x38fd: 0x0002, 0x38fe: 0x0002, 0x38ff: 0x0002,
// Block 0xe4, offset 0x3900
0x3900: 0x0004, 0x3901: 0x0004, 0x3902: 0x0004, 0x3903: 0x0004, 0x3904: 0x0004, 0x3905: 0x0004,
0x3906: 0x0004, 0x3907: 0x0004, 0x3908: 0x0004, 0x3909: 0x0004, 0x390a: 0x0004, 0x390b: 0x0004,
0x390c: 0x0004, 0x390d: 0x0004, 0x390e: 0x0004, 0x390f: 0x0004, 0x3910: 0x0004, 0x3911: 0x0004,
0x3912: 0x0004, 0x3913: 0x0004, 0x3914: 0x0004, 0x3915: 0x0004, 0x3916: 0x0004, 0x3917: 0x0004,
0x3918: 0x0004, 0x3919: 0x0004, 0x391a: 0x0004, 0x391b: 0x0004, 0x391c: 0x0004, 0x391d: 0x0004,
0x391e: 0x0004, 0x391f: 0x0004, 0x3920: 0x0004, 0x3921: 0x0004, 0x3922: 0x0004, 0x3923: 0x0004,
0x3924: 0x0004, 0x3925: 0x0004, 0x3926: 0x0004, 0x3927: 0x0004, 0x3928: 0x0004, 0x3929: 0x0004,
0x392a: 0x0004, 0x392b: 0x0004, 0x392c: 0x0004, 0x392d: 0x0004, 0x392e: 0x0004, 0x392f: 0x0004,
0x3930: 0x0004, 0x3931: 0x0004, 0x3932: 0x0004, 0x3933: 0x0004, 0x3934: 0x0004, 0x3935: 0x0004,
0x3936: 0x0004, 0x3937: 0x0004, 0x3938: 0x0004, 0x3939: 0x0004, 0x393a: 0x0004, 0x393b: 0x0004,
0x393c: 0x0004, 0x393d: 0x0004, 0x393e: 0x0004,
0x3900: 0x0002, 0x3901: 0x0002, 0x3902: 0x0002, 0x3903: 0x0002, 0x3904: 0x0002, 0x3905: 0x0002,
0x3906: 0x0002, 0x3907: 0x0002, 0x3908: 0x0002, 0x3909: 0x0002, 0x390a: 0x0002,
0x390f: 0x0002, 0x3910: 0x0002, 0x3911: 0x0002,
0x3912: 0x0002, 0x3913: 0x0002,
0x3920: 0x0002, 0x3921: 0x0002, 0x3922: 0x0002, 0x3923: 0x0002,
0x3924: 0x0002, 0x3925: 0x0002, 0x3926: 0x0002, 0x3927: 0x0002, 0x3928: 0x0002, 0x3929: 0x0002,
0x392a: 0x0002, 0x392b: 0x0002, 0x392c: 0x0002, 0x392d: 0x0002, 0x392e: 0x0002, 0x392f: 0x0002,
0x3930: 0x0002, 0x3934: 0x0002,
0x3938: 0x0002, 0x3939: 0x0002, 0x393a: 0x0002, 0x393b: 0x0002,
0x393c: 0x0002, 0x393d: 0x0002, 0x393e: 0x0002, 0x393f: 0x0002,
// Block 0xe5, offset 0x3940
0x3940: 0x0004, 0x3942: 0x0004, 0x3943: 0x0004, 0x3944: 0x0004, 0x3945: 0x0004,
0x3946: 0x0004, 0x3947: 0x0004, 0x3948: 0x0004, 0x3949: 0x0004, 0x394a: 0x0004, 0x394b: 0x0004,
0x394c: 0x0004, 0x394d: 0x0004, 0x394e: 0x0004, 0x394f: 0x0004, 0x3950: 0x0004, 0x3951: 0x0004,
0x3952: 0x0004, 0x3953: 0x0004, 0x3954: 0x0004, 0x3955: 0x0004, 0x3956: 0x0004, 0x3957: 0x0004,
0x3958: 0x0004, 0x3959: 0x0004, 0x395a: 0x0004, 0x395b: 0x0004, 0x395c: 0x0004, 0x395d: 0x0004,
0x395e: 0x0004, 0x395f: 0x0004, 0x3960: 0x0004, 0x3961: 0x0004, 0x3962: 0x0004, 0x3963: 0x0004,
0x3964: 0x0004, 0x3965: 0x0004, 0x3966: 0x0004, 0x3967: 0x0004, 0x3968: 0x0004, 0x3969: 0x0004,
0x396a: 0x0004, 0x396b: 0x0004, 0x396c: 0x0004, 0x396d: 0x0004, 0x396e: 0x0004, 0x396f: 0x0004,
0x3970: 0x0004, 0x3971: 0x0004, 0x3972: 0x0004, 0x3973: 0x0004, 0x3974: 0x0004, 0x3975: 0x0004,
0x3976: 0x0004, 0x3977: 0x0004, 0x3978: 0x0004, 0x3979: 0x0004, 0x397a: 0x0004, 0x397b: 0x0004,
0x397c: 0x0004, 0x397d: 0x0004, 0x397e: 0x0004, 0x397f: 0x0004,
0x3940: 0x0002, 0x3941: 0x0002, 0x3942: 0x0002, 0x3943: 0x0002, 0x3944: 0x0002, 0x3945: 0x0002,
0x3946: 0x0002, 0x3947: 0x0002, 0x3948: 0x0002, 0x3949: 0x0002, 0x394a: 0x0002, 0x394b: 0x0002,
0x394c: 0x0002, 0x394d: 0x0002, 0x394e: 0x0002, 0x394f: 0x0002, 0x3950: 0x0002, 0x3951: 0x0002,
0x3952: 0x0002, 0x3953: 0x0002, 0x3954: 0x0002, 0x3955: 0x0002, 0x3956: 0x0002, 0x3957: 0x0002,
0x3958: 0x0002, 0x3959: 0x0002, 0x395a: 0x0002, 0x395b: 0x0002, 0x395c: 0x0002, 0x395d: 0x0002,
0x395e: 0x0002, 0x395f: 0x0002, 0x3960: 0x0002, 0x3961: 0x0002, 0x3962: 0x0002, 0x3963: 0x0002,
0x3964: 0x0002, 0x3965: 0x0002, 0x3966: 0x0002, 0x3967: 0x0002, 0x3968: 0x0002, 0x3969: 0x0002,
0x396a: 0x0002, 0x396b: 0x0002, 0x396c: 0x0002, 0x396d: 0x0002, 0x396e: 0x0002, 0x396f: 0x0002,
0x3970: 0x0002, 0x3971: 0x0002, 0x3972: 0x0002, 0x3973: 0x0002, 0x3974: 0x0002, 0x3975: 0x0002,
0x3976: 0x0002, 0x3977: 0x0002, 0x3978: 0x0002, 0x3979: 0x0002, 0x397a: 0x0002, 0x397b: 0x0002,
0x397c: 0x0002, 0x397d: 0x0002, 0x397e: 0x0002,
// Block 0xe6, offset 0x3980
0x3980: 0x0004, 0x3981: 0x0004, 0x3982: 0x0004, 0x3983: 0x0004, 0x3984: 0x0004, 0x3985: 0x0004,
0x3986: 0x0004, 0x3987: 0x0004, 0x3988: 0x0004, 0x3989: 0x0004, 0x398a: 0x0004, 0x398b: 0x0004,
0x398c: 0x0004, 0x398d: 0x0004, 0x398e: 0x0004, 0x398f: 0x0004, 0x3990: 0x0004, 0x3991: 0x0004,
0x3992: 0x0004, 0x3993: 0x0004, 0x3994: 0x0004, 0x3995: 0x0004, 0x3996: 0x0004, 0x3997: 0x0004,
0x3998: 0x0004, 0x3999: 0x0004, 0x399a: 0x0004, 0x399b: 0x0004, 0x399c: 0x0004, 0x399d: 0x0004,
0x399e: 0x0004, 0x399f: 0x0004, 0x39a0: 0x0004, 0x39a1: 0x0004, 0x39a2: 0x0004, 0x39a3: 0x0004,
0x39a4: 0x0004, 0x39a5: 0x0004, 0x39a6: 0x0004, 0x39a7: 0x0004, 0x39a8: 0x0004, 0x39a9: 0x0004,
0x39aa: 0x0004, 0x39ab: 0x0004, 0x39ac: 0x0004, 0x39ad: 0x0004, 0x39ae: 0x0004, 0x39af: 0x0004,
0x39b0: 0x0004, 0x39b1: 0x0004, 0x39b2: 0x0004, 0x39b3: 0x0004, 0x39b4: 0x0004, 0x39b5: 0x0004,
0x39b6: 0x0004, 0x39b7: 0x0004, 0x39b8: 0x0004, 0x39b9: 0x0004, 0x39ba: 0x0004, 0x39bb: 0x0004,
0x39bc: 0x0004, 0x39bd: 0x0004, 0x39be: 0x0004, 0x39bf: 0x0004,
0x3980: 0x0002, 0x3982: 0x0002, 0x3983: 0x0002, 0x3984: 0x0002, 0x3985: 0x0002,
0x3986: 0x0002, 0x3987: 0x0002, 0x3988: 0x0002, 0x3989: 0x0002, 0x398a: 0x0002, 0x398b: 0x0002,
0x398c: 0x0002, 0x398d: 0x0002, 0x398e: 0x0002, 0x398f: 0x0002, 0x3990: 0x0002, 0x3991: 0x0002,
0x3992: 0x0002, 0x3993: 0x0002, 0x3994: 0x0002, 0x3995: 0x0002, 0x3996: 0x0002, 0x3997: 0x0002,
0x3998: 0x0002, 0x3999: 0x0002, 0x399a: 0x0002, 0x399b: 0x0002, 0x399c: 0x0002, 0x399d: 0x0002,
0x399e: 0x0002, 0x399f: 0x0002, 0x39a0: 0x0002, 0x39a1: 0x0002, 0x39a2: 0x0002, 0x39a3: 0x0002,
0x39a4: 0x0002, 0x39a5: 0x0002, 0x39a6: 0x0002, 0x39a7: 0x0002, 0x39a8: 0x0002, 0x39a9: 0x0002,
0x39aa: 0x0002, 0x39ab: 0x0002, 0x39ac: 0x0002, 0x39ad: 0x0002, 0x39ae: 0x0002, 0x39af: 0x0002,
0x39b0: 0x0002, 0x39b1: 0x0002, 0x39b2: 0x0002, 0x39b3: 0x0002, 0x39b4: 0x0002, 0x39b5: 0x0002,
0x39b6: 0x0002, 0x39b7: 0x0002, 0x39b8: 0x0002, 0x39b9: 0x0002, 0x39ba: 0x0002, 0x39bb: 0x0002,
0x39bc: 0x0002, 0x39bd: 0x0002, 0x39be: 0x0002, 0x39bf: 0x0002,
// Block 0xe7, offset 0x39c0
0x39c0: 0x0004, 0x39c1: 0x0004, 0x39c2: 0x0004, 0x39c3: 0x0004, 0x39c4: 0x0004, 0x39c5: 0x0004,
0x39c6: 0x0004, 0x39c7: 0x0004, 0x39c8: 0x0004, 0x39c9: 0x0004, 0x39ca: 0x0004, 0x39cb: 0x0004,
0x39cc: 0x0004, 0x39cd: 0x0004, 0x39ce: 0x0004, 0x39cf: 0x0004, 0x39d0: 0x0004, 0x39d1: 0x0004,
0x39d2: 0x0004, 0x39d3: 0x0004, 0x39d4: 0x0004, 0x39d5: 0x0004, 0x39d6: 0x0004, 0x39d7: 0x0004,
0x39d8: 0x0004, 0x39d9: 0x0004, 0x39da: 0x0004, 0x39db: 0x0004, 0x39dc: 0x0004, 0x39dd: 0x0004,
0x39de: 0x0004, 0x39df: 0x0004, 0x39e0: 0x0004, 0x39e1: 0x0004, 0x39e2: 0x0004, 0x39e3: 0x0004,
0x39e4: 0x0004, 0x39e5: 0x0004, 0x39e6: 0x0004, 0x39e7: 0x0004, 0x39e8: 0x0004, 0x39e9: 0x0004,
0x39ea: 0x0004, 0x39eb: 0x0004, 0x39ec: 0x0004, 0x39ed: 0x0004, 0x39ee: 0x0004, 0x39ef: 0x0004,
0x39f0: 0x0004, 0x39f1: 0x0004, 0x39f2: 0x0004, 0x39f3: 0x0004, 0x39f4: 0x0004, 0x39f5: 0x0004,
0x39f6: 0x0004, 0x39f7: 0x0004, 0x39f8: 0x0004, 0x39f9: 0x0004, 0x39fa: 0x0004, 0x39fb: 0x0004,
0x39fc: 0x0004, 0x39ff: 0x0004,
0x39c0: 0x0002, 0x39c1: 0x0002, 0x39c2: 0x0002, 0x39c3: 0x0002, 0x39c4: 0x0002, 0x39c5: 0x0002,
0x39c6: 0x0002, 0x39c7: 0x0002, 0x39c8: 0x0002, 0x39c9: 0x0002, 0x39ca: 0x0002, 0x39cb: 0x0002,
0x39cc: 0x0002, 0x39cd: 0x0002, 0x39ce: 0x0002, 0x39cf: 0x0002, 0x39d0: 0x0002, 0x39d1: 0x0002,
0x39d2: 0x0002, 0x39d3: 0x0002, 0x39d4: 0x0002, 0x39d5: 0x0002, 0x39d6: 0x0002, 0x39d7: 0x0002,
0x39d8: 0x0002, 0x39d9: 0x0002, 0x39da: 0x0002, 0x39db: 0x0002, 0x39dc: 0x0002, 0x39dd: 0x0002,
0x39de: 0x0002, 0x39df: 0x0002, 0x39e0: 0x0002, 0x39e1: 0x0002, 0x39e2: 0x0002, 0x39e3: 0x0002,
0x39e4: 0x0002, 0x39e5: 0x0002, 0x39e6: 0x0002, 0x39e7: 0x0002, 0x39e8: 0x0002, 0x39e9: 0x0002,
0x39ea: 0x0002, 0x39eb: 0x0002, 0x39ec: 0x0002, 0x39ed: 0x0002, 0x39ee: 0x0002, 0x39ef: 0x0002,
0x39f0: 0x0002, 0x39f1: 0x0002, 0x39f2: 0x0002, 0x39f3: 0x0002, 0x39f4: 0x0002, 0x39f5: 0x0002,
0x39f6: 0x0002, 0x39f7: 0x0002, 0x39f8: 0x0002, 0x39f9: 0x0002, 0x39fa: 0x0002, 0x39fb: 0x0002,
0x39fc: 0x0002, 0x39ff: 0x0002,
// Block 0xe8, offset 0x3a00
0x3a00: 0x0004, 0x3a01: 0x0004, 0x3a02: 0x0004, 0x3a03: 0x0004, 0x3a04: 0x0004, 0x3a05: 0x0004,
0x3a06: 0x0004, 0x3a07: 0x0004, 0x3a08: 0x0004, 0x3a09: 0x0004, 0x3a0a: 0x0004, 0x3a0b: 0x0004,
0x3a0c: 0x0004, 0x3a0d: 0x0004, 0x3a0e: 0x0004, 0x3a0f: 0x0004, 0x3a10: 0x0004, 0x3a11: 0x0004,
0x3a12: 0x0004, 0x3a13: 0x0004, 0x3a14: 0x0004, 0x3a15: 0x0004, 0x3a16: 0x0004, 0x3a17: 0x0004,
0x3a18: 0x0004, 0x3a19: 0x0004, 0x3a1a: 0x0004, 0x3a1b: 0x0004, 0x3a1c: 0x0004, 0x3a1d: 0x0004,
0x3a1e: 0x0004, 0x3a1f: 0x0004, 0x3a20: 0x0004, 0x3a21: 0x0004, 0x3a22: 0x0004, 0x3a23: 0x0004,
0x3a24: 0x0004, 0x3a25: 0x0004, 0x3a26: 0x0004, 0x3a27: 0x0004, 0x3a28: 0x0004, 0x3a29: 0x0004,
0x3a2a: 0x0004, 0x3a2b: 0x0004, 0x3a2c: 0x0004, 0x3a2d: 0x0004, 0x3a2e: 0x0004, 0x3a2f: 0x0004,
0x3a30: 0x0004, 0x3a31: 0x0004, 0x3a32: 0x0004, 0x3a33: 0x0004, 0x3a34: 0x0004, 0x3a35: 0x0004,
0x3a36: 0x0004, 0x3a37: 0x0004, 0x3a38: 0x0004, 0x3a39: 0x0004, 0x3a3a: 0x0004, 0x3a3b: 0x0004,
0x3a3c: 0x0004, 0x3a3d: 0x0004,
0x3a00: 0x0002, 0x3a01: 0x0002, 0x3a02: 0x0002, 0x3a03: 0x0002, 0x3a04: 0x0002, 0x3a05: 0x0002,
0x3a06: 0x0002, 0x3a07: 0x0002, 0x3a08: 0x0002, 0x3a09: 0x0002, 0x3a0a: 0x0002, 0x3a0b: 0x0002,
0x3a0c: 0x0002, 0x3a0d: 0x0002, 0x3a0e: 0x0002, 0x3a0f: 0x0002, 0x3a10: 0x0002, 0x3a11: 0x0002,
0x3a12: 0x0002, 0x3a13: 0x0002, 0x3a14: 0x0002, 0x3a15: 0x0002, 0x3a16: 0x0002, 0x3a17: 0x0002,
0x3a18: 0x0002, 0x3a19: 0x0002, 0x3a1a: 0x0002, 0x3a1b: 0x0002, 0x3a1c: 0x0002, 0x3a1d: 0x0002,
0x3a1e: 0x0002, 0x3a1f: 0x0002, 0x3a20: 0x0002, 0x3a21: 0x0002, 0x3a22: 0x0002, 0x3a23: 0x0002,
0x3a24: 0x0002, 0x3a25: 0x0002, 0x3a26: 0x0002, 0x3a27: 0x0002, 0x3a28: 0x0002, 0x3a29: 0x0002,
0x3a2a: 0x0002, 0x3a2b: 0x0002, 0x3a2c: 0x0002, 0x3a2d: 0x0002, 0x3a2e: 0x0002, 0x3a2f: 0x0002,
0x3a30: 0x0002, 0x3a31: 0x0002, 0x3a32: 0x0002, 0x3a33: 0x0002, 0x3a34: 0x0002, 0x3a35: 0x0002,
0x3a36: 0x0002, 0x3a37: 0x0002, 0x3a38: 0x0002, 0x3a39: 0x0002, 0x3a3a: 0x0002, 0x3a3b: 0x0002,
0x3a3c: 0x0002, 0x3a3d: 0x0002,
// Block 0xe9, offset 0x3a40
0x3a4b: 0x0004,
0x3a4c: 0x0004, 0x3a4d: 0x0004, 0x3a4e: 0x0004, 0x3a50: 0x0004, 0x3a51: 0x0004,
0x3a52: 0x0004, 0x3a53: 0x0004, 0x3a54: 0x0004, 0x3a55: 0x0004, 0x3a56: 0x0004, 0x3a57: 0x0004,
0x3a58: 0x0004, 0x3a59: 0x0004, 0x3a5a: 0x0004, 0x3a5b: 0x0004, 0x3a5c: 0x0004, 0x3a5d: 0x0004,
0x3a5e: 0x0004, 0x3a5f: 0x0004, 0x3a60: 0x0004, 0x3a61: 0x0004, 0x3a62: 0x0004, 0x3a63: 0x0004,
0x3a64: 0x0004, 0x3a65: 0x0004, 0x3a66: 0x0004, 0x3a67: 0x0004,
0x3a7a: 0x0004,
0x3a4b: 0x0002,
0x3a4c: 0x0002, 0x3a4d: 0x0002, 0x3a4e: 0x0002, 0x3a50: 0x0002, 0x3a51: 0x0002,
0x3a52: 0x0002, 0x3a53: 0x0002, 0x3a54: 0x0002, 0x3a55: 0x0002, 0x3a56: 0x0002, 0x3a57: 0x0002,
0x3a58: 0x0002, 0x3a59: 0x0002, 0x3a5a: 0x0002, 0x3a5b: 0x0002, 0x3a5c: 0x0002, 0x3a5d: 0x0002,
0x3a5e: 0x0002, 0x3a5f: 0x0002, 0x3a60: 0x0002, 0x3a61: 0x0002, 0x3a62: 0x0002, 0x3a63: 0x0002,
0x3a64: 0x0002, 0x3a65: 0x0002, 0x3a66: 0x0002, 0x3a67: 0x0002,
0x3a7a: 0x0002,
// Block 0xea, offset 0x3a80
0x3a95: 0x0004, 0x3a96: 0x0004,
0x3aa4: 0x0004,
0x3a95: 0x0002, 0x3a96: 0x0002,
0x3aa4: 0x0002,
// Block 0xeb, offset 0x3ac0
0x3afb: 0x0004,
0x3afc: 0x0004, 0x3afd: 0x0004, 0x3afe: 0x0004, 0x3aff: 0x0004,
0x3afb: 0x0002,
0x3afc: 0x0002, 0x3afd: 0x0002, 0x3afe: 0x0002, 0x3aff: 0x0002,
// Block 0xec, offset 0x3b00
0x3b00: 0x0004, 0x3b01: 0x0004, 0x3b02: 0x0004, 0x3b03: 0x0004, 0x3b04: 0x0004, 0x3b05: 0x0004,
0x3b06: 0x0004, 0x3b07: 0x0004, 0x3b08: 0x0004, 0x3b09: 0x0004, 0x3b0a: 0x0004, 0x3b0b: 0x0004,
0x3b0c: 0x0004, 0x3b0d: 0x0004, 0x3b0e: 0x0004, 0x3b0f: 0x0004,
0x3b00: 0x0002, 0x3b01: 0x0002, 0x3b02: 0x0002, 0x3b03: 0x0002, 0x3b04: 0x0002, 0x3b05: 0x0002,
0x3b06: 0x0002, 0x3b07: 0x0002, 0x3b08: 0x0002, 0x3b09: 0x0002, 0x3b0a: 0x0002, 0x3b0b: 0x0002,
0x3b0c: 0x0002, 0x3b0d: 0x0002, 0x3b0e: 0x0002, 0x3b0f: 0x0002,
// Block 0xed, offset 0x3b40
0x3b40: 0x0004, 0x3b41: 0x0004, 0x3b42: 0x0004, 0x3b43: 0x0004, 0x3b44: 0x0004, 0x3b45: 0x0004,
0x3b4c: 0x0004, 0x3b50: 0x0004, 0x3b51: 0x0004,
0x3b52: 0x0004, 0x3b55: 0x0004, 0x3b56: 0x0004, 0x3b57: 0x0004,
0x3b5c: 0x0004, 0x3b5d: 0x0004,
0x3b5e: 0x0004, 0x3b5f: 0x0004,
0x3b6b: 0x0004, 0x3b6c: 0x0004,
0x3b74: 0x0004, 0x3b75: 0x0004,
0x3b76: 0x0004, 0x3b77: 0x0004, 0x3b78: 0x0004, 0x3b79: 0x0004, 0x3b7a: 0x0004, 0x3b7b: 0x0004,
0x3b7c: 0x0004,
0x3b40: 0x0002, 0x3b41: 0x0002, 0x3b42: 0x0002, 0x3b43: 0x0002, 0x3b44: 0x0002, 0x3b45: 0x0002,
0x3b4c: 0x0002, 0x3b50: 0x0002, 0x3b51: 0x0002,
0x3b52: 0x0002, 0x3b55: 0x0002, 0x3b56: 0x0002, 0x3b57: 0x0002,
0x3b5c: 0x0002, 0x3b5d: 0x0002,
0x3b5e: 0x0002, 0x3b5f: 0x0002,
0x3b6b: 0x0002, 0x3b6c: 0x0002,
0x3b74: 0x0002, 0x3b75: 0x0002,
0x3b76: 0x0002, 0x3b77: 0x0002, 0x3b78: 0x0002, 0x3b79: 0x0002, 0x3b7a: 0x0002, 0x3b7b: 0x0002,
0x3b7c: 0x0002,
// Block 0xee, offset 0x3b80
0x3ba0: 0x0004, 0x3ba1: 0x0004, 0x3ba2: 0x0004, 0x3ba3: 0x0004,
0x3ba4: 0x0004, 0x3ba5: 0x0004, 0x3ba6: 0x0004, 0x3ba7: 0x0004, 0x3ba8: 0x0004, 0x3ba9: 0x0004,
0x3baa: 0x0004, 0x3bab: 0x0004,
0x3bb0: 0x0004,
0x3ba0: 0x0002, 0x3ba1: 0x0002, 0x3ba2: 0x0002, 0x3ba3: 0x0002,
0x3ba4: 0x0002, 0x3ba5: 0x0002, 0x3ba6: 0x0002, 0x3ba7: 0x0002, 0x3ba8: 0x0002, 0x3ba9: 0x0002,
0x3baa: 0x0002, 0x3bab: 0x0002,
0x3bb0: 0x0002,
// Block 0xef, offset 0x3bc0
0x3bcc: 0x0004, 0x3bcd: 0x0004, 0x3bce: 0x0004, 0x3bcf: 0x0004, 0x3bd0: 0x0004, 0x3bd1: 0x0004,
0x3bd2: 0x0004, 0x3bd3: 0x0004, 0x3bd4: 0x0004, 0x3bd5: 0x0004, 0x3bd6: 0x0004, 0x3bd7: 0x0004,
0x3bd8: 0x0004, 0x3bd9: 0x0004, 0x3bda: 0x0004, 0x3bdb: 0x0004, 0x3bdc: 0x0004, 0x3bdd: 0x0004,
0x3bde: 0x0004, 0x3bdf: 0x0004, 0x3be0: 0x0004, 0x3be1: 0x0004, 0x3be2: 0x0004, 0x3be3: 0x0004,
0x3be4: 0x0004, 0x3be5: 0x0004, 0x3be6: 0x0004, 0x3be7: 0x0004, 0x3be8: 0x0004, 0x3be9: 0x0004,
0x3bea: 0x0004, 0x3beb: 0x0004, 0x3bec: 0x0004, 0x3bed: 0x0004, 0x3bee: 0x0004, 0x3bef: 0x0004,
0x3bf0: 0x0004, 0x3bf1: 0x0004, 0x3bf2: 0x0004, 0x3bf3: 0x0004, 0x3bf4: 0x0004, 0x3bf5: 0x0004,
0x3bf6: 0x0004, 0x3bf7: 0x0004, 0x3bf8: 0x0004, 0x3bf9: 0x0004, 0x3bfa: 0x0004,
0x3bfc: 0x0004, 0x3bfd: 0x0004, 0x3bfe: 0x0004, 0x3bff: 0x0004,
0x3bcc: 0x0002, 0x3bcd: 0x0002, 0x3bce: 0x0002, 0x3bcf: 0x0002, 0x3bd0: 0x0002, 0x3bd1: 0x0002,
0x3bd2: 0x0002, 0x3bd3: 0x0002, 0x3bd4: 0x0002, 0x3bd5: 0x0002, 0x3bd6: 0x0002, 0x3bd7: 0x0002,
0x3bd8: 0x0002, 0x3bd9: 0x0002, 0x3bda: 0x0002, 0x3bdb: 0x0002, 0x3bdc: 0x0002, 0x3bdd: 0x0002,
0x3bde: 0x0002, 0x3bdf: 0x0002, 0x3be0: 0x0002, 0x3be1: 0x0002, 0x3be2: 0x0002, 0x3be3: 0x0002,
0x3be4: 0x0002, 0x3be5: 0x0002, 0x3be6: 0x0002, 0x3be7: 0x0002, 0x3be8: 0x0002, 0x3be9: 0x0002,
0x3bea: 0x0002, 0x3beb: 0x0002, 0x3bec: 0x0002, 0x3bed: 0x0002, 0x3bee: 0x0002, 0x3bef: 0x0002,
0x3bf0: 0x0002, 0x3bf1: 0x0002, 0x3bf2: 0x0002, 0x3bf3: 0x0002, 0x3bf4: 0x0002, 0x3bf5: 0x0002,
0x3bf6: 0x0002, 0x3bf7: 0x0002, 0x3bf8: 0x0002, 0x3bf9: 0x0002, 0x3bfa: 0x0002,
0x3bfc: 0x0002, 0x3bfd: 0x0002, 0x3bfe: 0x0002, 0x3bff: 0x0002,
// Block 0xf0, offset 0x3c00
0x3c00: 0x0004, 0x3c01: 0x0004, 0x3c02: 0x0004, 0x3c03: 0x0004, 0x3c04: 0x0004, 0x3c05: 0x0004,
0x3c07: 0x0004, 0x3c08: 0x0004, 0x3c09: 0x0004, 0x3c0a: 0x0004, 0x3c0b: 0x0004,
0x3c0c: 0x0004, 0x3c0d: 0x0004, 0x3c0e: 0x0004, 0x3c0f: 0x0004, 0x3c10: 0x0004, 0x3c11: 0x0004,
0x3c12: 0x0004, 0x3c13: 0x0004, 0x3c14: 0x0004, 0x3c15: 0x0004, 0x3c16: 0x0004, 0x3c17: 0x0004,
0x3c18: 0x0004, 0x3c19: 0x0004, 0x3c1a: 0x0004, 0x3c1b: 0x0004, 0x3c1c: 0x0004, 0x3c1d: 0x0004,
0x3c1e: 0x0004, 0x3c1f: 0x0004, 0x3c20: 0x0004, 0x3c21: 0x0004, 0x3c22: 0x0004, 0x3c23: 0x0004,
0x3c24: 0x0004, 0x3c25: 0x0004, 0x3c26: 0x0004, 0x3c27: 0x0004, 0x3c28: 0x0004, 0x3c29: 0x0004,
0x3c2a: 0x0004, 0x3c2b: 0x0004, 0x3c2c: 0x0004, 0x3c2d: 0x0004, 0x3c2e: 0x0004, 0x3c2f: 0x0004,
0x3c30: 0x0004, 0x3c31: 0x0004, 0x3c32: 0x0004, 0x3c33: 0x0004, 0x3c34: 0x0004, 0x3c35: 0x0004,
0x3c36: 0x0004, 0x3c37: 0x0004, 0x3c38: 0x0004, 0x3c39: 0x0004, 0x3c3a: 0x0004, 0x3c3b: 0x0004,
0x3c3c: 0x0004, 0x3c3d: 0x0004, 0x3c3e: 0x0004, 0x3c3f: 0x0004,
0x3c00: 0x0002, 0x3c01: 0x0002, 0x3c02: 0x0002, 0x3c03: 0x0002, 0x3c04: 0x0002, 0x3c05: 0x0002,
0x3c07: 0x0002, 0x3c08: 0x0002, 0x3c09: 0x0002, 0x3c0a: 0x0002, 0x3c0b: 0x0002,
0x3c0c: 0x0002, 0x3c0d: 0x0002, 0x3c0e: 0x0002, 0x3c0f: 0x0002, 0x3c10: 0x0002, 0x3c11: 0x0002,
0x3c12: 0x0002, 0x3c13: 0x0002, 0x3c14: 0x0002, 0x3c15: 0x0002, 0x3c16: 0x0002, 0x3c17: 0x0002,
0x3c18: 0x0002, 0x3c19: 0x0002, 0x3c1a: 0x0002, 0x3c1b: 0x0002, 0x3c1c: 0x0002, 0x3c1d: 0x0002,
0x3c1e: 0x0002, 0x3c1f: 0x0002, 0x3c20: 0x0002, 0x3c21: 0x0002, 0x3c22: 0x0002, 0x3c23: 0x0002,
0x3c24: 0x0002, 0x3c25: 0x0002, 0x3c26: 0x0002, 0x3c27: 0x0002, 0x3c28: 0x0002, 0x3c29: 0x0002,
0x3c2a: 0x0002, 0x3c2b: 0x0002, 0x3c2c: 0x0002, 0x3c2d: 0x0002, 0x3c2e: 0x0002, 0x3c2f: 0x0002,
0x3c30: 0x0002, 0x3c31: 0x0002, 0x3c32: 0x0002, 0x3c33: 0x0002, 0x3c34: 0x0002, 0x3c35: 0x0002,
0x3c36: 0x0002, 0x3c37: 0x0002, 0x3c38: 0x0002, 0x3c39: 0x0002, 0x3c3a: 0x0002, 0x3c3b: 0x0002,
0x3c3c: 0x0002, 0x3c3d: 0x0002, 0x3c3e: 0x0002, 0x3c3f: 0x0002,
// Block 0xf1, offset 0x3c40
0x3c70: 0x0004, 0x3c71: 0x0004, 0x3c72: 0x0004, 0x3c73: 0x0004, 0x3c74: 0x0004, 0x3c75: 0x0004,
0x3c76: 0x0004, 0x3c77: 0x0004, 0x3c78: 0x0004, 0x3c79: 0x0004, 0x3c7a: 0x0004, 0x3c7b: 0x0004,
0x3c7c: 0x0004,
0x3c70: 0x0002, 0x3c71: 0x0002, 0x3c72: 0x0002, 0x3c73: 0x0002, 0x3c74: 0x0002, 0x3c75: 0x0002,
0x3c76: 0x0002, 0x3c77: 0x0002, 0x3c78: 0x0002, 0x3c79: 0x0002, 0x3c7a: 0x0002, 0x3c7b: 0x0002,
0x3c7c: 0x0002,
// Block 0xf2, offset 0x3c80
0x3c80: 0x0004, 0x3c81: 0x0004, 0x3c82: 0x0004, 0x3c83: 0x0004, 0x3c84: 0x0004, 0x3c85: 0x0004,
0x3c86: 0x0004, 0x3c87: 0x0004, 0x3c88: 0x0004, 0x3c89: 0x0004,
0x3c8f: 0x0004, 0x3c90: 0x0004, 0x3c91: 0x0004,
0x3c92: 0x0004, 0x3c93: 0x0004, 0x3c94: 0x0004, 0x3c95: 0x0004, 0x3c96: 0x0004, 0x3c97: 0x0004,
0x3c98: 0x0004, 0x3c99: 0x0004, 0x3c9a: 0x0004, 0x3c9b: 0x0004, 0x3c9c: 0x0004, 0x3c9d: 0x0004,
0x3c9e: 0x0004, 0x3c9f: 0x0004, 0x3ca0: 0x0004, 0x3ca1: 0x0004, 0x3ca2: 0x0004, 0x3ca3: 0x0004,
0x3ca4: 0x0004, 0x3ca5: 0x0004, 0x3ca6: 0x0004, 0x3ca7: 0x0004, 0x3ca8: 0x0004, 0x3ca9: 0x0004,
0x3caa: 0x0004, 0x3cab: 0x0004, 0x3cac: 0x0004, 0x3cad: 0x0004, 0x3cae: 0x0004, 0x3caf: 0x0004,
0x3cb0: 0x0004, 0x3cb1: 0x0004, 0x3cb2: 0x0004, 0x3cb3: 0x0004, 0x3cb4: 0x0004, 0x3cb5: 0x0004,
0x3cb6: 0x0004, 0x3cb7: 0x0004, 0x3cb8: 0x0004, 0x3cb9: 0x0004, 0x3cba: 0x0004, 0x3cbb: 0x0004,
0x3cbc: 0x0004, 0x3cbd: 0x0004, 0x3cbe: 0x0004, 0x3cbf: 0x0004,
0x3c80: 0x0002, 0x3c81: 0x0002, 0x3c82: 0x0002, 0x3c83: 0x0002, 0x3c84: 0x0002, 0x3c85: 0x0002,
0x3c86: 0x0002, 0x3c87: 0x0002, 0x3c88: 0x0002, 0x3c89: 0x0002,
0x3c8f: 0x0002, 0x3c90: 0x0002, 0x3c91: 0x0002,
0x3c92: 0x0002, 0x3c93: 0x0002, 0x3c94: 0x0002, 0x3c95: 0x0002, 0x3c96: 0x0002, 0x3c97: 0x0002,
0x3c98: 0x0002, 0x3c99: 0x0002, 0x3c9a: 0x0002, 0x3c9b: 0x0002, 0x3c9c: 0x0002, 0x3c9d: 0x0002,
0x3c9e: 0x0002, 0x3c9f: 0x0002, 0x3ca0: 0x0002, 0x3ca1: 0x0002, 0x3ca2: 0x0002, 0x3ca3: 0x0002,
0x3ca4: 0x0002, 0x3ca5: 0x0002, 0x3ca6: 0x0002, 0x3ca7: 0x0002, 0x3ca8: 0x0002, 0x3ca9: 0x0002,
0x3caa: 0x0002, 0x3cab: 0x0002, 0x3cac: 0x0002, 0x3cad: 0x0002, 0x3cae: 0x0002, 0x3caf: 0x0002,
0x3cb0: 0x0002, 0x3cb1: 0x0002, 0x3cb2: 0x0002, 0x3cb3: 0x0002, 0x3cb4: 0x0002, 0x3cb5: 0x0002,
0x3cb6: 0x0002, 0x3cb7: 0x0002, 0x3cb8: 0x0002, 0x3cb9: 0x0002, 0x3cba: 0x0002, 0x3cbb: 0x0002,
0x3cbc: 0x0002, 0x3cbd: 0x0002, 0x3cbe: 0x0002, 0x3cbf: 0x0002,
// Block 0xf3, offset 0x3cc0
0x3cc0: 0x0004, 0x3cc1: 0x0004, 0x3cc2: 0x0004, 0x3cc3: 0x0004, 0x3cc4: 0x0004, 0x3cc5: 0x0004,
0x3cc6: 0x0004,
0x3cce: 0x0004, 0x3ccf: 0x0004, 0x3cd0: 0x0004, 0x3cd1: 0x0004,
0x3cd2: 0x0004, 0x3cd3: 0x0004, 0x3cd4: 0x0004, 0x3cd5: 0x0004, 0x3cd6: 0x0004, 0x3cd7: 0x0004,
0x3cd8: 0x0004, 0x3cd9: 0x0004, 0x3cda: 0x0004, 0x3cdb: 0x0004, 0x3cdc: 0x0004,
0x3cdf: 0x0004, 0x3ce0: 0x0004, 0x3ce1: 0x0004, 0x3ce2: 0x0004, 0x3ce3: 0x0004,
0x3ce4: 0x0004, 0x3ce5: 0x0004, 0x3ce6: 0x0004, 0x3ce7: 0x0004, 0x3ce8: 0x0004, 0x3ce9: 0x0004,
0x3cf0: 0x0004, 0x3cf1: 0x0004, 0x3cf2: 0x0004, 0x3cf3: 0x0004, 0x3cf4: 0x0004, 0x3cf5: 0x0004,
0x3cf6: 0x0004, 0x3cf7: 0x0004, 0x3cf8: 0x0004,
0x3cc0: 0x0002, 0x3cc1: 0x0002, 0x3cc2: 0x0002, 0x3cc3: 0x0002, 0x3cc4: 0x0002, 0x3cc5: 0x0002,
0x3cc6: 0x0002,
0x3cce: 0x0002, 0x3ccf: 0x0002, 0x3cd0: 0x0002, 0x3cd1: 0x0002,
0x3cd2: 0x0002, 0x3cd3: 0x0002, 0x3cd4: 0x0002, 0x3cd5: 0x0002, 0x3cd6: 0x0002, 0x3cd7: 0x0002,
0x3cd8: 0x0002, 0x3cd9: 0x0002, 0x3cda: 0x0002, 0x3cdb: 0x0002, 0x3cdc: 0x0002,
0x3cdf: 0x0002, 0x3ce0: 0x0002, 0x3ce1: 0x0002, 0x3ce2: 0x0002, 0x3ce3: 0x0002,
0x3ce4: 0x0002, 0x3ce5: 0x0002, 0x3ce6: 0x0002, 0x3ce7: 0x0002, 0x3ce8: 0x0002, 0x3ce9: 0x0002,
0x3cf0: 0x0002, 0x3cf1: 0x0002, 0x3cf2: 0x0002, 0x3cf3: 0x0002, 0x3cf4: 0x0002, 0x3cf5: 0x0002,
0x3cf6: 0x0002, 0x3cf7: 0x0002, 0x3cf8: 0x0002,
// Block 0xf4, offset 0x3d00
0x3d00: 0x0002, 0x3d01: 0x0002, 0x3d02: 0x0002, 0x3d03: 0x0002, 0x3d04: 0x0002, 0x3d05: 0x0002,
0x3d06: 0x0002, 0x3d07: 0x0002, 0x3d08: 0x0002, 0x3d09: 0x0002, 0x3d0a: 0x0002, 0x3d0b: 0x0002,
0x3d0c: 0x0002, 0x3d0d: 0x0002, 0x3d0e: 0x0002, 0x3d0f: 0x0002, 0x3d10: 0x0002, 0x3d11: 0x0002,
0x3d12: 0x0002, 0x3d13: 0x0002, 0x3d14: 0x0002, 0x3d15: 0x0002, 0x3d16: 0x0002, 0x3d17: 0x0002,
0x3d18: 0x0002, 0x3d19: 0x0002, 0x3d1a: 0x0002, 0x3d1b: 0x0002, 0x3d1c: 0x0002, 0x3d1d: 0x0002,
0x3d1e: 0x0002, 0x3d1f: 0x0002, 0x3d20: 0x0002, 0x3d21: 0x0002, 0x3d22: 0x0002, 0x3d23: 0x0002,
0x3d24: 0x0002, 0x3d25: 0x0002, 0x3d26: 0x0002, 0x3d27: 0x0002, 0x3d28: 0x0002, 0x3d29: 0x0002,
0x3d2a: 0x0002, 0x3d2b: 0x0002, 0x3d2c: 0x0002, 0x3d2d: 0x0002, 0x3d2e: 0x0002, 0x3d2f: 0x0002,
0x3d30: 0x0002, 0x3d31: 0x0002, 0x3d32: 0x0002, 0x3d33: 0x0002, 0x3d34: 0x0002, 0x3d35: 0x0002,
0x3d36: 0x0002, 0x3d37: 0x0002, 0x3d38: 0x0002, 0x3d39: 0x0002, 0x3d3a: 0x0002, 0x3d3b: 0x0002,
0x3d3c: 0x0002, 0x3d3d: 0x0002,
0x3d01: 0x0001,
0x3d20: 0x0001, 0x3d21: 0x0001, 0x3d22: 0x0001, 0x3d23: 0x0001,
0x3d24: 0x0001, 0x3d25: 0x0001, 0x3d26: 0x0001, 0x3d27: 0x0001, 0x3d28: 0x0001, 0x3d29: 0x0001,
0x3d2a: 0x0001, 0x3d2b: 0x0001, 0x3d2c: 0x0001, 0x3d2d: 0x0001, 0x3d2e: 0x0001, 0x3d2f: 0x0001,
0x3d30: 0x0001, 0x3d31: 0x0001, 0x3d32: 0x0001, 0x3d33: 0x0001, 0x3d34: 0x0001, 0x3d35: 0x0001,
0x3d36: 0x0001, 0x3d37: 0x0001, 0x3d38: 0x0001, 0x3d39: 0x0001, 0x3d3a: 0x0001, 0x3d3b: 0x0001,
0x3d3c: 0x0001, 0x3d3d: 0x0001, 0x3d3e: 0x0001, 0x3d3f: 0x0001,
// Block 0xf5, offset 0x3d40
0x3d41: 0x0001,
0x3d60: 0x0001, 0x3d61: 0x0001, 0x3d62: 0x0001, 0x3d63: 0x0001,
0x3d64: 0x0001, 0x3d65: 0x0001, 0x3d66: 0x0001, 0x3d67: 0x0001, 0x3d68: 0x0001, 0x3d69: 0x0001,
0x3d6a: 0x0001, 0x3d6b: 0x0001, 0x3d6c: 0x0001, 0x3d6d: 0x0001, 0x3d6e: 0x0001, 0x3d6f: 0x0001,
0x3d70: 0x0001, 0x3d71: 0x0001, 0x3d72: 0x0001, 0x3d73: 0x0001, 0x3d74: 0x0001, 0x3d75: 0x0001,
0x3d76: 0x0001, 0x3d77: 0x0001, 0x3d78: 0x0001, 0x3d79: 0x0001, 0x3d7a: 0x0001, 0x3d7b: 0x0001,
0x3d7c: 0x0001, 0x3d7d: 0x0001, 0x3d7e: 0x0001, 0x3d7f: 0x0001,
// Block 0xf6, offset 0x3d80
0x3d80: 0x0003, 0x3d81: 0x0003, 0x3d82: 0x0003, 0x3d83: 0x0003, 0x3d84: 0x0003, 0x3d85: 0x0003,
0x3d86: 0x0003, 0x3d87: 0x0003, 0x3d88: 0x0003, 0x3d89: 0x0003, 0x3d8a: 0x0003, 0x3d8b: 0x0003,
0x3d8c: 0x0003, 0x3d8d: 0x0003, 0x3d8e: 0x0003, 0x3d8f: 0x0003, 0x3d90: 0x0003, 0x3d91: 0x0003,
0x3d92: 0x0003, 0x3d93: 0x0003, 0x3d94: 0x0003, 0x3d95: 0x0003, 0x3d96: 0x0003, 0x3d97: 0x0003,
0x3d98: 0x0003, 0x3d99: 0x0003, 0x3d9a: 0x0003, 0x3d9b: 0x0003, 0x3d9c: 0x0003, 0x3d9d: 0x0003,
0x3d9e: 0x0003, 0x3d9f: 0x0003, 0x3da0: 0x0003, 0x3da1: 0x0003, 0x3da2: 0x0003, 0x3da3: 0x0003,
0x3da4: 0x0003, 0x3da5: 0x0003, 0x3da6: 0x0003, 0x3da7: 0x0003, 0x3da8: 0x0003, 0x3da9: 0x0003,
0x3daa: 0x0003, 0x3dab: 0x0003, 0x3dac: 0x0003, 0x3dad: 0x0003, 0x3dae: 0x0003, 0x3daf: 0x0003,
0x3db0: 0x0003, 0x3db1: 0x0003, 0x3db2: 0x0003, 0x3db3: 0x0003, 0x3db4: 0x0003, 0x3db5: 0x0003,
0x3db6: 0x0003, 0x3db7: 0x0003, 0x3db8: 0x0003, 0x3db9: 0x0003, 0x3dba: 0x0003, 0x3dbb: 0x0003,
0x3dbc: 0x0003, 0x3dbd: 0x0003,
0x3d40: 0x0003, 0x3d41: 0x0003, 0x3d42: 0x0003, 0x3d43: 0x0003, 0x3d44: 0x0003, 0x3d45: 0x0003,
0x3d46: 0x0003, 0x3d47: 0x0003, 0x3d48: 0x0003, 0x3d49: 0x0003, 0x3d4a: 0x0003, 0x3d4b: 0x0003,
0x3d4c: 0x0003, 0x3d4d: 0x0003, 0x3d4e: 0x0003, 0x3d4f: 0x0003, 0x3d50: 0x0003, 0x3d51: 0x0003,
0x3d52: 0x0003, 0x3d53: 0x0003, 0x3d54: 0x0003, 0x3d55: 0x0003, 0x3d56: 0x0003, 0x3d57: 0x0003,
0x3d58: 0x0003, 0x3d59: 0x0003, 0x3d5a: 0x0003, 0x3d5b: 0x0003, 0x3d5c: 0x0003, 0x3d5d: 0x0003,
0x3d5e: 0x0003, 0x3d5f: 0x0003, 0x3d60: 0x0003, 0x3d61: 0x0003, 0x3d62: 0x0003, 0x3d63: 0x0003,
0x3d64: 0x0003, 0x3d65: 0x0003, 0x3d66: 0x0003, 0x3d67: 0x0003, 0x3d68: 0x0003, 0x3d69: 0x0003,
0x3d6a: 0x0003, 0x3d6b: 0x0003, 0x3d6c: 0x0003, 0x3d6d: 0x0003, 0x3d6e: 0x0003, 0x3d6f: 0x0003,
0x3d70: 0x0003, 0x3d71: 0x0003, 0x3d72: 0x0003, 0x3d73: 0x0003, 0x3d74: 0x0003, 0x3d75: 0x0003,
0x3d76: 0x0003, 0x3d77: 0x0003, 0x3d78: 0x0003, 0x3d79: 0x0003, 0x3d7a: 0x0003, 0x3d7b: 0x0003,
0x3d7c: 0x0003, 0x3d7d: 0x0003,
}
// stringWidthIndex: 30 blocks, 1920 entries, 1920 bytes
@@ -1673,11 +1653,11 @@ var stringWidthIndex = [1920]uint8{
0x593: 0xd4,
0x5a3: 0xd5, 0x5a5: 0xd6,
// Block 0x17, offset 0x5c0
0x5c0: 0xd7, 0x5c3: 0xd8, 0x5c4: 0xd9, 0x5c5: 0xda, 0x5c6: 0xdb,
0x5c8: 0xdc, 0x5c9: 0xdd, 0x5cc: 0xde, 0x5cd: 0xdf, 0x5ce: 0xe0, 0x5cf: 0xe1,
0x5d0: 0xe2, 0x5d1: 0xe3, 0x5d2: 0xe4, 0x5d3: 0xe5, 0x5d4: 0xe6, 0x5d5: 0xe7, 0x5d6: 0xe8, 0x5d7: 0xe9,
0x5d8: 0xe4, 0x5d9: 0xea, 0x5da: 0xe4, 0x5db: 0xeb, 0x5df: 0xec,
0x5e4: 0xed, 0x5e5: 0xee, 0x5e6: 0xe4, 0x5e7: 0xe4,
0x5c0: 0xd7, 0x5c3: 0xd8, 0x5c4: 0xd9, 0x5c5: 0xda, 0x5c6: 0xdb, 0x5c7: 0xdc,
0x5c8: 0xdd, 0x5c9: 0xde, 0x5cc: 0xdf, 0x5cd: 0xe0, 0x5ce: 0xe1, 0x5cf: 0xe2,
0x5d0: 0xe3, 0x5d1: 0xe4, 0x5d2: 0x39, 0x5d3: 0xe5, 0x5d4: 0xe6, 0x5d5: 0xe7, 0x5d6: 0xe8, 0x5d7: 0xe9,
0x5d8: 0x39, 0x5d9: 0xea, 0x5da: 0x39, 0x5db: 0xeb, 0x5df: 0xec,
0x5e4: 0xed, 0x5e5: 0xee, 0x5e6: 0x39, 0x5e7: 0x39,
0x5e9: 0xef, 0x5ea: 0xf0, 0x5eb: 0xf1,
// Block 0x18, offset 0x600
0x600: 0x39, 0x601: 0x39, 0x602: 0x39, 0x603: 0x39, 0x604: 0x39, 0x605: 0x39, 0x606: 0x39, 0x607: 0x39,
@@ -1687,7 +1667,7 @@ var stringWidthIndex = [1920]uint8{
0x620: 0x39, 0x621: 0x39, 0x622: 0x39, 0x623: 0x39, 0x624: 0x39, 0x625: 0x39, 0x626: 0x39, 0x627: 0x39,
0x628: 0x39, 0x629: 0x39, 0x62a: 0x39, 0x62b: 0x39, 0x62c: 0x39, 0x62d: 0x39, 0x62e: 0x39, 0x62f: 0x39,
0x630: 0x39, 0x631: 0x39, 0x632: 0x39, 0x633: 0x39, 0x634: 0x39, 0x635: 0x39, 0x636: 0x39, 0x637: 0x39,
0x638: 0x39, 0x639: 0x39, 0x63a: 0x39, 0x63b: 0x39, 0x63c: 0x39, 0x63d: 0x39, 0x63e: 0x39, 0x63f: 0xf2,
0x638: 0x39, 0x639: 0x39, 0x63a: 0x39, 0x63b: 0x39, 0x63c: 0x39, 0x63d: 0x39, 0x63e: 0x39, 0x63f: 0xe6,
// Block 0x19, offset 0x640
0x650: 0x0b, 0x651: 0x0c, 0x653: 0x0d, 0x656: 0x0e, 0x657: 0x06,
0x658: 0x0f, 0x65a: 0x10, 0x65b: 0x11, 0x65c: 0x12, 0x65d: 0x13, 0x65e: 0x14, 0x65f: 0x15,
@@ -1696,7 +1676,7 @@ var stringWidthIndex = [1920]uint8{
0x670: 0x06, 0x671: 0x06, 0x672: 0x06, 0x673: 0x06, 0x674: 0x06, 0x675: 0x06, 0x676: 0x06, 0x677: 0x06,
0x678: 0x06, 0x679: 0x06, 0x67a: 0x06, 0x67b: 0x06, 0x67c: 0x06, 0x67d: 0x06, 0x67e: 0x06, 0x67f: 0x16,
// Block 0x1a, offset 0x680
0x680: 0xf3, 0x681: 0x08, 0x684: 0x08, 0x685: 0x08, 0x686: 0x08, 0x687: 0x09,
0x680: 0xf2, 0x681: 0x08, 0x684: 0x08, 0x685: 0x08, 0x686: 0x08, 0x687: 0x09,
// Block 0x1b, offset 0x6c0
0x6c0: 0x5b, 0x6c1: 0x5b, 0x6c2: 0x5b, 0x6c3: 0x5b, 0x6c4: 0x5b, 0x6c5: 0x5b, 0x6c6: 0x5b, 0x6c7: 0x5b,
0x6c8: 0x5b, 0x6c9: 0x5b, 0x6ca: 0x5b, 0x6cb: 0x5b, 0x6cc: 0x5b, 0x6cd: 0x5b, 0x6ce: 0x5b, 0x6cf: 0x5b,
@@ -1705,7 +1685,7 @@ var stringWidthIndex = [1920]uint8{
0x6e0: 0x5b, 0x6e1: 0x5b, 0x6e2: 0x5b, 0x6e3: 0x5b, 0x6e4: 0x5b, 0x6e5: 0x5b, 0x6e6: 0x5b, 0x6e7: 0x5b,
0x6e8: 0x5b, 0x6e9: 0x5b, 0x6ea: 0x5b, 0x6eb: 0x5b, 0x6ec: 0x5b, 0x6ed: 0x5b, 0x6ee: 0x5b, 0x6ef: 0x5b,
0x6f0: 0x5b, 0x6f1: 0x5b, 0x6f2: 0x5b, 0x6f3: 0x5b, 0x6f4: 0x5b, 0x6f5: 0x5b, 0x6f6: 0x5b, 0x6f7: 0x5b,
0x6f8: 0x5b, 0x6f9: 0x5b, 0x6fa: 0x5b, 0x6fb: 0x5b, 0x6fc: 0x5b, 0x6fd: 0x5b, 0x6fe: 0x5b, 0x6ff: 0xf4,
0x6f8: 0x5b, 0x6f9: 0x5b, 0x6fa: 0x5b, 0x6fb: 0x5b, 0x6fc: 0x5b, 0x6fd: 0x5b, 0x6fe: 0x5b, 0x6ff: 0xf3,
// Block 0x1c, offset 0x700
0x720: 0x18,
0x730: 0x09, 0x731: 0x09, 0x732: 0x09, 0x733: 0x09, 0x734: 0x09, 0x735: 0x09, 0x736: 0x09, 0x737: 0x09,

View File

@@ -34,7 +34,7 @@ func (options Options) String(s string) int {
case 0:
return 0
case 1:
return int(asciiWidths[s[0]])
return asciiWidth(s[0])
}
width := 0
@@ -60,7 +60,7 @@ func (options Options) Bytes(s []byte) int {
case 0:
return 0
case 1:
return int(asciiWidths[s[0]])
return asciiWidth(s[0])
}
width := 0
@@ -90,7 +90,7 @@ func Rune(r rune) int {
// Iterating over runes to measure width is incorrect in many cases.
func (options Options) Rune(r rune) int {
if r < utf8.RuneSelf {
return int(asciiWidths[byte(r)])
return asciiWidth(byte(r))
}
// Surrogates (U+D800-U+DFFF) are invalid UTF-8.
@@ -102,9 +102,11 @@ func (options Options) Rune(r rune) int {
n := utf8.EncodeRune(buf[:], r)
// Skip the grapheme iterator
return lookupProperties(buf[:n]).width(options)
return graphemeWidth(buf[:n], options)
}
const _Default property = 0
// graphemeWidth returns the display width of a grapheme cluster.
// The passed string must be a single grapheme cluster.
func graphemeWidth[T stringish.Interface](s T, options Options) int {
@@ -113,16 +115,39 @@ func graphemeWidth[T stringish.Interface](s T, options Options) int {
case 0:
return 0
case 1:
return int(asciiWidths[s[0]])
return asciiWidth(s[0])
}
return lookupProperties(s).width(options)
p, sz := lookup(s)
prop := property(p)
// Variation Selector 16 (VS16) requests emoji presentation
if prop != _Wide && sz > 0 && len(s) >= sz+3 {
vs := s[sz : sz+3]
if isVS16(vs) {
prop = _Wide
}
// VS15 (0x8E) requests text presentation but does not affect width,
// in my reading of Unicode TR51. Falls through to return the base
// character's property.
}
if options.EastAsianWidth && prop == _East_Asian_Ambiguous {
prop = _Wide
}
if prop > upperBound {
prop = _Default
}
return propertyWidths[prop]
}
// isRIPrefix checks if the slice matches the Regional Indicator prefix
// (F0 9F 87). It assumes len(s) >= 3.
func isRIPrefix[T stringish.Interface](s T) bool {
return s[0] == 0xF0 && s[1] == 0x9F && s[2] == 0x87
func asciiWidth(b byte) int {
if b <= 0x1F || b == 0x7F {
return 0
}
return 1
}
// isVS16 checks if the slice matches VS16 (U+FE0F) UTF-8 encoding
@@ -131,81 +156,12 @@ func isVS16[T stringish.Interface](s T) bool {
return s[0] == 0xEF && s[1] == 0xB8 && s[2] == 0x8F
}
// lookupProperties returns the properties for a grapheme.
// The passed string must be at least one byte long.
//
// Callers must handle zero and single-byte strings upstream, both as an
// optimization, and to reduce the scope of this function.
func lookupProperties[T stringish.Interface](s T) property {
l := len(s)
if s[0] < utf8.RuneSelf {
// Check for variation selector after ASCII (e.g., keycap sequences like 1⃣)
if l >= 4 {
// Subslice may help eliminate bounds checks
vs := s[1:4]
if isVS16(vs) {
// VS16 requests emoji presentation (width 2)
return _Emoji
}
// VS15 (0x8E) requests text presentation but does not affect width,
// in my reading of Unicode TR51. Falls through to _Default.
}
return asciiProperties[s[0]]
}
// Regional indicator pair (flag)
if l >= 8 {
// Subslice may help eliminate bounds checks
ri := s[:8]
// First rune
if isRIPrefix(ri[0:3]) {
b3 := ri[3]
if b3 >= 0xA6 && b3 <= 0xBF {
// Second rune
if isRIPrefix(ri[4:7]) {
b7 := ri[7]
if b7 >= 0xA6 && b7 <= 0xBF {
return _Emoji
}
}
}
}
}
p, sz := lookup(s)
// Variation Selectors
if sz > 0 && l >= sz+3 {
// Subslice may help eliminate bounds checks
vs := s[sz : sz+3]
if isVS16(vs) {
// VS16 requests emoji presentation (width 2)
return _Emoji
}
// VS15 (0x8E) requests text presentation but does not affect width,
// in my reading of Unicode TR51. Falls through to return the base
// character's property.
}
return property(p)
// propertyWidths is a jump table of sorts, instead of a switch
var propertyWidths = [4]int{
_Default: 1,
_Zero_Width: 0,
_Wide: 2,
_East_Asian_Ambiguous: 1,
}
const _Default property = 0
const boundsCheck = property(len(propertyWidths) - 1)
// width determines the display width of a character based on its properties,
// and configuration options
func (p property) width(options Options) int {
if options.EastAsianWidth && p == _East_Asian_Ambiguous {
return 2
}
// Bounds check may help the compiler eliminate its bounds check,
// and safety of course.
if p > boundsCheck {
return 1 // default width
}
return propertyWidths[p]
}
const upperBound = property(len(propertyWidths) - 1)

View File

@@ -26,6 +26,5 @@ _testmain.go
coverage.out
coverage.txt
# Exclude IDE folders
# Exclude intellij IDE folders
.idea/*
.vscode/*

View File

@@ -1,5 +1,5 @@
load("@bazel_gazelle//:def.bzl", "gazelle")
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
load("@bazel_gazelle//:def.bzl", "gazelle")
# gazelle:prefix github.com/go-resty/resty/v2
# gazelle:go_naming_convention import_alias
@@ -9,7 +9,6 @@ go_library(
name = "resty",
srcs = [
"client.go",
"digest.go",
"middleware.go",
"redirect.go",
"request.go",
@@ -19,17 +18,11 @@ go_library(
"trace.go",
"transport.go",
"transport112.go",
"transport_js.go",
"transport_other.go",
"util.go",
"util_curl.go",
],
importpath = "github.com/go-resty/resty/v2",
visibility = ["//visibility:public"],
deps = [
"//shellescape",
"@org_golang_x_net//publicsuffix:go_default_library",
],
deps = ["@org_golang_x_net//publicsuffix:go_default_library"],
)
go_test(
@@ -38,7 +31,6 @@ go_test(
"client_test.go",
"context_test.go",
"example_test.go",
"middleware_test.go",
"request_test.go",
"resty_test.go",
"retry_test.go",
@@ -46,10 +38,7 @@ go_test(
],
data = glob([".testdata/*"]),
embed = [":resty"],
deps = [
"@org_golang_x_net//proxy:go_default_library",
"@org_golang_x_time//rate:go_default_library",
],
deps = ["@org_golang_x_net//proxy:go_default_library"],
)
alias(

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015-2024 Jeevanandam M., https://myjeeva.com <jeeva@myjeeva.com>
Copyright (c) 2015-2021 Jeevanandam M., https://myjeeva.com <jeeva@myjeeva.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -4,12 +4,16 @@
<p align="center"><a href="#features">Features</a> section describes in detail about Resty capabilities</p>
</p>
<p align="center">
<p align="center"><a href="https://github.com/go-resty/resty/actions/workflows/ci.yml?query=branch%3Av2"><img src="https://github.com/go-resty/resty/actions/workflows/ci.yml/badge.svg?branch=v2" alt="Build Status"></a> <a href="https://app.codecov.io/gh/go-resty/resty/tree/v2"><img src="https://codecov.io/gh/go-resty/resty/branch/v2/graph/badge.svg" alt="Code Coverage"></a> <a href="https://goreportcard.com/report/go-resty/resty"><img src="https://goreportcard.com/badge/go-resty/resty" alt="Go Report Card"></a> <a href="https://github.com/go-resty/resty/releases/latest"><img src="https://img.shields.io/badge/version-2.17.1-blue.svg" alt="Release Version"></a> <a href="https://pkg.go.dev/github.com/go-resty/resty/v2"><img src="https://pkg.go.dev/badge/github.com/go-resty/resty" alt="GoDoc"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/go-resty/resty.svg" alt="License"></a> <a href="https://github.com/avelino/awesome-go"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go"></a></p>
<p align="center"><a href="https://github.com/go-resty/resty/actions/workflows/ci.yml?query=branch%3Amaster"><img src="https://github.com/go-resty/resty/actions/workflows/ci.yml/badge.svg" alt="Build Status"></a> <a href="https://codecov.io/gh/go-resty/resty/branch/master"><img src="https://codecov.io/gh/go-resty/resty/branch/master/graph/badge.svg" alt="Code Coverage"></a> <a href="https://goreportcard.com/report/go-resty/resty"><img src="https://goreportcard.com/badge/go-resty/resty" alt="Go Report Card"></a> <a href="https://github.com/go-resty/resty/releases/latest"><img src="https://img.shields.io/badge/version-2.7.0-blue.svg" alt="Release Version"></a> <a href="https://pkg.go.dev/github.com/go-resty/resty/v2"><img src="https://pkg.go.dev/badge/github.com/go-resty/resty" alt="GoDoc"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/go-resty/resty.svg" alt="License"></a> <a href="https://github.com/avelino/awesome-go"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go"></a></p>
</p>
<p align="center">
<h4 align="center">Resty Communication Channels</h4>
<p align="center"><a href="https://gitter.im/go_resty/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/go_resty/community.svg" alt="Chat on Gitter - Resty Community"></a> <a href="https://twitter.com/go_resty"><img src="https://img.shields.io/badge/twitter-@go__resty-55acee.svg" alt="Twitter @go_resty"></a></p>
</p>
## News
* v2.17.1 [released](https://github.com/go-resty/resty/releases/tag/v2.17.1) and tagged on Dec 15, 2025.
* v2.7.0 [released](https://github.com/go-resty/resty/releases/tag/v2.7.0) and tagged on Nov 03, 2021.
* v2.0.0 [released](https://github.com/go-resty/resty/releases/tag/v2.0.0) and tagged on Jul 16, 2019.
* v1.12.0 [released](https://github.com/go-resty/resty/releases/tag/v1.12.0) and tagged on Feb 27, 2019.
* v1.0 released and tagged on Sep 25, 2017. - Resty's first version was released on Sep 15, 2015 then it grew gradually as a very handy and helpful library. Its been a two years since first release. I'm very thankful to Resty users and its [contributors](https://github.com/go-resty/resty/graphs/contributors).
@@ -58,10 +62,9 @@
* goroutine concurrent safe
* Resty Client trace, see [Client.EnableTrace](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.EnableTrace) and [Request.EnableTrace](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.EnableTrace)
* Since v2.4.0, trace info contains a `RequestAttempt` value, and the `Request` object contains an `Attempt` attribute
* Supports on-demand CURL command generation, see [Client.EnableGenerateCurlOnDebug](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.EnableGenerateCurlOnDebug), [Request.EnableGenerateCurlOnDebug](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.EnableGenerateCurlOnDebug). It requires debug mode to be enabled.
* Debug mode - clean and informative logging presentation
* Gzip - Go does it automatically also resty has fallback handling too
* Works fine with `HTTP/2` and `HTTP/1.1`, also `HTTP/3` can be used with Resty, see this [comment](https://github.com/go-resty/resty/issues/846#issuecomment-2329696110)
* Works fine with `HTTP/2` and `HTTP/1.1`
* [Bazel support](#bazel-support)
* Easily mock Resty for testing, [for e.g.](#mocking-http-requests-using-httpmock-library)
* Well tested client library
@@ -83,8 +86,6 @@
#### Supported Go Versions
Recommended to use `go1.23` and above.
Initially Resty started supporting `go modules` since `v1.10.0` release.
Starting Resty v2 and higher versions, it fully embraces [go modules](https://github.com/golang/go/wiki/Modules) package release. It requires a Go version capable of understanding `/vN` suffixed imports:
@@ -98,6 +99,8 @@ Starting Resty v2 and higher versions, it fully embraces [go modules](https://gi
Resty author also published following projects for Go Community.
* [aah framework](https://aahframework.org) - A secure, flexible, rapid Go web framework.
* [THUMBAI](https://thumbai.app) - Go Mod Repository, Go Vanity Service and Simple Proxy Server.
* [go-model](https://github.com/jeevatkm/go-model) - Robust & Easy to use model mapper and utility methods for Go `struct`.
@@ -105,7 +108,7 @@ Resty author also published following projects for Go Community.
```bash
# Go Modules
require github.com/go-resty/resty/v2 v2.16.5
require github.com/go-resty/resty/v2 v2.7.0
```
## Usage
@@ -262,7 +265,7 @@ resp, err := client.R().
Post("https://myapp.com/login")
// POST of raw bytes for file upload. For example: upload file to Dropbox
fileBytes, _ := os.ReadFile("/Users/jeeva/mydocument.pdf")
fileBytes, _ := ioutil.ReadFile("/Users/jeeva/mydocument.pdf")
// See we are not setting content-type header, since go-resty automatically detects Content-Type for you
resp, err := client.R().
@@ -366,13 +369,13 @@ import jsoniter "github.com/json-iterator/go"
json := jsoniter.ConfigCompatibleWithStandardLibrary
client := resty.New().
SetJSONMarshaler(json.Marshal).
SetJSONUnmarshaler(json.Unmarshal)
client := resty.New()
client.JSONMarshal = json.Marshal
client.JSONUnmarshal = json.Unmarshal
// similarly user could do for XML too with -
client.SetXMLMarshaler(xml.Marshal).
SetXMLUnmarshaler(xml.Unmarshal)
client.XMLMarshal
client.XMLUnmarshal
```
### Multipart File(s) upload
@@ -380,8 +383,8 @@ client.SetXMLMarshaler(xml.Marshal).
#### Using io.Reader
```go
profileImgBytes, _ := os.ReadFile("/Users/jeeva/test-img.png")
notesBytes, _ := os.ReadFile("/Users/jeeva/text-file.txt")
profileImgBytes, _ := ioutil.ReadFile("/Users/jeeva/test-img.png")
notesBytes, _ := ioutil.ReadFile("/Users/jeeva/text-file.txt")
// Create a Resty Client
client := resty.New()
@@ -472,7 +475,7 @@ resp, err := client.R().
client := resty.New()
// Setting output directory path, If directory not exists then resty creates one!
// This is optional one, if you're planning using absolute path in
// This is optional one, if you're planning using absoule path in
// `Request.SetOutput` and can used together.
client.SetOutputDirectory("/Users/jeeva/Downloads")
@@ -553,30 +556,6 @@ client.OnError(func(req *resty.Request, err error) {
})
```
#### Generate CURL Command
>Refer: [curl_cmd_test.go](https://github.com/go-resty/resty/blob/v2/curl_cmd_test.go)
```go
// Create a Resty Client
client := resty.New()
resp, err := client.R().
SetDebug(true).
EnableGenerateCurlOnDebug(). // CURL command generated when debug mode enabled with this option
SetBody(map[string]string{"name": "Alex"}).
Post("https://httpbin.org/post")
curlCmdExecuted := resp.Request.GenerateCurlCommand()
// Explore curl command
fmt.Println("Curl Command:\n ", curlCmdExecuted+"\n")
/* Output
Curl Command:
curl -X POST -H 'Content-Type: application/json' -H 'User-Agent: go-resty/2.14.0 (https://github.com/go-resty/resty)' -d '{"name":"Alex"}' https://httpbin.org/post
*/
```
#### Redirect Policy
Resty provides few ready to use redirect policy(s) also it supports multiple policies together.
@@ -656,7 +635,7 @@ client.SetCertificates(cert1, cert2, cert3)
```go
// Custom Root certificates from string
// You can pass you certificates through env variables as strings
// You can pass you certificates throught env variables as strings
// you can add one or more root certificates, its get appended
client.SetRootCertificateFromString("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----")
client.SetRootCertificateFromString("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----")
@@ -675,7 +654,7 @@ if err != nil {
client.SetCertificates(cert1, cert2, cert3)
```
#### Proxy Settings
#### Proxy Settings - Client as well as at Request Level
Default `Go` supports Proxy via environment variable `HTTP_PROXY`. Resty provides support via `SetProxy` & `RemoveProxy`.
Choose as per your need.
@@ -721,9 +700,8 @@ client.
})
```
By default, resty will retry requests that return a non-nil error during execution.
Therefore, the above setup will result in resty retrying requests with non-nil errors up to 3 times,
with the delay increasing after each attempt.
Above setup will result in resty retrying requests returned non nil error up to
3 times with delay increased after each attempt.
You can optionally provide client with [custom retry conditions](https://pkg.go.dev/github.com/go-resty/resty/v2#RetryConditionFunc):
@@ -740,26 +718,10 @@ client.AddRetryCondition(
)
```
The above example will make resty retry requests that end with a `429 Too Many Requests` status code.
It's important to note that when you specify conditions using `AddRetryCondition`,
it will override the default retry behavior, which retries on errors encountered during the request.
If you want to retry on errors encountered during the request, similar to the default behavior,
you'll need to configure it as follows:
```go
// Create a Resty Client
client := resty.New()
client.AddRetryCondition(
func(r *resty.Response, err error) bool {
// Including "err != nil" emulates the default retry behavior for errors encountered during the request.
return err != nil || r.StatusCode() == http.StatusTooManyRequests
},
)
```
Above example will make resty retry requests ended with `429 Too Many Requests`
status code.
Multiple retry conditions can be added.
Note that if multiple conditions are specified, a retry will occur if any of the conditions are met.
It is also possible to use `resty.Backoff(...)` to get arbitrary retry scenarios
implemented. [Reference](retry_test.go).
@@ -816,7 +778,7 @@ client.SetTimeout(1 * time.Minute)
// You can override all below settings and options at request level if you want to
//--------------------------------------------------------------------------------
// Host URL for all request. So you can use relative URL in the request
client.SetBaseURL("http://httpbin.org")
client.SetHostURL("http://httpbin.org")
// Headers for all request
client.SetHeader("Accept", "application/json")
@@ -841,7 +803,7 @@ client.SetCookies(cookies)
client.SetQueryParam("user_id", "00001")
client.SetQueryParams(map[string]string{ // sample of those who use this manner
"api_key": "api-key-here",
"api_secret": "api-secret",
"api_secert": "api-secert",
})
client.R().SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more")
@@ -880,10 +842,10 @@ client := resty.New()
// Set the previous transport that we created, set the scheme of the communication to the
// socket and set the unixSocket as the HostURL.
client.SetTransport(&transport).SetScheme("http").SetBaseURL(unixSocket)
client.SetTransport(&transport).SetScheme("http").SetHostURL(unixSocket)
// No need to write the host's URL on the request, just the path.
client.R().Get("http://localhost/index.html")
client.R().Get("/index.html")
```
#### Bazel Support

View File

@@ -4,10 +4,10 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "io_bazel_rules_go",
sha256 = "80a98277ad1311dacd837f9b16db62887702e9f1d1c4c9f796d0121a46c8e184",
sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.46.0/rules_go-v0.46.0.zip",
"https://github.com/bazelbuild/rules_go/releases/download/v0.46.0/rules_go-v0.46.0.zip",
"https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
"https://github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
],
)
@@ -24,7 +24,7 @@ load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_depe
go_rules_dependencies()
go_register_toolchains(version = "1.19")
go_register_toolchains(version = "1.16")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,327 +0,0 @@
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com)
// 2023 Segev Dagan (https://github.com/segevda)
// 2024 Philipp Wolfer (https://github.com/phw)
// All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package resty
import (
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"errors"
"fmt"
"hash"
"io"
"net/http"
"strings"
)
var (
ErrDigestBadChallenge = errors.New("digest: challenge is bad")
ErrDigestCharset = errors.New("digest: unsupported charset")
ErrDigestAlgNotSupported = errors.New("digest: algorithm is not supported")
ErrDigestQopNotSupported = errors.New("digest: no supported qop in list")
ErrDigestNoQop = errors.New("digest: qop must be specified")
)
var hashFuncs = map[string]func() hash.Hash{
"": md5.New,
"MD5": md5.New,
"MD5-sess": md5.New,
"SHA-256": sha256.New,
"SHA-256-sess": sha256.New,
"SHA-512-256": sha512.New,
"SHA-512-256-sess": sha512.New,
}
type digestCredentials struct {
username, password string
}
type digestTransport struct {
digestCredentials
transport http.RoundTripper
}
func (dt *digestTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Copy the request, so we don't modify the input.
req2 := new(http.Request)
*req2 = *req
req2.Header = make(http.Header)
for k, s := range req.Header {
req2.Header[k] = s
}
// Fix http: ContentLength=xxx with Body length 0
if req2.Body == nil {
req2.ContentLength = 0
} else if req2.GetBody != nil {
var err error
req2.Body, err = req2.GetBody()
if err != nil {
return nil, err
}
}
// Make a request to get the 401 that contains the challenge.
resp, err := dt.transport.RoundTrip(req)
if err != nil || resp.StatusCode != http.StatusUnauthorized {
return resp, err
}
chal := resp.Header.Get(hdrWwwAuthenticateKey)
if chal == "" {
return resp, ErrDigestBadChallenge
}
c, err := parseChallenge(chal)
if err != nil {
return resp, err
}
// Form credentials based on the challenge
cr := dt.newCredentials(req2, c)
auth, err := cr.authorize()
if err != nil {
return resp, err
}
err = resp.Body.Close()
if err != nil {
return nil, err
}
// Make authenticated request
req2.Header.Set(hdrAuthorizationKey, auth)
return dt.transport.RoundTrip(req2)
}
func (dt *digestTransport) newCredentials(req *http.Request, c *challenge) *credentials {
return &credentials{
username: dt.username,
userhash: c.userhash,
realm: c.realm,
nonce: c.nonce,
digestURI: req.URL.RequestURI(),
algorithm: c.algorithm,
sessionAlg: strings.HasSuffix(c.algorithm, "-sess"),
opaque: c.opaque,
messageQop: c.qop,
nc: 0,
method: req.Method,
password: dt.password,
}
}
type challenge struct {
realm string
domain string
nonce string
opaque string
stale string
algorithm string
qop string
userhash string
}
func (c *challenge) setValue(k, v string) error {
switch k {
case "realm":
c.realm = v
case "domain":
c.domain = v
case "nonce":
c.nonce = v
case "opaque":
c.opaque = v
case "stale":
c.stale = v
case "algorithm":
c.algorithm = v
case "qop":
c.qop = v
case "charset":
if strings.ToUpper(v) != "UTF-8" {
return ErrDigestCharset
}
case "userhash":
c.userhash = v
default:
return ErrDigestBadChallenge
}
return nil
}
func parseChallenge(input string) (*challenge, error) {
const ws = " \n\r\t"
s := strings.Trim(input, ws)
if !strings.HasPrefix(s, "Digest ") {
return nil, ErrDigestBadChallenge
}
s = strings.Trim(s[7:], ws)
c := &challenge{}
b := strings.Builder{}
key := ""
quoted := false
for _, r := range s {
switch r {
case '"':
quoted = !quoted
case ',':
if quoted {
b.WriteRune(r)
} else {
val := strings.Trim(b.String(), ws)
b.Reset()
if err := c.setValue(key, val); err != nil {
return nil, err
}
key = ""
}
case '=':
if quoted {
b.WriteRune(r)
} else {
key = strings.Trim(b.String(), ws)
b.Reset()
}
default:
b.WriteRune(r)
}
}
if quoted || (key == "" && b.Len() > 0) {
return nil, ErrDigestBadChallenge
}
if key != "" {
val := strings.Trim(b.String(), ws)
if err := c.setValue(key, val); err != nil {
return nil, err
}
}
return c, nil
}
type credentials struct {
username string
userhash string
realm string
nonce string
digestURI string
algorithm string
sessionAlg bool
cNonce string
opaque string
messageQop string
nc int
method string
password string
}
func (c *credentials) authorize() (string, error) {
if _, ok := hashFuncs[c.algorithm]; !ok {
return "", ErrDigestAlgNotSupported
}
if err := c.validateQop(); err != nil {
return "", err
}
resp, err := c.resp()
if err != nil {
return "", err
}
sl := make([]string, 0, 10)
if c.userhash == "true" {
// RFC 7616 3.4.4
c.username = c.h(fmt.Sprintf("%s:%s", c.username, c.realm))
sl = append(sl, fmt.Sprintf(`userhash=%s`, c.userhash))
}
sl = append(sl, fmt.Sprintf(`username="%s"`, c.username))
sl = append(sl, fmt.Sprintf(`realm="%s"`, c.realm))
sl = append(sl, fmt.Sprintf(`nonce="%s"`, c.nonce))
sl = append(sl, fmt.Sprintf(`uri="%s"`, c.digestURI))
sl = append(sl, fmt.Sprintf(`response="%s"`, resp))
sl = append(sl, fmt.Sprintf(`algorithm=%s`, c.algorithm))
if c.opaque != "" {
sl = append(sl, fmt.Sprintf(`opaque="%s"`, c.opaque))
}
if c.messageQop != "" {
sl = append(sl, fmt.Sprintf("qop=%s", c.messageQop))
sl = append(sl, fmt.Sprintf("nc=%08x", c.nc))
sl = append(sl, fmt.Sprintf(`cnonce="%s"`, c.cNonce))
}
return fmt.Sprintf("Digest %s", strings.Join(sl, ", ")), nil
}
func (c *credentials) validateQop() error {
// Currently only supporting auth quality of protection. TODO: add auth-int support
// NOTE: cURL support auth-int qop for requests other than POST and PUT (i.e. w/o body) by hashing an empty string
// is this applicable for resty? see: https://github.com/curl/curl/blob/307b7543ea1e73ab04e062bdbe4b5bb409eaba3a/lib/vauth/digest.c#L774
if c.messageQop == "" {
return ErrDigestNoQop
}
possibleQops := strings.Split(c.messageQop, ",")
var authSupport bool
for _, qop := range possibleQops {
qop = strings.TrimSpace(qop)
if qop == "auth" {
authSupport = true
break
}
}
if !authSupport {
return ErrDigestQopNotSupported
}
c.messageQop = "auth"
return nil
}
func (c *credentials) h(data string) string {
hfCtor := hashFuncs[c.algorithm]
hf := hfCtor()
_, _ = hf.Write([]byte(data)) // Hash.Write never returns an error
return fmt.Sprintf("%x", hf.Sum(nil))
}
func (c *credentials) resp() (string, error) {
c.nc++
b := make([]byte, 16)
_, err := io.ReadFull(rand.Reader, b)
if err != nil {
return "", err
}
c.cNonce = fmt.Sprintf("%x", b)[:32]
ha1 := c.ha1()
ha2 := c.ha2()
return c.kd(ha1, fmt.Sprintf("%s:%08x:%s:%s:%s",
c.nonce, c.nc, c.cNonce, c.messageQop, ha2)), nil
}
func (c *credentials) kd(secret, data string) string {
return c.h(fmt.Sprintf("%s:%s", secret, data))
}
// RFC 7616 3.4.2
func (c *credentials) ha1() string {
ret := c.h(fmt.Sprintf("%s:%s:%s", c.username, c.realm, c.password))
if c.sessionAlg {
return c.h(fmt.Sprintf("%s:%s:%s", ret, c.nonce, c.cNonce))
}
return ret
}
// RFC 7616 3.4.3
func (c *credentials) ha2() string {
// currently no auth-int support
return c.h(fmt.Sprintf("%s:%s", c.method, c.digestURI))
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
@@ -9,13 +9,13 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"time"
)
@@ -27,76 +27,15 @@ const debugRequestLogKey = "__restyDebugRequestLog"
//_______________________________________________________________________
func parseRequestURL(c *Client, r *Request) error {
if l := len(c.PathParams) + len(c.RawPathParams) + len(r.PathParams) + len(r.RawPathParams); l > 0 {
params := make(map[string]string, l)
// GitHub #103 Path Params
// GitHub #103 Path Params
if len(r.PathParams) > 0 {
for p, v := range r.PathParams {
params[p] = url.PathEscape(v)
r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1)
}
}
if len(c.PathParams) > 0 {
for p, v := range c.PathParams {
if _, ok := params[p]; !ok {
params[p] = url.PathEscape(v)
}
}
// GitHub #663 Raw Path Params
for p, v := range r.RawPathParams {
if _, ok := params[p]; !ok {
params[p] = v
}
}
for p, v := range c.RawPathParams {
if _, ok := params[p]; !ok {
params[p] = v
}
}
if len(params) > 0 {
var prev int
buf := acquireBuffer()
defer releaseBuffer(buf)
// search for the next or first opened curly bracket
for curr := strings.Index(r.URL, "{"); curr == 0 || curr >= prev; curr = prev + strings.Index(r.URL[prev:], "{") {
// write everything from the previous position up to the current
if curr > prev {
buf.WriteString(r.URL[prev:curr])
}
// search for the closed curly bracket from current position
next := curr + strings.Index(r.URL[curr:], "}")
// if not found, then write the remainder and exit
if next < curr {
buf.WriteString(r.URL[curr:])
prev = len(r.URL)
break
}
// special case for {}, without parameter's name
if next == curr+1 {
buf.WriteString("{}")
} else {
// check for the replacement
key := r.URL[curr+1 : next]
value, ok := params[key]
/// keep the original string if the replacement not found
if !ok {
value = r.URL[curr : next+1]
}
buf.WriteString(value)
}
// set the previous position after the closed curly bracket
prev = next + 1
if prev >= len(r.URL) {
break
}
}
if buf.Len() > 0 {
// write remainder
if prev < len(r.URL) {
buf.WriteString(r.URL[prev:])
}
r.URL = buf.String()
}
r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1)
}
}
@@ -114,12 +53,7 @@ func parseRequestURL(c *Client, r *Request) error {
r.URL = "/" + r.URL
}
// TODO: change to use c.BaseURL only in v3.0.0
baseURL := c.BaseURL
if len(baseURL) == 0 {
baseURL = c.HostURL
}
reqURL, err = url.Parse(baseURL + r.URL)
reqURL, err = url.Parse(c.HostURL + r.URL)
if err != nil {
return err
}
@@ -131,36 +65,33 @@ func parseRequestURL(c *Client, r *Request) error {
}
// Adding Query Param
if len(c.QueryParam)+len(r.QueryParam) > 0 {
for k, v := range c.QueryParam {
// skip query parameter if it was set in request
if _, ok := r.QueryParam[k]; ok {
continue
}
r.QueryParam[k] = v[:]
}
// GitHub #123 Preserve query string order partially.
// Since not feasible in `SetQuery*` resty methods, because
// standard package `url.Encode(...)` sorts the query params
// alphabetically
if len(r.QueryParam) > 0 {
if IsStringEmpty(reqURL.RawQuery) {
reqURL.RawQuery = r.QueryParam.Encode()
} else {
reqURL.RawQuery = reqURL.RawQuery + "&" + r.QueryParam.Encode()
}
query := make(url.Values)
for k, v := range c.QueryParam {
for _, iv := range v {
query.Add(k, iv)
}
}
// GH#797 Unescape query parameters
if r.unescapeQueryParams && len(reqURL.RawQuery) > 0 {
// at this point, all errors caught up in the above operations
// so ignore the return error on query unescape; I realized
// while writing the unit test
unescapedQuery, _ := url.QueryUnescape(reqURL.RawQuery)
reqURL.RawQuery = strings.ReplaceAll(unescapedQuery, " ", "+") // otherwise request becomes bad request
for k, v := range r.QueryParam {
// remove query param from client level by key
// since overrides happens for that key in the request
query.Del(k)
for _, iv := range v {
query.Add(k, iv)
}
}
// GitHub #123 Preserve query string order partially.
// Since not feasible in `SetQuery*` resty methods, because
// standard package `url.Encode(...)` sorts the query params
// alphabetically
if len(query) > 0 {
if IsStringEmpty(reqURL.RawQuery) {
reqURL.RawQuery = query.Encode()
} else {
reqURL.RawQuery = reqURL.RawQuery + "&" + query.Encode()
}
}
r.URL = reqURL.String()
@@ -169,61 +100,71 @@ func parseRequestURL(c *Client, r *Request) error {
}
func parseRequestHeader(c *Client, r *Request) error {
for k, v := range c.Header {
if _, ok := r.Header[k]; ok {
continue
}
r.Header[k] = v[:]
hdr := make(http.Header)
for k := range c.Header {
hdr[k] = append(hdr[k], c.Header[k]...)
}
if IsStringEmpty(r.Header.Get(hdrUserAgentKey)) {
r.Header.Set(hdrUserAgentKey, hdrUserAgentValue)
for k := range r.Header {
hdr.Del(k)
hdr[k] = append(hdr[k], r.Header[k]...)
}
if ct := r.Header.Get(hdrContentTypeKey); IsStringEmpty(r.Header.Get(hdrAcceptKey)) && !IsStringEmpty(ct) && (IsJSONType(ct) || IsXMLType(ct)) {
r.Header.Set(hdrAcceptKey, r.Header.Get(hdrContentTypeKey))
if IsStringEmpty(hdr.Get(hdrUserAgentKey)) {
hdr.Set(hdrUserAgentKey, hdrUserAgentValue)
}
ct := hdr.Get(hdrContentTypeKey)
if IsStringEmpty(hdr.Get(hdrAcceptKey)) && !IsStringEmpty(ct) &&
(IsJSONType(ct) || IsXMLType(ct)) {
hdr.Set(hdrAcceptKey, hdr.Get(hdrContentTypeKey))
}
r.Header = hdr
return nil
}
func parseRequestBody(c *Client, r *Request) error {
func parseRequestBody(c *Client, r *Request) (err error) {
if isPayloadSupported(r.Method, c.AllowGetMethodPayload) {
switch {
case r.isMultiPart: // Handling Multipart
if err := handleMultipart(c, r); err != nil {
return err
// Handling Multipart
if r.isMultiPart && !(r.Method == MethodPatch) {
if err = handleMultipart(c, r); err != nil {
return
}
case len(c.FormData) > 0 || len(r.FormData) > 0: // Handling Form Data
goto CL
}
// Handling Form Data
if len(c.FormData) > 0 || len(r.FormData) > 0 {
handleFormData(c, r)
case r.Body == nil && r.bodyBuf == nil: // Handling Request body when nil body
// Go http library omits Content-Length if body is nil; use http.NoBody to force it if SetContentLength is true
r.Body = http.NoBody
fallthrough
case r.Body != nil: // Handling Request body
goto CL
}
// Handling Request body
if r.Body != nil {
handleContentType(c, r)
if err := handleRequestBody(c, r); err != nil {
return err
if err = handleRequestBody(c, r); err != nil {
return
}
}
}
CL:
// by default resty won't set content length, you can if you want to :)
if c.setContentLength || r.setContentLength {
if r.bodyBuf == nil {
r.Header.Set(hdrContentLengthKey, "0")
} else {
r.Header.Set(hdrContentLengthKey, strconv.Itoa(r.bodyBuf.Len()))
}
if (c.setContentLength || r.setContentLength) && r.bodyBuf != nil {
r.Header.Set(hdrContentLengthKey, fmt.Sprintf("%d", r.bodyBuf.Len()))
}
return nil
return
}
func createHTTPRequest(c *Client, r *Request) (err error) {
if r.bodyBuf == nil {
if reader, ok := r.Body.(io.Reader); ok && isPayloadSupported(r.Method, c.AllowGetMethodPayload) {
if reader, ok := r.Body.(io.Reader); ok {
r.RawRequest, err = http.NewRequest(r.Method, r.URL, reader)
} else if c.setContentLength || r.setContentLength {
r.RawRequest, err = http.NewRequest(r.Method, r.URL, http.NoBody)
@@ -231,9 +172,7 @@ func createHTTPRequest(c *Client, r *Request) (err error) {
r.RawRequest, err = http.NewRequest(r.Method, r.URL, nil)
}
} else {
// fix data race: must deep copy.
bodyBuf := bytes.NewBuffer(append([]byte{}, r.bodyBuf.Bytes()...))
r.RawRequest, err = http.NewRequest(r.Method, r.URL, bodyBuf)
r.RawRequest, err = http.NewRequest(r.Method, r.URL, r.bodyBuf)
}
if err != nil {
@@ -267,19 +206,17 @@ func createHTTPRequest(c *Client, r *Request) (err error) {
r.RawRequest = r.RawRequest.WithContext(r.ctx)
}
bodyCopy, err := getBodyCopy(r)
if err != nil {
return err
}
// assign get body func for the underlying raw request instance
if r.RawRequest.GetBody == nil {
bodyCopy, err := getBodyCopy(r)
if err != nil {
return err
}
r.RawRequest.GetBody = func() (io.ReadCloser, error) {
if bodyCopy != nil {
buf := bodyCopy.Bytes()
r.RawRequest.GetBody = func() (io.ReadCloser, error) {
b := bytes.NewReader(buf)
return io.NopCloser(b), nil
}
return ioutil.NopCloser(bytes.NewReader(bodyCopy.Bytes())), nil
}
return nil, nil
}
return
@@ -298,60 +235,43 @@ func addCredentials(c *Client, r *Request) error {
if !c.DisableWarn {
if isBasicAuth && !strings.HasPrefix(r.URL, "https") {
r.log.Warnf("Using Basic Auth in HTTP mode is not secure, use HTTPS")
c.log.Warnf("Using Basic Auth in HTTP mode is not secure, use HTTPS")
}
}
// Build the token Auth header
if !IsStringEmpty(r.Token) {
r.RawRequest.Header.Set(c.HeaderAuthorizationKey, strings.TrimSpace(r.AuthScheme+" "+r.Token))
// Set the Authorization Header Scheme
var authScheme string
if !IsStringEmpty(r.AuthScheme) {
authScheme = r.AuthScheme
} else if !IsStringEmpty(c.AuthScheme) {
authScheme = c.AuthScheme
} else {
authScheme = "Bearer"
}
// Build the Token Auth header
if !IsStringEmpty(r.Token) { // takes precedence
r.RawRequest.Header.Set(c.HeaderAuthorizationKey, authScheme+" "+r.Token)
} else if !IsStringEmpty(c.Token) {
r.RawRequest.Header.Set(c.HeaderAuthorizationKey, strings.TrimSpace(r.AuthScheme+" "+c.Token))
}
return nil
}
func createCurlCmd(c *Client, r *Request) (err error) {
if r.Debug && r.generateCurlOnDebug {
if r.resultCurlCmd == nil {
r.resultCurlCmd = new(string)
}
*r.resultCurlCmd = buildCurlRequest(r.RawRequest, c.httpClient.Jar)
r.RawRequest.Header.Set(c.HeaderAuthorizationKey, authScheme+" "+c.Token)
}
return nil
}
func requestLogger(c *Client, r *Request) error {
if r.Debug {
if c.Debug {
rr := r.RawRequest
rh := copyHeaders(rr.Header)
if c.GetClient().Jar != nil {
for _, cookie := range c.GetClient().Jar.Cookies(r.RawRequest.URL) {
s := fmt.Sprintf("%s=%s", cookie.Name, cookie.Value)
if c := rh.Get("Cookie"); c != "" {
rh.Set("Cookie", c+"; "+s)
} else {
rh.Set("Cookie", s)
}
}
}
rl := &RequestLog{Header: rh, Body: r.fmtBodyString(c.debugBodySizeLimit)}
rl := &RequestLog{Header: copyHeaders(rr.Header), Body: r.fmtBodyString(c.debugBodySizeLimit)}
if c.requestLog != nil {
if err := c.requestLog(rl); err != nil {
return err
}
}
// fmt.Sprintf("COOKIES:\n%s\n", composeCookies(c.GetClient().Jar, *rr.URL)) +
reqLog := "\n==============================================================================\n"
if r.Debug && r.generateCurlOnDebug {
reqLog += "~~~ REQUEST(CURL) ~~~\n" +
fmt.Sprintf(" %v\n", *r.resultCurlCmd)
}
reqLog += "~~~ REQUEST ~~~\n" +
reqLog := "\n==============================================================================\n" +
"~~~ REQUEST ~~~\n" +
fmt.Sprintf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) +
fmt.Sprintf("HOST : %s\n", rr.URL.Host) +
fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(c, r, rl.Header)) +
@@ -370,7 +290,7 @@ func requestLogger(c *Client, r *Request) error {
//_______________________________________________________________________
func responseLogger(c *Client, res *Response) error {
if res.Request.Debug {
if c.Debug {
rl := &ResponseLog{Header: copyHeaders(res.Header()), Body: res.fmtBodyString(c.debugBodySizeLimit)}
if c.responseLog != nil {
if err := c.responseLog(rl); err != nil {
@@ -381,7 +301,7 @@ func responseLogger(c *Client, res *Response) error {
debugLog := res.Request.values[debugRequestLogKey].(string)
debugLog += "~~~ RESPONSE ~~~\n" +
fmt.Sprintf("STATUS : %s\n", res.Status()) +
fmt.Sprintf("PROTO : %s\n", res.Proto()) +
fmt.Sprintf("PROTO : %s\n", res.RawResponse.Proto) +
fmt.Sprintf("RECEIVED AT : %v\n", res.ReceivedAt().Format(time.RFC3339Nano)) +
fmt.Sprintf("TIME DURATION: %v\n", res.Time()) +
"HEADERS :\n" +
@@ -393,7 +313,7 @@ func responseLogger(c *Client, res *Response) error {
}
debugLog += "==============================================================================\n"
res.Request.log.Debugf("%s", debugLog)
c.log.Debugf("%s", debugLog)
}
return nil
@@ -401,7 +321,6 @@ func responseLogger(c *Client, res *Response) error {
func parseResponseBody(c *Client, res *Response) (err error) {
if res.StatusCode() == http.StatusNoContent {
res.Request.Error = nil
return
}
// Handles only JSON or XML content type
@@ -424,10 +343,7 @@ func parseResponseBody(c *Client, res *Response) (err error) {
}
if res.Request.Error != nil {
unmarshalErr := Unmarshalc(c, ct, res.body, res.Request.Error)
if unmarshalErr != nil {
c.log.Warnf("Cannot unmarshal response body: %s", unmarshalErr)
}
err = Unmarshalc(c, ct, res.body, res.Request.Error)
}
}
}
@@ -435,20 +351,13 @@ func parseResponseBody(c *Client, res *Response) (err error) {
return
}
func handleMultipart(c *Client, r *Request) error {
func handleMultipart(c *Client, r *Request) (err error) {
r.bodyBuf = acquireBuffer()
w := multipart.NewWriter(r.bodyBuf)
// Set boundary if not set by user
if r.multipartBoundary != "" {
if err := w.SetBoundary(r.multipartBoundary); err != nil {
return err
}
}
for k, v := range c.FormData {
for _, iv := range v {
if err := w.WriteField(k, iv); err != nil {
if err = w.WriteField(k, iv); err != nil {
return err
}
}
@@ -457,11 +366,12 @@ func handleMultipart(c *Client, r *Request) error {
for k, v := range r.FormData {
for _, iv := range v {
if strings.HasPrefix(k, "@") { // file
if err := addFile(w, k[1:], iv); err != nil {
return err
err = addFile(w, k[1:], iv)
if err != nil {
return
}
} else { // form value
if err := w.WriteField(k, iv); err != nil {
if err = w.WriteField(k, iv); err != nil {
return err
}
}
@@ -469,41 +379,55 @@ func handleMultipart(c *Client, r *Request) error {
}
// #21 - adding io.Reader support
for _, f := range r.multipartFiles {
if err := addFileReader(w, f); err != nil {
return err
if len(r.multipartFiles) > 0 {
for _, f := range r.multipartFiles {
err = addFileReader(w, f)
if err != nil {
return
}
}
}
// GitHub #130 adding multipart field support with content type
for _, mf := range r.multipartFields {
if err := addMultipartFormField(w, mf); err != nil {
return err
if len(r.multipartFields) > 0 {
for _, mf := range r.multipartFields {
if err = addMultipartFormField(w, mf); err != nil {
return
}
}
}
r.Header.Set(hdrContentTypeKey, w.FormDataContentType())
return w.Close()
err = w.Close()
return
}
func handleFormData(c *Client, r *Request) {
formData := url.Values{}
for k, v := range c.FormData {
if _, ok := r.FormData[k]; ok {
continue
for _, iv := range v {
formData.Add(k, iv)
}
r.FormData[k] = v[:]
}
r.bodyBuf = acquireBuffer()
r.bodyBuf.WriteString(r.FormData.Encode())
for k, v := range r.FormData {
// remove form data field from client level by key
// since overrides happens for that key in the request
formData.Del(k)
for _, iv := range v {
formData.Add(k, iv)
}
}
r.bodyBuf = bytes.NewBuffer([]byte(formData.Encode()))
r.Header.Set(hdrContentTypeKey, formContentType)
r.isFormData = true
}
func handleContentType(c *Client, r *Request) {
if r.Body == http.NoBody {
return
}
contentType := r.Header.Get(hdrContentTypeKey)
if IsStringEmpty(contentType) {
contentType = DetectContentType(r.Body)
@@ -511,42 +435,45 @@ func handleContentType(c *Client, r *Request) {
}
}
func handleRequestBody(c *Client, r *Request) error {
func handleRequestBody(c *Client, r *Request) (err error) {
var bodyBytes []byte
contentType := r.Header.Get(hdrContentTypeKey)
kind := kindOf(r.Body)
r.bodyBuf = nil
switch body := r.Body.(type) {
case io.Reader:
if reader, ok := r.Body.(io.Reader); ok {
if c.setContentLength || r.setContentLength { // keep backward compatibility
r.bodyBuf = acquireBuffer()
if _, err := r.bodyBuf.ReadFrom(body); err != nil {
return err
}
_, err = r.bodyBuf.ReadFrom(reader)
r.Body = nil
} else {
// Otherwise buffer less processing for `io.Reader`, sounds good.
return nil
}
case []byte:
bodyBytes = body
case string:
bodyBytes = []byte(body)
default:
contentType := r.Header.Get(hdrContentTypeKey)
kind := kindOf(r.Body)
var err error
if IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
r.bodyBuf, err = jsonMarshal(c, r, r.Body)
} else if IsXMLType(contentType) && (kind == reflect.Struct) {
bodyBytes, err = c.XMLMarshal(r.Body)
return
}
} else if b, ok := r.Body.([]byte); ok {
bodyBytes = b
} else if s, ok := r.Body.(string); ok {
bodyBytes = []byte(s)
} else if IsJSONType(contentType) &&
(kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
r.bodyBuf, err = jsonMarshal(c, r, r.Body)
if err != nil {
return err
return
}
} else if IsXMLType(contentType) && (kind == reflect.Struct) {
bodyBytes, err = c.XMLMarshal(r.Body)
if err != nil {
return
}
}
if bodyBytes == nil && r.bodyBuf == nil {
return errors.New("unsupported 'Body' type/value")
err = errors.New("unsupported 'Body' type/value")
}
// if any errors during body bytes handling, return it
if err != nil {
return
}
// []byte into Buffer
@@ -555,7 +482,7 @@ func handleRequestBody(c *Client, r *Request) error {
_, _ = r.bodyBuf.Write(bodyBytes)
}
return nil
return
}
func saveResponseIntoFile(c *Client, res *Response) error {
@@ -594,25 +521,20 @@ func saveResponseIntoFile(c *Client, res *Response) error {
func getBodyCopy(r *Request) (*bytes.Buffer, error) {
// If r.bodyBuf present, return the copy
if r.bodyBuf != nil {
bodyCopy := acquireBuffer()
if _, err := io.Copy(bodyCopy, bytes.NewReader(r.bodyBuf.Bytes())); err != nil {
// cannot use io.Copy(bodyCopy, r.bodyBuf) because io.Copy reset r.bodyBuf
return nil, err
}
return bodyCopy, nil
return bytes.NewBuffer(r.bodyBuf.Bytes()), nil
}
// Maybe body is `io.Reader`.
// Note: Resty user have to watchout for large body size of `io.Reader`
if r.RawRequest.Body != nil {
b, err := io.ReadAll(r.RawRequest.Body)
b, err := ioutil.ReadAll(r.RawRequest.Body)
if err != nil {
return nil, err
}
// Restore the Body
closeq(r.RawRequest.Body)
r.RawRequest.Body = io.NopCloser(bytes.NewBuffer(b))
r.RawRequest.Body = ioutil.NopCloser(bytes.NewBuffer(b))
// Return the Body bytes
return bytes.NewBuffer(b), nil

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
@@ -12,23 +12,18 @@ import (
"strings"
)
var (
ErrAutoRedirectDisabled = errors.New("auto redirect is disabled")
)
type (
// RedirectPolicy to regulate the redirects in the Resty client.
// Objects implementing the [RedirectPolicy] interface can be registered as
// RedirectPolicy to regulate the redirects in the resty client.
// Objects implementing the RedirectPolicy interface can be registered as
//
// Apply function should return nil to continue the redirect journey; otherwise
// Apply function should return nil to continue the redirect jounery, otherwise
// return error to stop the redirect.
RedirectPolicy interface {
Apply(req *http.Request, via []*http.Request) error
}
// The [RedirectPolicyFunc] type is an adapter to allow the use of ordinary
// functions as [RedirectPolicy]. If `f` is a function with the appropriate
// signature, RedirectPolicyFunc(f) is a RedirectPolicy object that calls `f`.
// The RedirectPolicyFunc type is an adapter to allow the use of ordinary functions as RedirectPolicy.
// If f is a function with the appropriate signature, RedirectPolicyFunc(f) is a RedirectPolicy object that calls f.
RedirectPolicyFunc func(*http.Request, []*http.Request) error
)
@@ -37,18 +32,16 @@ func (f RedirectPolicyFunc) Apply(req *http.Request, via []*http.Request) error
return f(req, via)
}
// NoRedirectPolicy is used to disable redirects in the Resty client
//
// resty.SetRedirectPolicy(NoRedirectPolicy())
// NoRedirectPolicy is used to disable redirects in the HTTP client
// resty.SetRedirectPolicy(NoRedirectPolicy())
func NoRedirectPolicy() RedirectPolicy {
return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
return ErrAutoRedirectDisabled
return errors.New("auto redirect is disabled")
})
}
// FlexibleRedirectPolicy method is convenient for creating several redirect policies for Resty clients.
//
// resty.SetRedirectPolicy(FlexibleRedirectPolicy(20))
// FlexibleRedirectPolicy is convenient method to create No of redirect policy for HTTP client.
// resty.SetRedirectPolicy(FlexibleRedirectPolicy(20))
func FlexibleRedirectPolicy(noOfRedirect int) RedirectPolicy {
return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
if len(via) >= noOfRedirect {
@@ -59,10 +52,9 @@ func FlexibleRedirectPolicy(noOfRedirect int) RedirectPolicy {
})
}
// DomainCheckRedirectPolicy method is convenient for defining domain name redirect rules in Resty clients.
// Redirect is allowed only for the host mentioned in the policy.
//
// resty.SetRedirectPolicy(DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net"))
// DomainCheckRedirectPolicy is convenient method to define domain name redirect rule in resty client.
// Redirect is allowed for only mentioned host in the policy.
// resty.SetRedirectPolicy(DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net"))
func DomainCheckRedirectPolicy(hostnames ...string) RedirectPolicy {
hosts := make(map[string]bool)
for _, h := range hostnames {
@@ -80,6 +72,10 @@ func DomainCheckRedirectPolicy(hostnames ...string) RedirectPolicy {
return fn
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Package Unexported methods
//_______________________________________________________________________
func getHostname(host string) (hostname string) {
if strings.Index(host, ":") > 0 {
host, _, _ = net.SplitHostPort(host)
@@ -88,11 +84,10 @@ func getHostname(host string) (hostname string) {
return
}
// By default, Golang will not redirect request headers.
// After reading through the various discussion comments from the thread -
// By default Golang will not redirect request headers
// after go throughing various discussion comments from thread
// https://github.com/golang/go/issues/4800
// Resty will add all the headers during a redirect for the same host and
// adds library user-agent if the Host is different.
// Resty will add all the headers during a redirect for the same host
func checkHostAndAddHeaders(cur *http.Request, pre *http.Request) {
curHostname := getHostname(cur.URL.Host)
preHostname := getHostname(pre.URL.Host)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
@@ -17,7 +17,7 @@ import (
// Response struct and methods
//_______________________________________________________________________
// Response struct holds response values of executed requests.
// Response struct holds response values of executed request.
type Response struct {
Request *Request
RawResponse *http.Response
@@ -27,10 +27,9 @@ type Response struct {
receivedAt time.Time
}
// Body method returns the HTTP response as `[]byte` slice for the executed request.
// Body method returns HTTP response as []byte array for the executed request.
//
// NOTE: [Response.Body] might be nil if [Request.SetOutput] is used.
// Also see [Request.SetDoNotParseResponse], [Client.SetDoNotParseResponse]
// Note: `Response.Body` might be nil, if `Request.SetOutput` is used.
func (r *Response) Body() []byte {
if r.RawResponse == nil {
return []byte{}
@@ -38,18 +37,7 @@ func (r *Response) Body() []byte {
return r.body
}
// SetBody method sets [Response] body in byte slice. Typically,
// It is helpful for test cases.
//
// resp.SetBody([]byte("This is test body content"))
// resp.SetBody(nil)
func (r *Response) SetBody(b []byte) *Response {
r.body = b
return r
}
// Status method returns the HTTP status string for the executed request.
//
// Example: 200 OK
func (r *Response) Status() string {
if r.RawResponse == nil {
@@ -59,7 +47,6 @@ func (r *Response) Status() string {
}
// StatusCode method returns the HTTP status code for the executed request.
//
// Example: 200
func (r *Response) StatusCode() int {
if r.RawResponse == nil {
@@ -77,15 +64,11 @@ func (r *Response) Proto() string {
}
// Result method returns the response value as an object if it has one
//
// See [Request.SetResult]
func (r *Response) Result() interface{} {
return r.Request.Result
}
// Error method returns the error object if it has one
//
// See [Request.SetError], [Client.SetError]
func (r *Response) Error() interface{} {
return r.Request.Error
}
@@ -98,7 +81,7 @@ func (r *Response) Header() http.Header {
return r.RawResponse.Header
}
// Cookies method to returns all the response cookies
// Cookies method to access all the response cookies
func (r *Response) Cookies() []*http.Cookie {
if r.RawResponse == nil {
return make([]*http.Cookie, 0)
@@ -106,20 +89,18 @@ func (r *Response) Cookies() []*http.Cookie {
return r.RawResponse.Cookies()
}
// String method returns the body of the HTTP response as a `string`.
// It returns an empty string if it is nil or the body is zero length.
// String method returns the body of the server response as String.
func (r *Response) String() string {
if len(r.body) == 0 {
if r.body == nil {
return ""
}
return strings.TrimSpace(string(r.body))
}
// Time method returns the duration of HTTP response time from the request we sent
// and received a request.
// Time method returns the time of HTTP response time that from request we sent and received a request.
//
// See [Response.ReceivedAt] to know when the client received a response and see
// `Response.Request.Time` to know when the client sent a request.
// See `Response.ReceivedAt` to know when client received response and see `Response.Request.Time` to know
// when client sent a request.
func (r *Response) Time() time.Duration {
if r.Request.clientTrace != nil {
return r.Request.TraceInfo().TotalTime
@@ -127,25 +108,23 @@ func (r *Response) Time() time.Duration {
return r.receivedAt.Sub(r.Request.Time)
}
// ReceivedAt method returns the time we received a response from the server for the request.
// ReceivedAt method returns when response got received from server for the request.
func (r *Response) ReceivedAt() time.Time {
return r.receivedAt
}
// Size method returns the HTTP response size in bytes. Yeah, you can rely on HTTP `Content-Length`
// header, however it won't be available for chucked transfer/compressed response.
// Since Resty captures response size details when processing the response body
// when possible. So that users get the actual size of response bytes.
// Size method returns the HTTP response size in bytes. Ya, you can relay on HTTP `Content-Length` header,
// however it won't be good for chucked transfer/compressed response. Since Resty calculates response size
// at the client end. You will get actual size of the http response.
func (r *Response) Size() int64 {
return r.size
}
// RawBody method exposes the HTTP raw response body. Use this method in conjunction with
// [Client.SetDoNotParseResponse] or [Request.SetDoNotParseResponse]
// option; otherwise, you get an error as `read err: http: read on closed response body.`
// RawBody method exposes the HTTP raw response body. Use this method in-conjunction with `SetDoNotParseResponse`
// option otherwise you get an error as `read err: http: read on closed response body`.
//
// Do not forget to close the body, otherwise you might get into connection leaks, no connection reuse.
// You have taken over the control of response parsing from Resty.
// Basically you have taken over the control of response parsing from `Resty`.
func (r *Response) RawBody() io.ReadCloser {
if r.RawResponse == nil {
return nil
@@ -163,6 +142,10 @@ func (r *Response) IsError() bool {
return r.StatusCode() > 399
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Response Unexported methods
//_______________________________________________________________________
func (r *Response) setReceivedAt() {
r.receivedAt = time.Now()
if r.Request.clientTrace != nil {
@@ -171,10 +154,7 @@ func (r *Response) setReceivedAt() {
}
func (r *Response) fmtBodyString(sl int64) string {
if r.Request.client.notParseResponse || r.Request.notParseResponse {
return "***** DO NOT PARSE RESPONSE - Enabled *****"
}
if len(r.body) > 0 {
if r.body != nil {
if int64(len(r.body)) > sl {
return fmt.Sprintf("***** RESPONSE TOO LARGE (size - %d) *****", len(r.body))
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
@@ -14,7 +14,7 @@ import (
)
// Version # of resty
const Version = "2.17.1"
const Version = "2.7.0"
// New method creates a new Resty client.
func New() *Client {
@@ -24,12 +24,12 @@ func New() *Client {
})
}
// NewWithClient method creates a new Resty client with given [http.Client].
// NewWithClient method creates a new Resty client with given `http.Client`.
func NewWithClient(hc *http.Client) *Client {
return createClient(hc)
}
// NewWithLocalAddr method creates a new Resty client with the given Local Address.
// NewWithLocalAddr method creates a new Resty client with given Local Address
// to dial from.
func NewWithLocalAddr(localAddr net.Addr) *Client {
cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})

Some files were not shown because too many files have changed in this diff Show More