pax_global_header00006660000000000000000000000064150543742600014520gustar00rootroot0000000000000052 comment=30773bba0e076d6b8438a3e9cec72a15f7b18f26 safecast-1.2.0/000077500000000000000000000000001505437426000133115ustar00rootroot00000000000000safecast-1.2.0/.github/000077500000000000000000000000001505437426000146515ustar00rootroot00000000000000safecast-1.2.0/.github/dependabot.yml000066400000000000000000000011501505437426000174760ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "gomod" directory: "/" # Location of package manifests schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every week interval: "weekly" safecast-1.2.0/.github/workflows/000077500000000000000000000000001505437426000167065ustar00rootroot00000000000000safecast-1.2.0/.github/workflows/include.yml000066400000000000000000000010151505437426000210510ustar00rootroot00000000000000# Same as full workflow (eg from fortio/multicurl) but without the goreleaser step name: "CI Checks" on: push: branches: [ main ] pull_request: branches: [ main ] jobs: call-gochecks: uses: fortio/workflows/.github/workflows/gochecks.yml@main call-codecov: uses: fortio/workflows/.github/workflows/codecov.yml@main secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} call-codeql: uses: fortio/workflows/.github/workflows/codeql-analysis.yml@main safecast-1.2.0/.gitignore000066400000000000000000000011361505437426000153020ustar00rootroot00000000000000# If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work go.work.sum # env file .env # Apple stuff .DS_Store # Local linter run .golangci.yml # Grol save state .gr # vscode tasks.json safecast-1.2.0/LICENSE000066400000000000000000000261351505437426000143250ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. safecast-1.2.0/README.md000066400000000000000000000041031505437426000145660ustar00rootroot00000000000000# safecast [![Go Report Card](https://goreportcard.com/badge/fortio.org/safecast)](https://goreportcard.com/report/fortio.org/safecast) [![GoDoc](https://godoc.org/fortio.org/safecast?status.svg)](https://pkg.go.dev/fortio.org/safecast) [![codecov](https://codecov.io/gh/fortio/safecast/branch/main/graph/badge.svg)](https://codecov.io/gh/fortio/safecast) [![Maintainability](https://api.codeclimate.com/v1/badges/bf83c496d49b169cd744/maintainability)](https://codeclimate.com/github/fortio/safecast/maintainability) [![CI Checks](https://github.com/fortio/safecast/actions/workflows/include.yml/badge.svg)](https://github.com/fortio/safecast/actions/workflows/include.yml) Avoid accidental overflow of numbers during go type conversions (e.g instead of `shorter := bigger.(int8)` type conversions use `shorter := safecast.MustConv[int8](bigger)`. Safecast allows you to safely convert between numeric types in Go and return errors (or panic when using the `Must*` variants) when the cast would result in a loss of precision, range or sign. See https://pkg.go.dev/fortio.org/safecast for docs and example. This is usable from any go with generics (1.18 or later) though our CI uses the latest go. `safecast` is about avoiding [gosec G115](https://github.com/securego/gosec#available-rules) and [CWE-190: Integer Overflow or Wraparound](https://cwe.mitre.org/data/definitions/190.html) class of overflow and loss of precision bugs, extended to float64/float32 issues. Credit for the idea (and a finding a bug in the first implementation) goes to [@ccoVeille](https://github.com/ccoVeille), Please see https://github.com/ccoVeille/go-safecast for an different style API and implementation to pick whichever fits your style best (though I believe this implementation and API is better both in simplicity of principle (round trip check) and performance and api surface). Please note that conversions from integer to float are suffering from CPU architecture differences and issues at the "edge" (max int) which are handled by Convert but you should use the new int only Conv/MustConv if possible, to avoid them. safecast-1.2.0/SECURITY.md000066400000000000000000000003101505437426000150740ustar00rootroot00000000000000# Security Policy ## Supported Versions Latest tag is the only supported version. ## Reporting a Vulnerability Please report any issue or bug through https://github.com/fortio/safecast/issues/new safecast-1.2.0/go.mod000066400000000000000000000001641505437426000144200ustar00rootroot00000000000000module fortio.org/safecast go 1.20 // Really 1.18 or higher, just need generics. do use a supported version of go. safecast-1.2.0/safecast.go000066400000000000000000000114741505437426000154400ustar00rootroot00000000000000// Package safecast allows you to safely cast between numeric types in Go and return errors (or panic when using the // Must* variants) when the cast would result in a loss of precision, range or sign. package safecast // Implementation: me (@ldemailly), idea: @ccoVeille - https://github.com/ccoVeille/go-safecast // Started https://github.com/ldemailly/go-scratch/commits/main/safecast/safecast.go // then moved here to fortio.org/safecast import ( "errors" "fmt" "math" "reflect" "unsafe" ) type Integer interface { // Same as golang.org/x/contraints.Integer but without importing the whole thing for 1 line. ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~uintptr } type Float interface { ~float32 | ~float64 // Consider removing the ~ because of +Inf issue. } type Number interface { Integer | Float } var ErrOutOfRange = errors.New("out of range") const ( all63bits = uint64(math.MaxInt64) all31bits = uint64(math.MaxInt32) ) func isFloat[Num Number](f Num) (isFloat bool) { switch reflect.TypeOf(f).Kind() { //nolint:exhaustive // only 2 we want to check case reflect.Float32, reflect.Float64: isFloat = true } return } // Convert converts a number from one type to another, // returning an error if the conversion would result in a loss of precision, // range or sign (overflow). In other words if the converted number is not // equal to the original number. // Use [Conv] instead if both of your types are Integer. // Do not use for identity (same type in and out) but in particular this // will error for Convert[uint64](uint64(math.MaxUint64)) or // Convert[int64](int64(math.MaxInt64)) because it needs to // when converting to any float. Note that +Inf will convert correctly (as in error // only if going to an integer type) from a float64/float32 and not a ~float (it will // error from say ~float32 to float64 while it shouldn't). func Convert[NumOut Number, NumIn Number](orig NumIn) (converted NumOut, err error) { origPositive := (orig >= 0) // All bits set on uint64 or positive int63 are two of 4 special cases not detected by roundtrip (afaik). if origPositive && (uint64(orig)&all63bits == all63bits) && !isFloat(orig) { err = ErrOutOfRange return } converted = NumOut(orig) if origPositive != (converted >= 0) { err = ErrOutOfRange return } if NumIn(converted) != orig && ((converted == converted) || (orig == orig)) { //nolint:gocritic // NaN check err = ErrOutOfRange return } // And this are the other 2 weird case, maxint32 and maxuint32 (on armhf) conversion to float32. if origPositive && (uint64(orig)&all31bits == all31bits) && unsafe.Sizeof(converted) == 4 && !isFloat(orig) { err = ErrOutOfRange } return } // Conv is an integer only and simpler version of [Convert] without the // weird issue with round trip to floating point. // Unlike [Convert] it can be used for identity (same type in and out) even if that's // of dubious value. func Conv[NumOut Integer, NumIn Integer](orig NumIn) (converted NumOut, err error) { origPositive := (orig >= 0) converted = NumOut(orig) if origPositive != (converted >= 0) { err = ErrOutOfRange return } if NumIn(converted) != orig { err = ErrOutOfRange } return } // Same as Convert but panics if there is an error. func MustConvert[NumOut Number, NumIn Number](orig NumIn) NumOut { converted, err := Convert[NumOut](orig) if err != nil { doPanic(err, orig, converted) } return converted } // MustConv is as [Conv] but panics if there is an error. func MustConv[NumOut Integer, NumIn Integer](orig NumIn) NumOut { converted, err := Conv[NumOut](orig) if err != nil { doPanic(err, orig, converted) } return converted } // Converts a float to an integer by truncating the fractional part. // Returns an error if the conversion would result in a loss of precision. func Truncate[NumOut Number, NumIn Float](orig NumIn) (converted NumOut, err error) { return Convert[NumOut](math.Trunc(float64(orig))) } // Converts a float to an integer by rounding to the nearest integer. // Returns an error if the conversion would result in a loss of precision. func Round[NumOut Number, NumIn Float](orig NumIn) (converted NumOut, err error) { return Convert[NumOut](math.Round(float64(orig))) } // Same as Truncate but panics if there is an error. func MustTruncate[NumOut Number, NumIn Float](orig NumIn) NumOut { converted, err := Truncate[NumOut, NumIn](orig) if err != nil { doPanic(err, orig, converted) } return converted } // Same as Round but panics if there is an error. func MustRound[NumOut Number, NumIn Float](orig NumIn) NumOut { converted, err := Round[NumOut, NumIn](orig) if err != nil { doPanic(err, orig, converted) } return converted } func doPanic[NumOut Number, NumIn Number](err error, orig NumIn, converted NumOut) { panic(fmt.Sprintf("safecast: %v for %v (%T) to %T", err, orig, orig, converted)) } safecast-1.2.0/safecast_test.go000066400000000000000000000260371505437426000165000ustar00rootroot00000000000000package safecast_test import ( "fmt" "math" "testing" "fortio.org/safecast" ) // TODO: steal the tests from https://github.com/ccoVeille/go-safecast const all64bitsOne = ^uint64(0) // Interesting part is the "true" for the first line, which is why we have to change the // code in Convert to handle that 1 special case. // safecast_test.go:22: bits 64: 1111111111111111111111111111111111111111111111111111111111111111 // : 18446744073709551615 -> float64 18446744073709551616 true. func FindNumIntBits[T safecast.Float](t *testing.T) int { var v T for i := 0; i < 64; i++ { bits := (all64bitsOne >> i) v = T(bits) t.Logf("bits %02d: %b : %d -> %T %.0f %t", 64-i, bits, bits, v, v, uint64(v) == bits) if v != v-1 { return 64 - i } } panic("bug... didn't find num bits") } // https://en.wikipedia.org/wiki/Double-precision_floating-point_format const expectedFloat64Bits = 53 // https://en.wikipedia.org/wiki/Single-precision_floating-point_format#IEEE_754_standard:_binary32 const expectedFloat32Bits = 24 func TestFloat32Bounds(t *testing.T) { float32bits := FindNumIntBits[float32](t) t.Logf("float32: %d bits", float32bits) if float32bits != expectedFloat32Bits { t.Errorf("unexpected number of bits: %d", float32bits) } float32int := uint64(1<<(float32bits) - 1) // 24 bits for i := 0; i <= 64-float32bits; i++ { t.Logf("float32int %b %d", float32int, float32int) f := safecast.MustConvert[float32](float32int) t.Logf("float32int -> %.0f", f) float32int <<= 1 } } func TestFloat64Bounds(t *testing.T) { float64bits := FindNumIntBits[float64](t) t.Logf("float64: %d bits", float64bits) float64int := uint64(1<<(float64bits) - 1) // 53 bits if float64bits != expectedFloat64Bits { t.Errorf("unexpected number of bits: %d", float64bits) } for i := 0; i <= 64-float64bits; i++ { t.Logf("float64int %b %d", float64int, float64int) f := safecast.MustConvert[float64](float64int) t.Logf("float64int -> %.0f", f) float64int <<= 1 } } func TestNonIntegerFloat(t *testing.T) { _, err := safecast.Convert[int](math.Pi) if err == nil { t.Errorf("expected error") } truncPi := math.Trunc(math.Pi) // math.Trunc returns a float64 i, err := safecast.Convert[int](truncPi) if err != nil { t.Errorf("unexpected error: %v", err) } if i != 3 { t.Errorf("unexpected value: %v", i) } i, err = safecast.Truncate[int](math.Pi) if err != nil { t.Errorf("unexpected error: %v", err) } if i != 3 { t.Errorf("unexpected value: %v", i) } i, err = safecast.Round[int](math.Phi) if err != nil { t.Errorf("unexpected error: %v", err) } if i != 2 { t.Errorf("unexpected value: %v", i) } } // MaxUint64 special case and also MinInt64+1. func TestMaxUint64(t *testing.T) { f32, err := safecast.Convert[float32](all64bitsOne) if err == nil { t.Errorf("expected error, got %d -> %.0f", all64bitsOne, f32) } f64, err := safecast.Convert[float64](all64bitsOne) if err == nil { t.Errorf("expected error, got %d -> %.0f", all64bitsOne, f64) } minInt64p1 := int64(math.MinInt64) + 1 // not a power of 2 t.Logf("minInt64p1 %b %d", minInt64p1, minInt64p1) _, err = safecast.Convert[float64](minInt64p1) f64 = float64(minInt64p1) int2 := int64(f64) t.Logf("minInt64p1 -> %.0f %d", f64, int2) if err == nil { t.Errorf("expected error, got %d -> %.0f", minInt64p1, f64) } } // TestMaxInt64 special test. func TestMaxInt64(t *testing.T) { mi64 := int64(math.MaxInt64) f32, err := safecast.Convert[float32](mi64) if err == nil { t.Errorf("expected error, got %d -> %.0f", mi64, f32) } f64, err := safecast.Convert[float64](mi64) if err == nil { t.Errorf("expected error, got %d -> %.0f", mi64, f64) } } func TestMaxInt32(t *testing.T) { var inp int32 = math.MaxInt32 f32, err := safecast.Convert[float32](inp) if err == nil { t.Errorf("expected error, got %d (%x) -> %.0f", inp, inp, f32) } // but should work to 64 bits types _ = safecast.MustConvert[float64](inp) _ = safecast.MustConvert[uint64](inp) i64 := safecast.MustConvert[int64](inp) // 64 bits variant of same maxint32 should also error out to 32 bits: f32, err = safecast.Convert[float32](i64) if err == nil { t.Errorf("expected error, got %d (%x) -> %.0f", i64, i64, f32) } } // Same as above but checks all the 25->31 bits. func TestFloat32Int32Bounds(t *testing.T) { float32bits := FindNumIntBits[float32](t) float32int32 := int32(1<<(float32bits+1) - 1) // 25 bits is start of error range for i := 0; i < 31-float32bits; i++ { t.Logf("float32int %b %d", float32int32, float32int32) f, err := safecast.Convert[float32](float32int32) t.Logf("float32int -> %.0f", f) if err == nil { t.Errorf("expected error for %d (%x %b) -> %.0f", float32int32, float32int32, float32int32, f) } float32int32 = float32int32<<1 | 1 } } func TestFloat32UInt32Bounds(t *testing.T) { float32bits := FindNumIntBits[float32](t) float32int32 := uint32(1<<(float32bits+1) - 1) // 25 bits is start of error range for i := 0; i < 32-float32bits; i++ { t.Logf("float32int %b %d", float32int32, float32int32) f, err := safecast.Convert[float32](float32int32) t.Logf("float32int -> %.0f", f) if err == nil { t.Errorf("expected error for %d (%x %b) -> %.0f", float32int32, float32int32, float32int32, f) } float32int32 = float32int32<<1 | 1 } } func TestConvert(t *testing.T) { var inp uint32 = 42 out, err := safecast.Convert[int8](inp) t.Logf("Out is %T: %v", out, out) if err != nil { t.Errorf("unexpected error: %v", err) } if out != 42 { t.Errorf("unexpected value: %v", out) } inp = 129 _, err = safecast.Convert[int8](inp) t.Logf("Got err: %v", err) if err == nil { t.Errorf("expected error") } inp2 := int32(-1) _, err = safecast.Convert[uint8](inp2) t.Logf("Got err: %v", err) if err == nil { t.Errorf("expected error") } out, err = safecast.Convert[int8](inp2) t.Logf("Out is %T: %v", out, out) if err != nil { t.Errorf("unexpected error: %v", err) } if out != -1 { t.Errorf("unexpected value: %v", out) } inp2 = -129 _, err = safecast.Convert[uint8](inp2) t.Logf("Got err: %v", err) if err == nil { t.Errorf("expected error") } var a uint16 = 65535 x, err := safecast.Convert[int16](a) if err == nil { t.Errorf("expected error, %d %d", a, x) } b := int8(-1) y, err := safecast.Convert[uint](b) if err == nil { t.Errorf("expected error, %d %d", b, y) } up := uintptr(42) b, err = safecast.Convert[int8](up) if err != nil { t.Errorf("unexpected err: %v", err) } if b != 42 { t.Errorf("unexpected value: %v", b) } b = -1 _, err = safecast.Convert[uintptr](b) if err == nil { t.Errorf("expected err") } ub := safecast.MustTruncate[uint8](255.6) if ub != 255 { t.Errorf("unexpected value: %v", ub) } ub = safecast.MustConvert[uint8](int64(255)) // shouldn't panic if ub != 255 { t.Errorf("unexpected value: %v", ub) } } // a bit of copy pasta from the previous test. func TestConvInteger(t *testing.T) { var maxU64 uint64 = math.MaxUint64 _ = safecast.MustConv[uint64](maxU64) // shouldn't panic var inp uint32 = 42 out, err := safecast.Conv[int8](inp) t.Logf("Out is %T: %v", out, out) if err != nil { t.Errorf("unexpected error: %v", err) } if out != 42 { t.Errorf("unexpected value: %v", out) } inp = 129 _, err = safecast.Conv[int8](inp) t.Logf("Got err: %v", err) if err == nil { t.Errorf("expected error") } inp2 := int32(-1) _, err = safecast.Conv[uint8](inp2) t.Logf("Got err: %v", err) if err == nil { t.Errorf("expected error") } out, err = safecast.Conv[int8](inp2) t.Logf("Out is %T: %v", out, out) if err != nil { t.Errorf("unexpected error: %v", err) } if out != -1 { t.Errorf("unexpected value: %v", out) } inp2 = -129 _, err = safecast.Conv[uint8](inp2) t.Logf("Got err: %v", err) if err == nil { t.Errorf("expected error") } var mi int32 = math.MaxInt32 _ = safecast.MustConv[int32](mi) // self on maxint32 shouldn't panic. } func TestNaNOk(t *testing.T) { n32, err := safecast.Convert[float32](math.NaN()) if err != nil { t.Errorf("unexpected error: %v", err) } if n32 == n32 { t.Errorf("unexpected NaN handling: %v", n32) } n64, err := safecast.Convert[float64](n32) if err != nil { t.Errorf("unexpected error: %v", err) } if n64 == n64 { t.Errorf("unexpected NaN handling: %v", n64) } } type myFloat float64 // Also tests ~float (#19). func TestPlusInfiniteOk(t *testing.T) { inf64 := myFloat(math.Inf(1)) inf32, err := safecast.Convert[float32](inf64) if err != nil { t.Errorf("unexpected 64->32 error %f -> %f: %v", inf64, inf32, err) } inf32 = float32(math.Inf(1)) outf64, err := safecast.Convert[float64](inf32) if err != nil { t.Errorf("unexpected 32->64 error %f -> %f: %v", inf32, outf64, err) } } func TestPlusInfiniteToInt(t *testing.T) { inf64 := math.Inf(1) intInf, err := safecast.Convert[uint64](inf64) if err == nil { t.Errorf("expected inf to int error %f -> %d", inf64, intInf) } } func TestMinusInfiniteOk(t *testing.T) { inf64 := math.Inf(-1) inf32, err := safecast.Convert[float32](inf64) if err != nil { t.Errorf("unexpected 64->32 error %f -> %f: %v", inf64, inf32, err) } t.Logf("inf32: %f", inf32) inf32 = float32(math.Inf(-1)) outf64, err := safecast.Convert[float64](inf32) if err != nil { t.Errorf("unexpected 32->64 error %f -> %f: %v", inf32, outf64, err) } } func TestPanicMustRound(t *testing.T) { defer func() { r := recover() if r == nil { t.Errorf("expected panic") } else { expected := "safecast: out of range for 255.5 (float32) to uint8" if r != expected { t.Errorf("unexpected panic: %q wanted %q", r, expected) } } }() safecast.MustRound[uint8](float32(255.5)) } func TestPanicMustTruncate(t *testing.T) { defer func() { r := recover() if r == nil { t.Errorf("expected panic") } else { expected := "safecast: out of range for -1.5 (float32) to uint8" if r != expected { t.Errorf("unexpected panic: %q wanted %q", r, expected) } } }() safecast.MustTruncate[uint8](float32(-1.5)) } func TestPanicMustConvert(t *testing.T) { defer func() { r := recover() if r == nil { t.Errorf("expected panic") } else { expected := "safecast: out of range for 256 (int) to uint8" if r != expected { t.Errorf("unexpected panic: %q wanted %q", r, expected) } } }() safecast.MustConvert[uint8](256) } func TestPanicMustConv(t *testing.T) { defer func() { r := recover() if r == nil { t.Errorf("expected panic") } else { expected := "safecast: out of range for 256 (int) to uint8" if r != expected { t.Errorf("unexpected panic: %q wanted %q", r, expected) } } }() safecast.MustConv[uint8](256) } func Example() { var in int16 = 256 // will error out out, err := safecast.Conv[uint8](in) fmt.Println(out, err) // will be fine out = safecast.MustRound[uint8](255.4) fmt.Println(out) // Also fine out = safecast.MustTruncate[uint8](255.6) fmt.Println(out) // Output: 0 out of range // 255 // 255 } func ExampleMustRound() { defer func() { if r := recover(); r != nil { fmt.Println("panic:", r) } }() out := safecast.MustRound[int8](-128.6) fmt.Println("not reached", out) // not reached // Output: panic: safecast: out of range for -128.6 (float64) to int8 }